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.12.1"
checkstyleVersion = "10.4"
immutablesVersion = "2.10.1"
}
}

Expand Down
8 changes: 8 additions & 0 deletions extractor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,12 @@ dependencies {

testImplementation "com.squareup.okhttp3:okhttp:4.12.0"
testImplementation 'com.google.code.gson:gson:2.12.1'
testImplementation "org.immutables:value:$immutablesVersion"
testAnnotationProcessor "org.immutables:value:$immutablesVersion"
compileOnly "org.immutables:value:$immutablesVersion"

}

repositories {
mavenCentral()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import java.util.List;
import java.util.Map;

import org.schabi.newpipe.extractor.exceptions.HttpResponseException;
import org.schabi.newpipe.extractor.utils.HttpUtils;

/**
* A Data class used to hold the results from requests made by the Downloader implementation.
*/
Expand Down Expand Up @@ -80,4 +83,21 @@ public String getHeader(final String name) {

return null;
}
// CHECKSTYLE:OFF
/**
* Helper function simply to make it easier to validate response code inline
* before getting the code/body/latestUrl/etc.
* Validates the response codes for the given {@link Response}, and throws a {@link HttpResponseException} if the code is invalid
* @see HttpUtils#validateResponseCode(Response, int...)
* @param validResponseCodes Expected valid response codes
* @return {@link this} response
* @throws HttpResponseException Thrown when the response code is not in {@code validResponseCodes},
* or when {@code validResponseCodes} is empty and the code is a 4xx or 5xx error.
*/
// CHECKSTYLE:ON
public Response validateResponseCode(final int... validResponseCodes)
throws HttpResponseException {
HttpUtils.validateResponseCode(this, validResponseCodes);
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.schabi.newpipe.extractor.exceptions;

import java.io.IOException;
import org.schabi.newpipe.extractor.downloader.Response;

public class HttpResponseException extends IOException {
public HttpResponseException(final Response response) {
this("Error in HTTP Response for " + response.latestUrl() + "\n\t"
+ response.responseCode() + " - " + response.responseMessage());
}

public HttpResponseException(final String message) {
super(message);
}
}
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 @@ -5,14 +5,14 @@
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
import static org.schabi.newpipe.extractor.utils.HttpUtils.validateResponseCode;

import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
import org.schabi.newpipe.extractor.Image;
Expand Down Expand Up @@ -103,8 +103,8 @@ public static synchronized String clientId() throws ExtractionException, IOExcep

final Downloader dl = NewPipe.getDownloader();

final Response download = dl.get("https://soundcloud.com");
final String responseBody = download.responseBody();
final Response downloadResponse = dl.get("https://soundcloud.com").validateResponseCode();
final String responseBody = downloadResponse.responseBody();
final String clientIdPattern = ",client_id:\"(.*?)\"";

final Document doc = Jsoup.parse(responseBody);
Expand All @@ -115,11 +115,12 @@ public static synchronized String clientId() throws ExtractionException, IOExcep

final var headers = Map.of("Range", List.of("bytes=0-50000"));

for (final Element element : possibleScripts) {
for (final var element : possibleScripts) {
final String srcUrl = element.attr("src");
if (!isNullOrEmpty(srcUrl)) {
try {
clientId = Parser.matchGroup1(clientIdPattern, dl.get(srcUrl, headers)
.validateResponseCode()
.responseBody());
return clientId;
} catch (final RegexException ignored) {
Expand Down Expand Up @@ -147,11 +148,13 @@ public static OffsetDateTime parseDateFrom(final String textualUploadDate)
}
}

// CHECKSTYLE:OFF
/**
* Call the endpoint "/resolve" of the API.<p>
* Call the endpoint "/resolve" of the API.
* <p>
* See https://developers.soundcloud.com/docs/api/reference#resolve
* See https://web.archive.org/web/20170804051146/https://developers.soundcloud.com/docs/api/reference#resolve
*/
// CHECKSTYLE:ON
public static JsonObject resolveFor(@Nonnull final Downloader downloader, final String url)
throws IOException, ExtractionException {
final String apiUrl = SOUNDCLOUD_API_V2_URL + "resolve"
Expand All @@ -176,10 +179,11 @@ public static JsonObject resolveFor(@Nonnull final Downloader downloader, final
public static String resolveUrlWithEmbedPlayer(final String apiUrl) throws IOException,
ReCaptchaException {

final String response = NewPipe.getDownloader().get("https://w.soundcloud.com/player/?url="
+ Utils.encodeUrlUtf8(apiUrl), SoundCloud.getLocalization()).responseBody();

return Jsoup.parse(response).select("link[rel=\"canonical\"]").first()
final var response = NewPipe.getDownloader().get("https://w.soundcloud.com/player/?url="
+ Utils.encodeUrlUtf8(apiUrl), SoundCloud.getLocalization());
validateResponseCode(response);
final var responseBody = response.responseBody();
return Jsoup.parse(responseBody).select("link[rel=\"canonical\"]").first()
.attr("abs:href");
}

Expand All @@ -188,6 +192,7 @@ public static String resolveUrlWithEmbedPlayer(final String apiUrl) throws IOExc
*
* @return the resolved id
*/
// TODO: what makes this method different from the others? Don' they all return the same?
public static String resolveIdWithWidgetApi(final String urlString) throws IOException,
ParsingException {
String fixedUrl = urlString;
Expand Down Expand Up @@ -223,9 +228,12 @@ public static String resolveIdWithWidgetApi(final String urlString) throws IOExc
final String widgetUrl = "https://api-widget.soundcloud.com/resolve?url="
+ Utils.encodeUrlUtf8(url.toString())
+ "&format=json&client_id=" + SoundcloudParsingHelper.clientId();
final String response = NewPipe.getDownloader().get(widgetUrl,
SoundCloud.getLocalization()).responseBody();
final JsonObject o = JsonParser.object().from(response);

final var response = NewPipe.getDownloader().get(widgetUrl,
SoundCloud.getLocalization());

final var responseBody = response.validateResponseCode().responseBody();
final JsonObject o = JsonParser.object().from(responseBody);
return String.valueOf(JsonUtils.getValue(o, "id"));
} catch (final JsonParserException e) {
throw new ParsingException("Could not parse JSON response", e);
Expand Down
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import java.util.Locale;
import java.util.Objects;

public final class AudioStream extends Stream {
public class AudioStream extends Stream {
public static final int UNKNOWN_BITRATE = -1;

private final int averageBitrate;
Expand Down Expand Up @@ -60,7 +60,7 @@ public final class AudioStream extends Stream {
* Class to build {@link AudioStream} objects.
*/
@SuppressWarnings("checkstyle:hiddenField")
public static final class Builder {
public static class Builder {
private String id;
private String content;
private boolean isUrl;
Expand Down Expand Up @@ -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
*
* <p>
* It <b>must not be null</b> and should be non empty.
Expand All @@ -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
* <p>
* It must not be null, and should be non empty.
* </p>
*
* @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,
Expand All @@ -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
*
* <p>
* It should be one of the audio {@link MediaFormat}s ({@link MediaFormat#M4A M4A},
Expand Down Expand Up @@ -278,16 +279,22 @@ public Builder setItagItem(@Nullable final ItagItem itagItem) {
* Build an {@link AudioStream} using the builder's current values.
*
* <p>
* 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.
* </p>
*
* @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 "
Expand All @@ -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;
}

/**
Expand Down
Loading
Loading