pull/1324/merge
David 2025-07-17 23:13:13 -07:00 zatwierdzone przez GitHub
commit 362c25720a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
15 zmienionych plików z 620 dodań i 123 usunięć

Wyświetl plik

@ -31,6 +31,7 @@ allprojects {
jsr305Version = "3.0.2"
junitVersion = "5.13.3"
checkstyleVersion = "10.4"
immutablesVersion = "2.10.1"
}
}

Wyświetl plik

@ -52,6 +52,12 @@ dependencies {
testImplementation "com.squareup.okhttp3:okhttp:4.12.0"
testImplementation 'com.google.code.gson:gson:2.13.1'
testImplementation "org.immutables:value:$immutablesVersion"
testAnnotationProcessor "org.immutables:value:$immutablesVersion"
}
repositories {
mavenCentral()
}
protobuf {

Wyświetl plik

@ -69,4 +69,12 @@ public class DateWrapper implements Serializable {
public boolean isApproximation() {
return isApproximation;
}
@Override
public String toString() {
return "DateWrapper{"
+ "offsetDateTime=" + offsetDateTime
+ ", isApproximation=" + isApproximation
+ '}';
}
}

Wyświetl plik

@ -121,7 +121,8 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
@Override
public long getTimeStamp() throws ParsingException {
return getTimestampSeconds("(#t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)");
final var timestamp = getTimestampSeconds("(#t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)");
return timestamp == -2 ? 0 : timestamp;
}
@Override
@ -170,7 +171,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
try {
final JsonArray transcodings = track.getObject("media")
.getArray("transcodings");
.getArray("transcodings");
if (!isNullOrEmpty(transcodings)) {
// Get information about what stream formats are available
extractAudioStreams(transcodings, audioStreams);

Wyświetl plik

@ -1,5 +1,7 @@
package org.schabi.newpipe.extractor.services.soundcloud.linkHandler;
import java.util.regex.Pattern;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
@ -9,11 +11,18 @@ import org.schabi.newpipe.extractor.utils.Utils;
public final class SoundcloudStreamLinkHandlerFactory extends LinkHandlerFactory {
private static final SoundcloudStreamLinkHandlerFactory INSTANCE
= new SoundcloudStreamLinkHandlerFactory();
private static final String URL_PATTERN = "^https?://(www\\.|m\\.|on\\.)?"
+ "soundcloud.com/[0-9a-z_-]+"
+ "/(?!(tracks|albums|sets|reposts|followers|following)/?$)[0-9a-z_-]+/?([#?].*)?$";
private static final String API_URL_PATTERN = "^https?://api-v2\\.soundcloud.com"
+ "/(tracks|albums|sets|reposts|followers|following)/([0-9a-z_-]+)/";
private static final Pattern URL_PATTERN = Pattern.compile(
"^https?://(?:www\\.|m\\.|on\\.)?"
+ "soundcloud.com/[0-9a-z_-]+"
+ "/(?!(?:tracks|albums|sets|reposts|followers|following)/?$)[0-9a-z_-]+/?(?:[#?].*)?$"
);
private static final Pattern API_URL_PATTERN = Pattern.compile(
"^https?://api-v2\\.soundcloud.com"
+ "/(tracks|albums|sets|reposts|followers|following)/([0-9a-z_-]+)/"
);
private SoundcloudStreamLinkHandlerFactory() {
}

Wyświetl plik

@ -44,50 +44,113 @@ public final class Parser {
}
}
@Nonnull
public static Matcher matchOrThrow(@Nonnull final Pattern pattern,
final String input) throws RegexException {
final Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
return matcher;
} else {
String errorMessage = "Failed to find pattern \"" + pattern.pattern() + "\"";
if (input.length() <= 1024) {
errorMessage += " inside of \"" + input + "\"";
}
throw new RegexException(errorMessage);
}
}
/**
* Matches group 1 of the given pattern against the input
* and returns the matched group
*
* @param pattern The regex pattern to match.
* @param input The input string to match against.
* @return The matching group as a string.
* @throws RegexException If the pattern does not match the input or if the group is not found.
*/
@Nonnull
public static String matchGroup1(final String pattern, final String input)
throws RegexException {
return matchGroup(pattern, input, 1);
}
public static String matchGroup1(final Pattern pattern,
final String input) throws RegexException {
/**
* Matches group 1 of the given pattern against the input
* and returns the matched group
*
* @param pattern The regex pattern to match.
* @param input The input string to match against.
* @return The matching group as a string.
* @throws RegexException If the pattern does not match the input or if the group is not found.
*/
@Nonnull
public static String matchGroup1(final Pattern pattern, final String input)
throws RegexException {
return matchGroup(pattern, input, 1);
}
public static String matchGroup(final String pattern,
final String input,
final int group) throws RegexException {
/**
* Matches the specified group of the given pattern against the input,
* and returns the matched group
*
* @param pattern The regex pattern to match.
* @param input The input string to match against.
* @param group The group number to retrieve (1-based index).
* @return The matching group as a string.
* @throws RegexException If the pattern does not match the input or if the group is not found.
*/
@Nonnull
public static String matchGroup(final String pattern, final String input, final int group)
throws RegexException {
return matchGroup(Pattern.compile(pattern), input, group);
}
public static String matchGroup(@Nonnull final Pattern pat,
/**
* Matches the specified group of the given pattern against the input,
* and returns the matched group
*
* @param pattern The regex pattern to match.
* @param input The input string to match against.
* @param group The group number to retrieve (1-based index).
* @return The matching group as a string.
* @throws RegexException If the pattern does not match the input or if the group is not found.
*/
@Nonnull
public static String matchGroup(@Nonnull final Pattern pattern,
final String input,
final int group) throws RegexException {
final Matcher matcher = pat.matcher(input);
final boolean foundMatch = matcher.find();
if (foundMatch) {
return matcher.group(group);
} else {
// only pass input to exception message when it is not too long
if (input.length() > 1024) {
throw new RegexException("Failed to find pattern \"" + pat.pattern() + "\"");
} else {
throw new RegexException("Failed to find pattern \"" + pat.pattern()
+ "\" inside of \"" + input + "\"");
}
}
final int group)
throws RegexException {
return matchOrThrow(pattern, input).group(group);
}
/**
* Matches multiple patterns against the input string and
* returns the first successful matcher
*
* @param patterns The array of regex patterns to match.
* @param input The input string to match against.
* @return A {@code Matcher} for the first successful match.
* @throws RegexException If no patterns match the input or if {@code patterns} is empty.
*/
public static String matchGroup1MultiplePatterns(final Pattern[] patterns, final String input)
throws RegexException {
return matchMultiplePatterns(patterns, input).group(1);
}
/**
* Matches multiple patterns against the input string and
* returns the first successful matcher
*
* @param patterns The array of regex patterns to match.
* @param input The input string to match against.
* @return A {@code Matcher} for the first successful match.
* @throws RegexException If no patterns match the input or if {@code patterns} is empty.
*/
public static Matcher matchMultiplePatterns(final Pattern[] patterns, final String input)
throws RegexException {
Parser.RegexException exception = null;
for (final Pattern pattern : patterns) {
final Matcher matcher = pattern.matcher(input);
RegexException exception = null;
for (final var pattern : patterns) {
final var matcher = pattern.matcher(input);
if (matcher.find()) {
return matcher;
} else if (exception == null) {
@ -110,14 +173,11 @@ public final class Parser {
}
public static boolean isMatch(final String pattern, final String input) {
final Pattern pat = Pattern.compile(pattern);
final Matcher mat = pat.matcher(input);
return mat.find();
return isMatch(Pattern.compile(pattern), input);
}
public static boolean isMatch(@Nonnull final Pattern pattern, final String input) {
final Matcher mat = pattern.matcher(input);
return mat.find();
return pattern.matcher(input).find();
}
@Nonnull

Wyświetl plik

@ -110,6 +110,16 @@ public final class Utils {
* @param url the url to be tested
*/
public static void checkUrl(final String pattern, final String url) throws ParsingException {
checkUrl(Pattern.compile(pattern), url);
}
/**
* Check if the url matches the pattern.
*
* @param pattern the pattern that will be used to check the url
* @param url the url to be tested
*/
public static void checkUrl(final Pattern pattern, final String url) throws ParsingException {
if (isNullOrEmpty(url)) {
throw new IllegalArgumentException("Url can't be null or empty");
}

Wyświetl plik

@ -8,6 +8,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
@ -168,6 +169,13 @@ public class ExtractorAsserts {
"'" + shouldBeContained + "' should be contained inside '" + container + "'");
}
public static void assertMatches(final Pattern pattern, final String input) {
assertNotNull(pattern, "pattern is null");
assertNotNull(input, "input is null");
assertTrue(pattern.matcher(input).find(),
"Pattern '" + pattern + "' not found in input '" + input + "'");
}
public static void assertTabsContain(@Nonnull final List<ListLinkHandler> tabs,
@Nonnull final String... expectedTabs) {
final Set<String> tabSet = tabs.stream()

Wyświetl plik

@ -0,0 +1,30 @@
package org.schabi.newpipe.extractor;
import org.immutables.value.Value;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
// CHECKSTYLE:OFF
/**
* Custom style for generated Immutables.
* See <a href="https://immutables.github.io/style.html">Style</a>.
* <p>
* - Abstract types start with 'I' (e.g., IExample).<p>
* - Concrete immutable types do not have a prefix (e.g., Example).<p>
* - Getters are prefixed with 'get', 'is', or no prefix.<p>
* - <a href="https://immutables.github.io/immutable.html#strict-builder">Strict builder pattern is enforced.</a><p>
*/
// CHECKSTYLE:ON
@Target({ElementType.PACKAGE, ElementType.TYPE})
@Value.Style(
get = {"get*", "is*", "*"}, // Methods matching these prefixes will be used as getters.
// Methods matching these patterns can NOT be used as setters.
typeAbstract = {"I*"}, // Abstract types start with I
typeImmutable = "*", // Generated concrete Immutable types will not have the I prefix
visibility = Value.Style.ImplementationVisibility.PUBLIC,
strictBuilder = true,
defaultAsDefault = true, // https://immutables.github.io/immutable.html#default-attributes
jdkOnly = true
)
public @interface ImmutableStyle { }

Wyświetl plik

@ -0,0 +1,57 @@
package org.schabi.newpipe.extractor.services;
import org.junit.jupiter.api.Test;
import org.schabi.newpipe.extractor.ExtractorAsserts;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.services.testcases.SoundcloudStreamExtractorTestCase;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import java.util.List;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.*;
public abstract class ParameterisedDefaultSoundcloudStreamExtractorTest
extends ParameterisedDefaultStreamExtractorTest<SoundcloudStreamExtractorTestCase> {
protected ParameterisedDefaultSoundcloudStreamExtractorTest(SoundcloudStreamExtractorTestCase testCase) {
super(testCase);
}
final Pattern mp3CdnUrlPattern = Pattern.compile("-media\\.sndcdn\\.com/[a-zA-Z0-9]{12}\\.128\\.mp3");
@Override
@Test
public void testAudioStreams() throws Exception {
super.testAudioStreams();
final List<AudioStream> audioStreams = extractor.getAudioStreams();
assertEquals(3, audioStreams.size()); // 2 MP3 streams (1 progressive, 1 HLS) and 1 OPUS
audioStreams.forEach(audioStream -> {
final DeliveryMethod deliveryMethod = audioStream.getDeliveryMethod();
final String mediaUrl = audioStream.getContent();
if (audioStream.getFormat() == MediaFormat.OPUS) {
assertSame(DeliveryMethod.HLS, deliveryMethod,
"Wrong delivery method for stream " + audioStream.getId() + ": "
+ deliveryMethod);
// Assert it's an OPUS 64 kbps media playlist URL which comes from an HLS
// SoundCloud CDN
ExtractorAsserts.assertContains("-hls-opus-media.sndcdn.com", mediaUrl);
ExtractorAsserts.assertContains(".64.opus", mediaUrl);
} else if (audioStream.getFormat() == MediaFormat.MP3) {
if (deliveryMethod == DeliveryMethod.PROGRESSIVE_HTTP) {
// Assert it's a MP3 128 kbps media URL which comes from a progressive
// SoundCloud CDN
ExtractorAsserts.assertMatches(mp3CdnUrlPattern, mediaUrl);
} else if (deliveryMethod == DeliveryMethod.HLS) {
// Assert it's a MP3 128 kbps media HLS playlist URL which comes from an HLS
// SoundCloud CDN
ExtractorAsserts.assertContains("-hls-media.sndcdn.com", mediaUrl);
ExtractorAsserts.assertContains(".128.mp3", mediaUrl);
} else {
fail("Wrong delivery method for stream " + audioStream.getId() + ": "
+ deliveryMethod);
}
}
});
}
}

Wyświetl plik

@ -0,0 +1,88 @@
package org.schabi.newpipe.extractor.services;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInstance;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.services.testcases.DefaultStreamExtractorTestCase;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Locale;
/**
* Test for {@link StreamExtractor}
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class ParameterisedDefaultStreamExtractorTest<TTestCase extends DefaultStreamExtractorTestCase> extends DefaultStreamExtractorTest {
protected TTestCase testCase;
protected StreamExtractor extractor;
protected ParameterisedDefaultStreamExtractorTest(TTestCase testCase)
{
this.testCase = testCase;
}
@BeforeAll
public void setUp() throws Exception {
if (extractor != null) {
throw new IllegalStateException("extractor already initialized before BeforeAll");
}
NewPipe.init(DownloaderTestImpl.getInstance());
extractor = testCase.service().getStreamExtractor(testCase.url());
extractor.fetchPage();
}
///
/// DefaultExtractorTest overrides
///
@Override public StreamExtractor extractor() throws Exception { return extractor; }
@Override public StreamingService expectedService() throws Exception { return testCase.service(); }
@Override public String expectedName() throws Exception { return testCase.name(); }
@Override public String expectedId() throws Exception { return testCase.id(); }
@Override public String expectedUrlContains() throws Exception { return testCase.urlContains(); }
@Override public String expectedOriginalUrlContains() throws Exception { return testCase.originalUrlContains(); }
///
/// DefaultStreamExtractorTest overrides
///
@Override public StreamType expectedStreamType() { return testCase.streamType(); }
@Override public String expectedUploaderName() { return testCase.uploaderName(); }
@Override public String expectedUploaderUrl() { return testCase.uploaderUrl(); }
@Override public boolean expectedUploaderVerified() { return testCase.uploaderVerified(); }
@Override public long expectedUploaderSubscriberCountAtLeast() { return testCase.uploaderSubscriberCountAtLeast(); }
@Override public String expectedSubChannelName() { return testCase.subChannelName(); }
@Override public String expectedSubChannelUrl() { return testCase.subChannelUrl(); }
@Override public boolean expectedDescriptionIsEmpty() { return testCase.descriptionIsEmpty(); }
@Override public List<String> expectedDescriptionContains() { return testCase.descriptionContains(); }
@Override public long expectedLength() { return testCase.length(); }
@Override public long expectedTimestamp() { return testCase.timestamp(); }
@Override public long expectedViewCountAtLeast() { return testCase.viewCountAtLeast(); }
@Override @Nullable public String expectedUploadDate() { return testCase.uploadDate(); }
@Override @Nullable public String expectedTextualUploadDate() { return testCase.textualUploadDate(); }
@Override public long expectedLikeCountAtLeast() { return testCase.likeCountAtLeast(); }
@Override public long expectedDislikeCountAtLeast() { return testCase.dislikeCountAtLeast(); }
@Override public boolean expectedHasRelatedItems() { return testCase.hasRelatedItems(); }
@Override public int expectedAgeLimit() { return testCase.ageLimit(); }
@Override @Nullable public String expectedErrorMessage() { return testCase.errorMessage(); }
@Override public boolean expectedHasVideoStreams() { return testCase.hasVideoStreams(); }
@Override public boolean expectedHasAudioStreams() { return testCase.hasAudioStreams(); }
@Override public boolean expectedHasSubtitles() { return testCase.hasSubtitles(); }
@Override @Nullable public String expectedDashMpdUrlContains() { return testCase.dashMpdUrlContains(); }
@Override public boolean expectedHasFrames() { return testCase.hasFrames(); }
@Override public String expectedHost() { return testCase.host(); }
@Override public StreamExtractor.Privacy expectedPrivacy() { return testCase.privacy(); }
@Override public String expectedCategory() { return testCase.category(); }
@Override public String expectedLicence() { return testCase.licence(); }
@Override public Locale expectedLanguageInfo() { return testCase.languageInfo(); }
@Override public List<String> expectedTags() { return testCase.tags(); }
@Override public String expectedSupportInfo() { return testCase.supportInfo(); }
@Override public int expectedStreamSegmentsCount() { return testCase.streamSegmentsCount(); }
@Override public List<MetaInfo> expectedMetaInfo() { return testCase.metaInfo(); }
}

Wyświetl plik

@ -2,43 +2,44 @@ package org.schabi.newpipe.extractor.services.soundcloud;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.schabi.newpipe.downloader.DownloaderTestImpl;
import org.schabi.newpipe.extractor.ExtractorAsserts;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.services.ParameterisedDefaultSoundcloudStreamExtractorTest;
import org.schabi.newpipe.extractor.services.testcases.SoundcloudStreamExtractorTestCase;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.fail;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
public class SoundcloudStreamExtractorTest {
private static final String SOUNDCLOUD = "https://soundcloud.com/";
public static class SoundcloudGeoRestrictedTrack extends DefaultStreamExtractorTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SoundcloudGeoRestrictedTrack extends DefaultStreamExtractorTest {
private static final String ID = "one-touch";
private static final String UPLOADER = SOUNDCLOUD + "jessglynne";
private static final int TIMESTAMP = 0;
private static final String URL = UPLOADER + "/" + ID + "#t=" + TIMESTAMP;
private static StreamExtractor extractor;
private StreamExtractor extractor;
@BeforeAll
public static void setUp() throws Exception {
public void setUp() throws Exception {
if (extractor != null) {
throw new IllegalStateException("extractor already initialized before BeforeAll");
}
NewPipe.init(DownloaderTestImpl.getInstance());
extractor = SoundCloud.getStreamExtractor(URL);
try {
@ -84,15 +85,20 @@ public class SoundcloudStreamExtractorTest {
}
}
public static class SoundcloudGoPlusTrack extends DefaultStreamExtractorTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SoundcloudGoPlusTrack extends DefaultStreamExtractorTest {
private static final String ID = "places";
private static final String UPLOADER = SOUNDCLOUD + "martinsolveig";
private static final int TIMESTAMP = 0;
private static final String URL = UPLOADER + "/" + ID + "#t=" + TIMESTAMP;
private static StreamExtractor extractor;
private StreamExtractor extractor;
@BeforeAll
public static void setUp() throws Exception {
public void setUp() throws Exception {
if (extractor != null) {
throw new IllegalStateException("extractor already initialized before BeforeAll");
}
NewPipe.init(DownloaderTestImpl.getInstance());
extractor = SoundCloud.getStreamExtractor(URL);
try {
@ -133,7 +139,6 @@ public class SoundcloudStreamExtractorTest {
@Override public long expectedDislikeCountAtLeast() { return -1; }
@Override public boolean expectedHasAudioStreams() { return false; }
@Override public boolean expectedHasVideoStreams() { return false; }
@Override public boolean expectedHasRelatedItems() { return true; }
@Override public boolean expectedHasSubtitles() { return false; }
@Override public boolean expectedHasFrames() { return false; }
@Override public int expectedStreamSegmentsCount() { return 0; }
@ -141,82 +146,79 @@ public class SoundcloudStreamExtractorTest {
@Override public String expectedCategory() { return "Dance"; }
}
static class CreativeCommonsOpenMindsEp21 extends DefaultStreamExtractorTest {
private static final String ID = "open-minds-ep-21-dr-beth-harris-and-dr-steven-zucker-of-smarthistory";
private static final String UPLOADER = SOUNDCLOUD + "wearecc";
private static final int TIMESTAMP = 69;
private static final String URL = UPLOADER + "/" + ID + "#t=" + TIMESTAMP;
private static StreamExtractor extractor;
@BeforeAll
static void setUp() throws Exception {
NewPipe.init(DownloaderTestImpl.getInstance());
extractor = SoundCloud.getStreamExtractor(URL);
extractor.fetchPage();
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SoundcloudTrackTest1 extends ParameterisedDefaultSoundcloudStreamExtractorTest {
public SoundcloudTrackTest1() {
super(
SoundcloudStreamExtractorTestCase.builder()
.url("https://soundcloud.com/user-904087338/nether#t=45")
.id("2057071056")
.name("Nether")
.uploaderName("Ambient Ghost")
.uploadDate("2025-03-18 12:19:19.000")
.textualUploadDate("2025-03-18 12:19:19")
.length(145)
.licence("all-rights-reserved")
.descriptionIsEmpty(true)
.viewCountAtLeast(1029)
.likeCountAtLeast(12)
.build()
);
}
}
@Override public StreamExtractor extractor() { return extractor; }
@Override public StreamingService expectedService() { return SoundCloud; }
@Override public String expectedName() { return "Open Minds, Ep 21: Dr. Beth Harris and Dr. Steven Zucker of Smarthistory"; }
@Override public String expectedId() { return "1356023209"; }
@Override public String expectedUrlContains() { return UPLOADER + "/" + ID; }
@Override public String expectedOriginalUrlContains() { return URL; }
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SoundcloudTrackTest2 extends ParameterisedDefaultSoundcloudStreamExtractorTest {
public SoundcloudTrackTest2() {
super(
SoundcloudStreamExtractorTestCase.builder()
.url("https://soundcloud.com/kaleidocollective/2subtact-splinter")
.id("230211123")
.name("Subtact - Splinter")
.uploaderVerified(true)
.uploaderName("Kaleido")
.uploadDate("2015-10-26 20:55:30.000")
.textualUploadDate("2015-10-26 20:55:30")
.length(225)
.licence("all-rights-reserved")
.descriptionIsEmpty(false)
.addDescriptionContains("follow @subtact",
"-twitter:",
"twitter.com/Subtact",
"-facebook:",
"www.facebook.com/subtact?fref=ts")
.viewCountAtLeast(157874)
.likeCountAtLeast(3142)
.category("ʕ•ᴥ•ʔ")
.build()
);
}
}
@Override public StreamType expectedStreamType() { return StreamType.AUDIO_STREAM; }
@Override public String expectedUploaderName() { return "Creative Commons"; }
@Override public String expectedUploaderUrl() { return UPLOADER; }
@Override public List<String> expectedDescriptionContains() {
return Arrays.asList("Smarthistory is a center for public art history",
"experts who want to share their knowledge with learners around the world",
"Available for use under the CC BY 3.0 license"); }
@Override public long expectedLength() { return 1500; }
@Override public long expectedTimestamp() { return TIMESTAMP; }
@Override public long expectedViewCountAtLeast() { return 15000; }
@Nullable @Override public String expectedUploadDate() { return "2022-10-03 18:49:49.000"; }
@Nullable @Override public String expectedTextualUploadDate() { return "2022-10-03 18:49:49"; }
@Override public long expectedLikeCountAtLeast() { return 10; }
@Override public long expectedDislikeCountAtLeast() { return -1; }
@Override public boolean expectedHasRelatedItems() { return false; }
@Override public boolean expectedHasVideoStreams() { return false; }
@Override public boolean expectedHasSubtitles() { return false; }
@Override public boolean expectedHasFrames() { return false; }
@Override public int expectedStreamSegmentsCount() { return 0; }
@Override public String expectedLicence() { return "cc-by"; }
@Override
@Test
public void testAudioStreams() throws Exception {
super.testAudioStreams();
final List<AudioStream> audioStreams = extractor.getAudioStreams();
assertEquals(3, audioStreams.size()); // 2 MP3 streams (1 progressive, 1 HLS) and 1 OPUS
audioStreams.forEach(audioStream -> {
final DeliveryMethod deliveryMethod = audioStream.getDeliveryMethod();
final String mediaUrl = audioStream.getContent();
if (audioStream.getFormat() == MediaFormat.OPUS) {
assertSame(DeliveryMethod.HLS, deliveryMethod,
"Wrong delivery method for stream " + audioStream.getId() + ": "
+ deliveryMethod);
// Assert it's an OPUS 64 kbps media playlist URL which comes from an HLS
// SoundCloud CDN
ExtractorAsserts.assertContains("-hls-opus-media.sndcdn.com", mediaUrl);
ExtractorAsserts.assertContains(".64.opus", mediaUrl);
} else if (audioStream.getFormat() == MediaFormat.MP3) {
if (deliveryMethod == DeliveryMethod.PROGRESSIVE_HTTP) {
// Assert it's a MP3 128 kbps media URL which comes from a progressive
// SoundCloud CDN
ExtractorAsserts.assertContains("-media.sndcdn.com/cyaz0oXJYbdt.128.mp3",
mediaUrl);
} else if (deliveryMethod == DeliveryMethod.HLS) {
// Assert it's a MP3 128 kbps media HLS playlist URL which comes from an HLS
// SoundCloud CDN
ExtractorAsserts.assertContains("-hls-media.sndcdn.com", mediaUrl);
ExtractorAsserts.assertContains(".128.mp3", mediaUrl);
} else {
fail("Wrong delivery method for stream " + audioStream.getId() + ": "
+ deliveryMethod);
}
}
});
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SoundcloudTrackTest3 extends ParameterisedDefaultSoundcloudStreamExtractorTest {
public SoundcloudTrackTest3() {
super(SoundcloudStreamExtractorTestCase.builder()
.url("https://soundcloud.com/wearecc/open-minds-ep-21-dr-beth-harris-and-dr-steven-zucker-of-smarthistory")
.id("1356023209")
.name("Open Minds, Ep 21: Dr. Beth Harris and Dr. Steven Zucker of Smarthistory")
.uploaderName("Creative Commons")
.uploadDate("2022-10-03 18:49:49.000")
.textualUploadDate("2022-10-03 18:49:49")
.hasRelatedItems(false)
.length(1500)
.licence("cc-by")
.descriptionIsEmpty(false)
.addDescriptionContains("Smarthistory is a center for public art history",
"experts who want to share their knowledge with learners around the world",
"Available for use under the CC BY 3.0 license")
.viewCountAtLeast(15000)
.likeCountAtLeast(10)
.build()
);
}
}
}

Wyświetl plik

@ -0,0 +1,17 @@
package org.schabi.newpipe.extractor.services.testcases;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.services.DefaultExtractorTest;
/**
* Test case base class for {@link DefaultExtractorTest}
*/
public interface DefaultExtractorTestCase {
public abstract StreamingService service();
public abstract String name();
public abstract String id();
public abstract String url();
public default String originalUrl() { return url(); }
public default String urlContains() { return url();}
public default String originalUrlContains() { return originalUrl(); }
}

Wyświetl plik

@ -0,0 +1,90 @@
package org.schabi.newpipe.extractor.services.testcases;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import javax.annotation.Nullable;
/**
* Test case base class for {@link org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest}<p>
* Ideally you will supply a regex matcher that the url that will automatically parse
* certain values for the tests.<p>
* Ones that can't be derived from the url should be overridden in the test case.
*/
public interface DefaultStreamExtractorTestCase extends DefaultExtractorTestCase {
/**
* Returns matcher for the URL<p>
* Implementations should throw IllegalArgumentException if the pattern does not match
*/
Matcher urlMatcher();
default String getGroupFromUrl(String groupName) {
return urlMatcher().group(groupName);
}
default int getGroupEndIndexFromUrl(String groupName) {
return urlMatcher().end(groupName);
}
default String id() { return getGroupFromUrl("id"); }
default String uploader() { return getGroupFromUrl("uploader"); }
StreamType streamType();
String uploaderName();
default String uploaderUrl() {
final int groupEndIndex = getGroupEndIndexFromUrl("uploader");
if (groupEndIndex < 0) {
return ""; // no uploader group found in url
}
return url().substring(0, groupEndIndex);
}
default boolean uploaderVerified() { return false; }
default long uploaderSubscriberCountAtLeast() { return UNKNOWN_SUBSCRIBER_COUNT; } // default: unknown
default String subChannelName() { return ""; } // default: no subchannel
default String subChannelUrl() { return ""; } // default: no subchannel
default boolean descriptionIsEmpty() { return false; } // default: description is not empty
List<String> descriptionContains();
long length();
default int timestamp() { return 0; } // default: there is no timestamp
long viewCountAtLeast();
@Nullable
String uploadDate(); // format: yyyy-MM-dd HH:mm:ss.SSS
@Nullable
String textualUploadDate();
long likeCountAtLeast();
long dislikeCountAtLeast();
default boolean hasRelatedItems() { return true; } // default: there are related videos
default int ageLimit() { return StreamExtractor.NO_AGE_LIMIT; } // default: no limit
@Nullable
default String errorMessage() { return null; } // default: no error message
default boolean hasVideoStreams() { return true; } // default: there are video streams
default boolean hasAudioStreams() { return true; } // default: there are audio streams
default boolean hasSubtitles() { return true; } // default: there are subtitles streams
@Nullable
default String dashMpdUrlContains() { return null; } // default: no dash mpd
default boolean hasFrames() { return true; } // default: there are frames
@Nullable
default String host() { return ""; } // default: no host for centralized platforms
@Nullable
default StreamExtractor.Privacy privacy() { return StreamExtractor.Privacy.PUBLIC; } // default: public
default String category() { return ""; } // default: no category
default String licence() { return ""; } // default: no licence
@Nullable
default Locale languageInfo() { return null; } // default: no language info available
@Nullable
default List<String> tags() { return Collections.emptyList(); } // default: no tags
@Nullable
default String supportInfo() { return ""; } // default: no support info available
default int streamSegmentsCount() { return -1; } // return 0 or greater to test (default is -1 to ignore)
@Nullable
default List<MetaInfo> metaInfo() { return Collections.emptyList(); } // default: no metadata info available
}

Wyświetl plik

@ -0,0 +1,110 @@
package org.schabi.newpipe.extractor.services.testcases;
import org.immutables.value.Value;
import org.schabi.newpipe.extractor.ImmutableStyle;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.Parser.RegexException;
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudStreamExtractorTest;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// CHECKSTYLE:OFF
/**
* Immutable definition of {@link DefaultStreamExtractorTestCase}
* for {@link SoundcloudStreamExtractorTest} streams
* @see SoundcloudStreamExtractorTest
*/
// CHECKSTYLE:ON
@ImmutableStyle
@Value.Immutable
public interface ISoundcloudStreamExtractorTestCase extends DefaultStreamExtractorTestCase {
/**
* Pattern for matching soundcloud stream URLs
* Matches URLs of the form:
* <pre>
* <a href="https://soundcloud.com/user-904087338/nether#t=46">https://soundcloud.com/user-904087338/nether#t=46</a>
* </pre>
*/
Pattern URL_PATTERN = Pattern.compile(
"^https?://(?:www\\.|m\\.|on\\.)?soundcloud\\.com/"
+ "(?<uploader>[0-9a-z_-]+)/(?!(?:tracks|albums|sets|reposts|followers|following)/?$)"
+ "(?<id>[0-9a-z_-]+)/?"
+ "([#?](t=(?<timestamp>\\d+)|.*))?$"
);
/**
* Returns the named group from the URL, or an empty string if not found.
*/
default String getGroupFromUrl(String group) {
try {
final String value = urlMatcher().group(group);
return value != null ? value : "";
} catch (IllegalArgumentException | IllegalStateException e) {
return "";
}
}
/**
* Returns the end index of the named group from the URL, or -1 if not found.
*/
default int getGroupEndIndexFromUrl(String group) {
try {
return urlMatcher().end(group);
} catch (IllegalArgumentException | IllegalStateException e) {
return -1;
}
}
default Matcher urlMatcher() {
try {
return Parser.matchOrThrow(URL_PATTERN, url());
} catch (RegexException e) {
throw new IllegalArgumentException("URL does not match expected SoundCloud pattern: " + url(), e);
}
}
default String urlContains() {
final int groupEndIndex = getGroupEndIndexFromUrl("id");
if (groupEndIndex < 0) {
return url(); // no id group found in url
}
return url().substring(0, groupEndIndex);
}
@Value.Derived
default StreamingService service() { return SoundCloud; }
@Value.Derived
@Override
default StreamType streamType() { return StreamType.AUDIO_STREAM; }
@Override
default int timestamp() {
try {
return Integer.parseInt(getGroupFromUrl("timestamp"));
}
catch (NumberFormatException e) {
// Return 0 if no timestamp
return 0;
}
}
@Override
default long dislikeCountAtLeast() { return -1; } // default: soundcloud has no dislikes
@Override
default boolean hasVideoStreams() { return false; } // default: soundcloud has no video streams
@Override
default boolean hasSubtitles() { return false; } // default: soundcloud has no subtitles
default boolean hasFrames() { return false; } // default: soundcloud has no frames
default int streamSegmentsCount() { return 0; }
}