diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/localization/DateWrapper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/localization/DateWrapper.java index ffc29a61ce..2f0a2bdd2a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/localization/DateWrapper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/localization/DateWrapper.java @@ -69,4 +69,12 @@ public OffsetDateTime offsetDateTime() { public boolean isApproximation() { return isApproximation; } + + @Override + public String toString() { + return "DateWrapper{" + + "offsetDateTime=" + offsetDateTime + + ", isApproximation=" + isApproximation + + '}'; + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 67cd533e9c..595862bde7 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -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 @@ -170,7 +171,7 @@ public List 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); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudStreamLinkHandlerFactory.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudStreamLinkHandlerFactory.java index e7809c52a1..421022ef0a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudStreamLinkHandlerFactory.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudStreamLinkHandlerFactory.java @@ -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 @@ 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() { } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java index e31e1aff35..410a20592f 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java @@ -88,7 +88,8 @@ public Builder() { } /** - * Set the identifier of the {@link AudioStream}. + * Set the identifier of the {@link AudioStream} which uniquely identifies the stream, + * e.g. for YouTube this would be the itag * *

* It must not be null and should be non empty. @@ -108,14 +109,14 @@ public Builder setId(@Nonnull final String id) { } /** - * Set the content of the {@link AudioStream}. - * + * Set the content or the URL of the {@link AudioStream}, depending on whether isUrl is + * true *

* It must not be null, and should be non empty. *

* * @param content the content of the {@link AudioStream} - * @param isUrl whether the content is a URL + * @param isUrl whether content is the URL or the actual content of e.g. a DASH manifest * @return this {@link Builder} instance */ public Builder setContent(@Nonnull final String content, @@ -126,7 +127,7 @@ public Builder setContent(@Nonnull final String content, } /** - * Set the {@link MediaFormat} used by the {@link AudioStream}. + * Set the {@link MediaFormat} used by the {@link AudioStream}, which can be null * *

* It should be one of the audio {@link MediaFormat}s ({@link MediaFormat#M4A M4A}, @@ -278,16 +279,22 @@ public Builder setItagItem(@Nullable final ItagItem itagItem) { * Build an {@link AudioStream} using the builder's current values. * *

- * The identifier and the content (and so the {@code isUrl} boolean) properties must have + * The identifier and the content (and thus {@code isUrl}) properties must have * been set. *

* * @return a new {@link AudioStream} using the builder's current values - * @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}) or + * @throws IllegalStateException if {@code id}, {@code content} (and thus {@code isUrl}) or * {@code deliveryMethod} have been not set, or have been set as {@code null} */ @Nonnull public AudioStream build() { + validateBuild(); + + return new AudioStream(this); + } + + void validateBuild() { if (id == null) { throw new IllegalStateException( "The identifier of the audio stream has been not set or is null. If you " @@ -305,64 +312,39 @@ public AudioStream build() { "The delivery method of the audio stream has been set as null, which is " + "not allowed. Pass a valid one instead with setDeliveryMethod."); } - - return new AudioStream(id, content, isUrl, mediaFormat, deliveryMethod, averageBitrate, - manifestUrl, audioTrackId, audioTrackName, audioLocale, audioTrackType, - itagItem); } } /** - * Create a new audio stream. + * Create a new audio stream using the given {@link Builder}. * - * @param id the identifier which uniquely identifies the stream, e.g. for YouTube - * this would be the itag - * @param content the content or the URL of the stream, depending on whether isUrl is - * true - * @param isUrl whether content is the URL or the actual content of e.g. a DASH - * manifest - * @param format the {@link MediaFormat} used by the stream, which can be null - * @param deliveryMethod the {@link DeliveryMethod} of the stream - * @param averageBitrate the average bitrate of the stream (which can be unknown, see - * {@link #UNKNOWN_BITRATE}) - * @param audioTrackId the id of the audio track - * @param audioTrackName the name of the audio track - * @param audioLocale the {@link Locale} of the audio stream, representing its language - * @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null - * @param manifestUrl the URL of the manifest this stream comes from (if applicable, - * otherwise null) + * @param builder The {@link Builder} to use to create the audio stream */ @SuppressWarnings("checkstyle:ParameterNumber") - private AudioStream(@Nonnull final String id, - @Nonnull final String content, - final boolean isUrl, - @Nullable final MediaFormat format, - @Nonnull final DeliveryMethod deliveryMethod, - final int averageBitrate, - @Nullable final String manifestUrl, - @Nullable final String audioTrackId, - @Nullable final String audioTrackName, - @Nullable final Locale audioLocale, - @Nullable final AudioTrackType audioTrackType, - @Nullable final ItagItem itagItem) { - super(id, content, isUrl, format, deliveryMethod, manifestUrl); - if (itagItem != null) { - this.itagItem = itagItem; - this.itag = itagItem.id; - this.quality = itagItem.getQuality(); - this.bitrate = itagItem.getBitrate(); - this.initStart = itagItem.getInitStart(); - this.initEnd = itagItem.getInitEnd(); - this.indexStart = itagItem.getIndexStart(); - this.indexEnd = itagItem.getIndexEnd(); - this.codec = itagItem.getCodec(); + AudioStream(final Builder builder) { + super(builder.id, + builder.content, + builder.isUrl, + builder.mediaFormat, + builder.deliveryMethod, + builder.manifestUrl); + if (builder.itagItem != null) { + this.itagItem = builder.itagItem; + this.itag = builder.itagItem.id; + this.quality = builder.itagItem.getQuality(); + this.bitrate = builder.itagItem.getBitrate(); + this.initStart = builder.itagItem.getInitStart(); + this.initEnd = builder.itagItem.getInitEnd(); + this.indexStart = builder.itagItem.getIndexStart(); + this.indexEnd = builder.itagItem.getIndexEnd(); + this.codec = builder.itagItem.getCodec(); } - this.averageBitrate = averageBitrate; - this.audioTrackId = audioTrackId; - this.audioTrackName = audioTrackName; - this.audioLocale = audioLocale; - this.audioTrackType = audioTrackType; + this.averageBitrate = builder.averageBitrate; + this.audioTrackId = builder.audioTrackId; + this.audioTrackName = builder.audioTrackName; + this.audioLocale = builder.audioLocale; + this.audioTrackType = builder.audioTrackType; } /** diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Description.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Description.java index 2641815b12..439609a2c1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Description.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Description.java @@ -17,11 +17,7 @@ public class Description implements Serializable { public Description(@Nullable final String content, final int type) { this.type = type; - if (content == null) { - this.content = ""; - } else { - this.content = content; - } + this.content = Objects.requireNonNullElse(content, ""); } public String getContent() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java index 04d2b3facb..57e824560d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java @@ -68,7 +68,7 @@ public Stream(final String id, * @param streamList the list of {@link Stream}s which will be compared * @return whether the list already contains one stream with equals stats */ - public static boolean containSimilarStream(final Stream stream, + public static boolean containSimilarStream(@Nonnull final Stream stream, final List streamList) { if (isNullOrEmpty(streamList)) { return false; @@ -98,11 +98,9 @@ public static boolean containSimilarStream(final Stream stream, * @return whether the stream have the same stats or not, based on the criteria above */ public boolean equalStats(@Nullable final Stream other) { - if (other == null || mediaFormat == null || other.mediaFormat == null) { - return false; - } - return mediaFormat.id == other.mediaFormat.id && deliveryMethod == other.deliveryMethod - && isUrl == other.isUrl; + return other != null && mediaFormat != null && other.mediaFormat != null + && mediaFormat.id == other.mediaFormat.id && deliveryMethod == other.deliveryMethod + && isUrl == other.isUrl; } /** @@ -137,6 +135,7 @@ public String getUrl() { * * @return the content or URL */ + @Nonnull public String getContent() { return content; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index b54c69afc2..21d07cd94a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -352,7 +352,7 @@ private static void extractOptionalData(final StreamInfo streamInfo, private String uploaderUrl = ""; @Nonnull private List uploaderAvatars = List.of(); - private boolean uploaderVerified = false; + private boolean uploaderVerified; private long uploaderSubscriberCount = -1; private String subChannelName = ""; @@ -368,7 +368,7 @@ private static void extractOptionalData(final StreamInfo streamInfo, private String hlsUrl = ""; private List relatedItems = List.of(); - private long startPosition = 0; + private long startPosition; private List subtitles = List.of(); private String host = ""; @@ -376,11 +376,11 @@ private static void extractOptionalData(final StreamInfo streamInfo, private String category = ""; private String licence = ""; private String supportInfo = ""; - private Locale language = null; + private Locale language; private List tags = List.of(); private List streamSegments = List.of(); private List metaInfo = List.of(); - private boolean shortFormContent = false; + private boolean shortFormContent; /** * Preview frames, e.g. for the storyboard / seekbar thumbnail preview diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ImageSuffix.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ImageSuffix.java index 4d8a141917..7332c75c08 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ImageSuffix.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ImageSuffix.java @@ -14,7 +14,7 @@ *

* This class is used to construct {@link org.schabi.newpipe.extractor.Image Image} * instances from a single base URL/path, in order to get all or most image resolutions provided, - * depending of the service and the resolutions provided. + * depending on the service and the resolutions provided. *

* *

diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java index 325867e75d..962d3f9478 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java @@ -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) { @@ -107,14 +170,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 diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java index c061ce30fa..bdf6bf20a2 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Utils.java @@ -110,12 +110,22 @@ 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"); } if (!Parser.isMatch(pattern, url.toLowerCase())) { - throw new ParsingException("Url don't match the pattern"); + throw new ParsingException("Url doesn't match the pattern"); } } diff --git a/extractor/src/test/java/org/schabi/newpipe/downloader/DownloaderTestImpl.java b/extractor/src/test/java/org/schabi/newpipe/downloader/DownloaderTestImpl.java index 091db071d0..6fd0c4c7cc 100644 --- a/extractor/src/test/java/org/schabi/newpipe/downloader/DownloaderTestImpl.java +++ b/extractor/src/test/java/org/schabi/newpipe/downloader/DownloaderTestImpl.java @@ -7,7 +7,6 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -35,7 +34,7 @@ private DownloaderTestImpl(final OkHttpClient.Builder builder) { // Required for certain services // For example Bandcamp otherwise fails on Windows with Java 17+ // as their Fastly-CDN returns 403 - .connectionSpecs(Arrays.asList(ConnectionSpec.RESTRICTED_TLS)) + .connectionSpecs(List.of(ConnectionSpec.RESTRICTED_TLS)) .build()); } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java index d33ea37443..d7fab32d1f 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java @@ -132,7 +132,6 @@ public void testRelatedItems() throws Exception { @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; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeSearchQHTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeSearchQHTest.java index b007f6ddc0..f7f1d95d1d 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeSearchQHTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/search/YoutubeSearchQHTest.java @@ -43,7 +43,7 @@ public void testGetContentFilter() throws Exception { } @Test - public void testWithContentfilter() throws Exception { + public void testWithContentFilter() throws Exception { assertEquals("https://www.youtube.com/results?search_query=asdf&sp=EgIQAfABAQ%253D%253D", YouTube.getSearchQHFactory() .fromQuery("asdf", List.of(VIDEOS), "").getUrl()); assertEquals("https://www.youtube.com/results?search_query=asdf&sp=EgIQAvABAQ%253D%253D", YouTube.getSearchQHFactory()