Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e9a5d62
Fix ``YoutubeDashManifestCreatorsTest``
litetex Jul 10, 2025
4f9e2a9
Fix ``YoutubeMixPlaylistExtractorTest#Music`` and update mocks
litetex Jul 11, 2025
084e66a
``YoutubeMusicSearchExtractor``: Add caching to improve performance
litetex Jul 11, 2025
9ef3a6e
Disable YoutubeMusicSearchExtractor test
litetex Jul 11, 2025
62b3c3b
Updated all YT mocks
litetex Jul 11, 2025
487dfea
Fix ``YoutubeStreamExtractorRelatedMixTest``
litetex Jul 11, 2025
bbe57e9
[Soundcloud] Removed Top 50 as it no longer exists
litetex Jul 11, 2025
2844424
[Soundcloud] Replaced no longer existing user
litetex Jul 11, 2025
c66ed65
Rename test + Fix assert order warning
litetex Jul 11, 2025
dda9783
Make ALL tests mockable
litetex Jul 12, 2025
a8c176a
Create new Mocks
litetex Jul 12, 2025
902a7c1
Remove unused imports
litetex Jul 12, 2025
a9c7c95
[Soundcloud] Charts: Fetch correctly so that tests work
litetex Jul 12, 2025
511a8a5
Fix corrupted response for ``YoutubeStreamExtractorLivestreamTest``
litetex Jul 12, 2025
1e34f50
Add missing annotations
litetex Jul 12, 2025
5c09e46
Fix test being not static and not executed
litetex Jul 12, 2025
65abde6
SoundcloudSearchQHTest does no communication
litetex Jul 12, 2025
d094c7d
YoutubeServiceTest requires the Downloader but never uses it
litetex Jul 12, 2025
3f88842
Delete old mocks
litetex Jul 12, 2025
b46169b
Remove additional boiler
litetex Jul 12, 2025
89a0923
Improved code style
litetex Jul 13, 2025
1b3a3f3
[SoundCloud] Purged Top 50
litetex Jul 13, 2025
99b9cdb
Fix format
litetex Jul 13, 2025
8bbb7e9
Modify ``SoundcloudChannelTabExtractorTest`` so that it compiles
litetex Jul 13, 2025
60c7790
Add mockdata for new ``SoundcloudChannelTabExtractorTest``
litetex Jul 13, 2025
599bc78
Improve naming
litetex Jul 16, 2025
1621336
Fix typo
litetex Jul 16, 2025
9a74872
Update docs
litetex Jul 16, 2025
2d3a7ae
Init in setup
litetex Jul 16, 2025
066553d
Cleanup
litetex Jul 16, 2025
de96e98
Fix naming
litetex Jul 16, 2025
c637c19
Updated mocks
litetex Jul 16, 2025
c443874
Fix broken real test and migrate mock data
litetex Jul 16, 2025
dd0f955
Removed unused mocks
litetex Jul 16, 2025
4fa9d74
Improve n parameter tests
litetex Jul 16, 2025
3f0b055
Use dedicated methods to fetch extractor related data
litetex Jul 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,9 @@ public static String getInfoItemsFromApi(final MultiInfoItemsCollector collector
// `Likes` feed, so they should be handled by the correct extractor.
final JsonObject likedPlaylist =
searchResult.getObject("playlist", null);
collector.commit(
null == likedPlaylist
? new SoundcloudLikesInfoItemExtractor(searchResult)
: new SoundcloudPlaylistInfoItemExtractor(likedPlaylist)
collector.commit(likedPlaylist == null
? new SoundcloudLikesInfoItemExtractor(searchResult)
: new SoundcloudPlaylistInfoItemExtractor(likedPlaylist)
);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ public KioskList getKioskList() throws ExtractionException {

// add kiosks here e.g.:
try {
list.addKioskEntry(chartsFactory, h, "Top 50");
list.addKioskEntry(chartsFactory, h, "New & hot");
list.setDefaultKiosk("New & hot");
} catch (final Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package org.schabi.newpipe.extractor.services.soundcloud.extractors;

import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;

import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
Expand All @@ -11,22 +15,54 @@
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;

import javax.annotation.Nonnull;
import java.io.IOException;

import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import javax.annotation.Nonnull;

public class SoundcloudChartsExtractor extends KioskExtractor<StreamInfoItem> {

private String initialFetchNextPageUrl;
private StreamInfoItemsCollector initialFetchCollector;

public SoundcloudChartsExtractor(final StreamingService service,
final ListLinkHandler linkHandler,
final String kioskId) {
super(service, linkHandler, kioskId);
}

@Override
public void onFetchPage(@Nonnull final Downloader downloader) {
public void onFetchPage(@Nonnull final Downloader downloader)
throws ExtractionException, IOException {
// Check if already run
if (initialFetchNextPageUrl != null) {
return;
}

initialFetchCollector = new StreamInfoItemsCollector(getServiceId());

final String apiUrl = SOUNDCLOUD_API_V2_URL + "charts"
+ "?genre=soundcloud:genres:all-music"
+ "&client_id=" + SoundcloudParsingHelper.clientId()
+ "&kind=trending";

final ContentCountry contentCountry = SoundCloud.getContentCountry();
String apiUrlWithRegion = null;
if (getService().getSupportedCountries().contains(contentCountry)) {
apiUrlWithRegion = apiUrl + "&region=soundcloud:regions:"
+ contentCountry.getCountryCode();
}

try {
initialFetchNextPageUrl = SoundcloudParsingHelper.getStreamsFromApi(
initialFetchCollector,
apiUrlWithRegion == null ? apiUrl : apiUrlWithRegion, true);
} catch (final IOException e) {
// Request to other region may be geo-restricted.
// See https://github.com/TeamNewPipe/NewPipeExtractor/issues/537.
// We retry without the specified region.
initialFetchNextPageUrl = SoundcloudParsingHelper.getStreamsFromApi(
initialFetchCollector, apiUrl, true);
}
}

@Nonnull
Expand All @@ -52,35 +88,6 @@ public InfoItemsPage<StreamInfoItem> getPage(final Page page) throws IOException
@Nonnull
@Override
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());

String apiUrl = SOUNDCLOUD_API_V2_URL + "charts" + "?genre=soundcloud:genres:all-music"
+ "&client_id=" + SoundcloudParsingHelper.clientId();

if (getId().equals("Top 50")) {
apiUrl += "&kind=top";
} else {
apiUrl += "&kind=trending";
}

final ContentCountry contentCountry = SoundCloud.getContentCountry();
String apiUrlWithRegion = null;
if (getService().getSupportedCountries().contains(contentCountry)) {
apiUrlWithRegion = apiUrl + "&region=soundcloud:regions:"
+ contentCountry.getCountryCode();
}

String nextPageUrl;
try {
nextPageUrl = SoundcloudParsingHelper.getStreamsFromApi(collector,
apiUrlWithRegion == null ? apiUrl : apiUrlWithRegion, true);
} catch (final IOException e) {
// Request to other region may be geo-restricted.
// See https://github.com/TeamNewPipe/NewPipeExtractor/issues/537.
// We retry without the specified region.
nextPageUrl = SoundcloudParsingHelper.getStreamsFromApi(collector, apiUrl, true);
}

return new InfoItemsPage<>(collector, new Page(nextPageUrl));
return new InfoItemsPage<>(initialFetchCollector, new Page(initialFetchNextPageUrl));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ public final class SoundcloudChartsLinkHandlerFactory extends ListLinkHandlerFac
private static final SoundcloudChartsLinkHandlerFactory INSTANCE =
new SoundcloudChartsLinkHandlerFactory();

private static final String TOP_URL_PATTERN =
"^https?://(www\\.|m\\.)?soundcloud.com/charts(/top)?/?([#?].*)?$";
private static final String URL_PATTERN =
"^https?://(www\\.|m\\.)?soundcloud.com/charts(/top|/new)?/?([#?].*)?$";

Expand All @@ -25,23 +23,15 @@ public static SoundcloudChartsLinkHandlerFactory getInstance() {

@Override
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
if (Parser.isMatch(TOP_URL_PATTERN, url.toLowerCase())) {
return "Top 50";
} else {
return "New & hot";
}
return "New & hot";
}

@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
if (id.equals("Top 50")) {
return "https://soundcloud.com/charts/top";
} else {
return "https://soundcloud.com/charts/new";
}
return "https://soundcloud.com/charts/new";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.schabi.newpipe.extractor.services.youtube;

import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;

import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.utils.JavaScript;

Expand Down Expand Up @@ -264,6 +266,10 @@ public static String getUrlWithThrottlingParameterDeobfuscated(
cachedThrottlingDeobfuscationFunctionName,
obfuscatedThrottlingParameter);

if (isNullOrEmpty(deobfuscatedThrottlingParameter)) {
throw new IllegalStateException("Extracted n-parameter is empty");
}

CACHED_THROTTLING_PARAMETERS.put(
obfuscatedThrottlingParameter, deobfuscatedThrottlingParameter);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,17 @@
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class YoutubeMusicSearchExtractor extends SearchExtractor {
private JsonObject initialData;

private List<JsonObject> cachedItemSectionRendererContents;

public YoutubeMusicSearchExtractor(final StreamingService service,
final SearchQueryHandler linkHandler) {
super(service, linkHandler);
Expand Down Expand Up @@ -116,7 +118,11 @@ public void onFetchPage(@Nonnull final Downloader downloader)
}

private List<JsonObject> getItemSectionRendererContents() {
return initialData
if (cachedItemSectionRendererContents != null) {
return cachedItemSectionRendererContents;
}

cachedItemSectionRendererContents = initialData
.getObject("contents")
.getObject("tabbedSearchResultsRenderer")
.getArray("tabs")
Expand All @@ -134,6 +140,7 @@ private List<JsonObject> getItemSectionRendererContents() {
.getArray("contents")
.getObject(0))
.collect(Collectors.toList());
return cachedItemSectionRendererContents;
}

@Nonnull
Expand All @@ -142,12 +149,16 @@ public String getSearchSuggestion() throws ParsingException {
for (final JsonObject obj : getItemSectionRendererContents()) {
final JsonObject didYouMeanRenderer = obj
.getObject("didYouMeanRenderer");
final JsonObject showingResultsForRenderer = obj
.getObject("showingResultsForRenderer");

if (!didYouMeanRenderer.isEmpty()) {
return getTextFromObject(didYouMeanRenderer.getObject("correctedQuery"));
} else if (!showingResultsForRenderer.isEmpty()) {
}

// NOTE: As of 2025-07 "showing results for ..." doesn't seem to be returned by
// the backend anymore, however the code is still present in the JS frontend.
final JsonObject showingResultsForRenderer = obj
.getObject("showingResultsForRenderer");
if (!showingResultsForRenderer.isEmpty()) {
return JsonUtils.getString(showingResultsForRenderer,
"correctedQueryEndpoint.searchEndpoint.query");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,19 @@ public static Matcher matchMultiplePatterns(final Pattern[] patterns, final Stri
if (matcher.find()) {
return matcher;
} else if (exception == null) {
// only pass input to exception message when it is not too long
if (input.length() > 1024) {
exception = new RegexException("Failed to find pattern \"" + pattern.pattern()
+ "\"");
} else {
exception = new RegexException("Failed to find pattern \"" + pattern.pattern()
+ "\" inside of \"" + input + "\"");
}
exception = new RegexException("Failed to find pattern \"" + pattern.pattern()
+ "\""
// only pass input to exception message when it is not too long
+ (input.length() <= 1000
? "inside of \"" + input + "\""
: "")
);
}
}

if (exception == null) {
throw new RegexException("Empty patterns array passed to matchMultiplePatterns");
} else {
throw exception;
}
throw exception != null
? exception
: new RegexException("Empty patterns array passed to matchMultiplePatterns");
}

public static boolean isMatch(final String pattern, final String input) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,56 @@

import org.schabi.newpipe.extractor.downloader.Downloader;

import java.io.IOException;
import java.util.Locale;

public class DownloaderFactory {

public static final String RESOURCE_PATH = "src/test/resources/org/schabi/newpipe/extractor/";

private static final DownloaderType DEFAULT_DOWNLOADER = DownloaderType.REAL;

public static DownloaderType getDownloaderType() {
private static DownloaderType cachedDownloaderType;

private static DownloaderType getDownloaderType() {
if (cachedDownloaderType == null) {
cachedDownloaderType = determineDownloaderType();
}

return cachedDownloaderType;
}

private static DownloaderType determineDownloaderType() {
String propValue = System.getProperty("downloader");
if (propValue == null) {
return DEFAULT_DOWNLOADER;
}
propValue = propValue.toUpperCase();
// Use shortcut because RECORDING is quite long
if (propValue.equals("REC")) {
return DownloaderType.RECORDING;
}
try {
return DownloaderType.valueOf(System.getProperty("downloader"));
return DownloaderType.valueOf(propValue);
} catch (final Exception e) {
return DEFAULT_DOWNLOADER;
}
}

public static Downloader getDownloader(final Class<?> clazz) {
return getDownloader(clazz, null);
}

public static Downloader getDownloader(final Class<?> clazz, final String specificUseCase) {
String baseName = clazz.getName();
if (specificUseCase != null) {
baseName += "." + specificUseCase;
}
return getDownloader("src/test/resources/mocks/v1/"
+ baseName
.toLowerCase(Locale.ENGLISH)
.replace('$', '.')
.replace("test", "")
.replace('.', '/'));
}

/**
* <p>
* Returns a implementation of a {@link Downloader}.
Expand All @@ -34,9 +68,8 @@ public static DownloaderType getDownloaderType() {
* </p>
*
* @param path The path to the folder where mocks are saved/retrieved.
* Preferably starting with {@link DownloaderFactory#RESOURCE_PATH}
*/
public static Downloader getDownloader(final String path) {
protected static Downloader getDownloader(final String path) {
final DownloaderType type = getDownloaderType();
switch (type) {
case REAL:
Expand Down

This file was deleted.

This file was deleted.

Loading