kopia lustrzana https://github.com/TeamNewPipe/NewPipeExtractor
Merge b89ac78090
into f3df599e8a
commit
362c25720a
|
@ -31,6 +31,7 @@ allprojects {
|
|||
jsr305Version = "3.0.2"
|
||||
junitVersion = "5.13.3"
|
||||
checkstyleVersion = "10.4"
|
||||
immutablesVersion = "2.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -69,4 +69,12 @@ public class DateWrapper implements Serializable {
|
|||
public boolean isApproximation() {
|
||||
return isApproximation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DateWrapper{"
|
||||
+ "offsetDateTime=" + offsetDateTime
|
||||
+ ", isApproximation=" + isApproximation
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 { }
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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(); }
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(); }
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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; }
|
||||
}
|
Ładowanie…
Reference in New Issue