Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ allprojects {
jsr305Version = "3.0.2"
junitVersion = "5.13.3"
checkstyleVersion = "10.4"
immutablesVersion = "2.10.1"
}
}

Expand Down
6 changes: 6 additions & 0 deletions extractor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@ 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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,12 @@ public OffsetDateTime offsetDateTime() {
public boolean isApproximation() {
return isApproximation;
}

@Override
public String toString() {
return "DateWrapper{"
+ "offsetDateTime=" + offsetDateTime
+ ", isApproximation=" + isApproximation
+ '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ public long getLength() {

@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
Expand Down Expand Up @@ -170,7 +171,7 @@ public List<AudioStream> getAudioStreams() throws ExtractionException {

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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,11 +11,18 @@
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() {
}

Expand Down
116 changes: 88 additions & 28 deletions extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,50 +44,113 @@ public RegexException(final String message) {
}
}

@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) {
Expand All @@ -110,14 +173,11 @@ public static Matcher matchMultiplePatterns(final Pattern[] patterns, final Stri
}

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ public static long mixedNumberWordToLong(final String numberWord)
* @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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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;
Expand Down Expand Up @@ -168,6 +169,13 @@ public static void assertContains(
"'" + 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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { }
Original file line number Diff line number Diff line change
@@ -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);
}
}
});
}
}
Loading
Loading