[SoundCloud] Add tabs support for users

Support of tracks, playlists and albums has been added for users.

Also add the declaration of the UnsupportedOperationException exception to the
service's LinkHandlers.

Co-authored-by: ThetaDev <t.testboy@gmail.com>
Co-authored-by: Stypox <stypox@pm.me>
This commit is contained in:
AudricV 2023-06-29 22:56:51 +02:00 committed by Stypox
parent 6f7d1f079f
commit d4bfe791ee
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
11 changed files with 265 additions and 51 deletions

View File

@ -8,6 +8,7 @@ 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.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfoItemsCollector;
import org.schabi.newpipe.extractor.downloader.Downloader;
@ -16,6 +17,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChannelInfoItemExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudPlaylistInfoItemExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudStreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
import org.schabi.newpipe.extractor.utils.JsonUtils;
@ -300,6 +302,54 @@ public final class SoundcloudParsingHelper {
return getStreamsFromApi(collector, apiUrl, false);
}
public static String getInfoItemsFromApi(final MultiInfoItemsCollector collector,
final String apiUrl) throws ReCaptchaException,
ParsingException, IOException {
final Response response = NewPipe.getDownloader().get(apiUrl, SoundCloud.getLocalization());
if (response.responseCode() >= 400) {
throw new IOException("Could not get streams from API, HTTP "
+ response.responseCode());
}
final JsonObject responseObject;
try {
responseObject = JsonParser.object().from(response.responseBody());
} catch (final JsonParserException e) {
throw new ParsingException("Could not parse json response", e);
}
responseObject.getArray("collection")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.forEach(searchResult -> {
final String kind = searchResult.getString("kind", "");
switch (kind) {
case "user":
collector.commit(new SoundcloudChannelInfoItemExtractor(searchResult));
break;
case "track":
collector.commit(new SoundcloudStreamInfoItemExtractor(searchResult));
break;
case "playlist":
collector.commit(new SoundcloudPlaylistInfoItemExtractor(searchResult));
break;
}
});
String nextPageUrl;
try {
nextPageUrl = responseObject.getString("next_href");
if (!nextPageUrl.contains("client_id=")) {
nextPageUrl += "&client_id=" + SoundcloudParsingHelper.clientId();
}
} catch (final Exception ignored) {
nextPageUrl = "";
}
return nextPageUrl;
}
@Nonnull
public static String getUploaderUrl(final JsonObject object) {
final String url = object.getObject("user").getString("permalink_url", "");
@ -312,6 +362,7 @@ public final class SoundcloudParsingHelper {
return replaceHttpWithHttps(url);
}
@Nonnull
public static String getUploaderName(final JsonObject object) {
return object.getObject("user").getString("username", "");
}

View File

@ -6,6 +6,7 @@ import static java.util.Arrays.asList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskList;
@ -19,6 +20,7 @@ import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChannelExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChannelTabExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChartsExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudCommentsExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudPlaylistExtractor;
@ -27,6 +29,7 @@ import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudStr
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudSubscriptionExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudSuggestionExtractor;
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudChannelLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudChannelTabLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudChartsLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudCommentsLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudPlaylistLinkHandlerFactory;
@ -50,7 +53,7 @@ public class SoundcloudService extends StreamingService {
@Override
public SearchQueryHandlerFactory getSearchQHFactory() {
return new SoundcloudSearchQueryHandlerFactory();
return SoundcloudSearchQueryHandlerFactory.getInstance();
}
@Override
@ -63,6 +66,11 @@ public class SoundcloudService extends StreamingService {
return SoundcloudChannelLinkHandlerFactory.getInstance();
}
@Override
public ListLinkHandlerFactory getChannelTabLHFactory() {
return SoundcloudChannelTabLinkHandlerFactory.getInstance();
}
@Override
public ListLinkHandlerFactory getPlaylistLHFactory() {
return SoundcloudPlaylistLinkHandlerFactory.getInstance();
@ -86,6 +94,11 @@ public class SoundcloudService extends StreamingService {
return new SoundcloudChannelExtractor(this, linkHandler);
}
@Override
public ChannelTabExtractor getChannelTabExtractor(final ListLinkHandler linkHandler) {
return new SoundcloudChannelTabExtractor(this, linkHandler);
}
@Override
public PlaylistExtractor getPlaylistExtractor(final ListLinkHandler linkHandler) {
return new SoundcloudPlaylistExtractor(this, linkHandler);
@ -103,14 +116,15 @@ public class SoundcloudService extends StreamingService {
@Override
public KioskList getKioskList() throws ExtractionException {
final KioskList.KioskExtractorFactory chartsFactory = (streamingService, url, id) ->
new SoundcloudChartsExtractor(SoundcloudService.this,
new SoundcloudChartsLinkHandlerFactory().fromUrl(url), id);
final KioskList list = new KioskList(this);
final SoundcloudChartsLinkHandlerFactory h =
SoundcloudChartsLinkHandlerFactory.getInstance();
final KioskList.KioskExtractorFactory chartsFactory = (streamingService, url, id) ->
new SoundcloudChartsExtractor(SoundcloudService.this,
h.fromUrl(url), id);
// add kiosks here e.g.:
final SoundcloudChartsLinkHandlerFactory h = new SoundcloudChartsLinkHandlerFactory();
try {
list.addKioskEntry(chartsFactory, h, "Top 50");
list.addKioskEntry(chartsFactory, h, "New & hot");

View File

@ -1,24 +1,23 @@
package org.schabi.newpipe.extractor.services.soundcloud.extractors;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudChannelTabLinkHandlerFactory;
import java.io.IOException;
import java.util.List;
import javax.annotation.Nonnull;
@ -108,34 +107,22 @@ public class SoundcloudChannelExtractor extends ChannelExtractor {
@Nonnull
@Override
public InfoItemsPage<StreamInfoItem> getInitialPage() throws ExtractionException {
try {
final StreamInfoItemsCollector streamInfoItemsCollector =
new StreamInfoItemsCollector(getServiceId());
public List<ListLinkHandler> getTabs() throws ParsingException {
final String url = getUrl();
final String urlTracks = url
+ SoundcloudChannelTabLinkHandlerFactory.getUrlSuffix(ChannelTabs.TRACKS);
final String urlPlaylists = url
+ SoundcloudChannelTabLinkHandlerFactory.getUrlSuffix(ChannelTabs.PLAYLISTS);
final String urlAlbums = url
+ SoundcloudChannelTabLinkHandlerFactory.getUrlSuffix(ChannelTabs.ALBUMS);
final String id = getId();
final String apiUrl = USERS_ENDPOINT + getId() + "/tracks" + "?client_id="
+ SoundcloudParsingHelper.clientId() + "&limit=20" + "&linked_partitioning=1";
final String nextPageUrl = SoundcloudParsingHelper.getStreamsFromApiMinItems(15,
streamInfoItemsCollector, apiUrl);
return new InfoItemsPage<>(streamInfoItemsCollector, new Page(nextPageUrl));
} catch (final Exception e) {
throw new ExtractionException("Could not get next page", e);
}
}
@Override
public InfoItemsPage<StreamInfoItem> getPage(final Page page) throws IOException,
ExtractionException {
if (page == null || isNullOrEmpty(page.getUrl())) {
throw new IllegalArgumentException("Page doesn't contain an URL");
}
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
final String nextPageUrl = SoundcloudParsingHelper.getStreamsFromApiMinItems(15, collector,
page.getUrl());
return new InfoItemsPage<>(collector, new Page(nextPageUrl));
return List.of(
new ListLinkHandler(urlTracks, urlTracks, id,
List.of(ChannelTabs.TRACKS), ""),
new ListLinkHandler(urlPlaylists, urlPlaylists, id,
List.of(ChannelTabs.PLAYLISTS), ""),
new ListLinkHandler(urlAlbums, urlAlbums, id,
List.of(ChannelTabs.ALBUMS), ""));
}
}

View File

@ -0,0 +1,76 @@
package org.schabi.newpipe.extractor.services.soundcloud.extractors;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
import javax.annotation.Nonnull;
import java.io.IOException;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public class SoundcloudChannelTabExtractor extends ChannelTabExtractor {
private static final String USERS_ENDPOINT = SOUNDCLOUD_API_V2_URL + "users/";
private final String userId;
public SoundcloudChannelTabExtractor(final StreamingService service,
final ListLinkHandler linkHandler) {
super(service, linkHandler);
userId = getLinkHandler().getId();
}
@Nonnull
private String getEndpoint() throws ParsingException {
switch (getName()) {
case ChannelTabs.TRACKS:
return "/tracks";
case ChannelTabs.PLAYLISTS:
return "/playlists_without_albums";
case ChannelTabs.ALBUMS:
return "/albums";
}
throw new ParsingException("Unsupported tab: " + getName());
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader) {
}
@Nonnull
@Override
public String getId() {
return userId;
}
@Nonnull
@Override
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
return getPage(new Page(USERS_ENDPOINT + userId + getEndpoint() + "?client_id="
+ SoundcloudParsingHelper.clientId() + "&limit=20" + "&linked_partitioning=1"));
}
@Override
public InfoItemsPage<InfoItem> getPage(final Page page)
throws IOException, ExtractionException {
if (page == null || isNullOrEmpty(page.getUrl())) {
throw new IllegalArgumentException("Page doesn't contain an URL");
}
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
final String nextPageUrl = SoundcloudParsingHelper.getInfoItemsFromApi(
collector, page.getUrl());
return new InfoItemsPage<>(collector, new Page(nextPageUrl));
}
}

View File

@ -23,7 +23,7 @@ public final class SoundcloudChannelLinkHandlerFactory extends ListLinkHandlerFa
@Override
public String getId(final String url) throws ParsingException {
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
Utils.checkUrl(URL_PATTERN, url);
try {
@ -36,7 +36,8 @@ public final class SoundcloudChannelLinkHandlerFactory extends ListLinkHandlerFa
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter) throws ParsingException {
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
try {
return SoundcloudParsingHelper.resolveUrlWithEmbedPlayer(
"https://api.soundcloud.com/users/" + id);

View File

@ -0,0 +1,61 @@
package org.schabi.newpipe.extractor.services.soundcloud.linkHandler;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.UnsupportedTabException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import javax.annotation.Nonnull;
import java.util.List;
public final class SoundcloudChannelTabLinkHandlerFactory extends ListLinkHandlerFactory {
private static final SoundcloudChannelTabLinkHandlerFactory INSTANCE
= new SoundcloudChannelTabLinkHandlerFactory();
private SoundcloudChannelTabLinkHandlerFactory() {
}
public static SoundcloudChannelTabLinkHandlerFactory getInstance() {
return INSTANCE;
}
@Nonnull
public static String getUrlSuffix(final String tab) throws UnsupportedOperationException {
switch (tab) {
case ChannelTabs.TRACKS:
return "/tracks";
case ChannelTabs.PLAYLISTS:
return "/sets";
case ChannelTabs.ALBUMS:
return "/albums";
}
throw new UnsupportedTabException(tab);
}
@Override
public String getId(final String url) throws ParsingException {
return SoundcloudChannelLinkHandlerFactory.getInstance().getId(url);
}
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter) throws ParsingException {
return SoundcloudChannelLinkHandlerFactory.getInstance().getUrl(id)
+ getUrlSuffix(contentFilter.get(0));
}
@Override
public boolean onAcceptUrl(final String url) throws ParsingException {
return SoundcloudChannelLinkHandlerFactory.getInstance().onAcceptUrl(url);
}
@Override
public String[] getAvailableContentFilter() {
return new String[] {
ChannelTabs.TRACKS,
ChannelTabs.PLAYLISTS,
ChannelTabs.ALBUMS,
};
}
}

View File

@ -1,18 +1,30 @@
package org.schabi.newpipe.extractor.services.soundcloud.linkHandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.utils.Parser;
import java.util.List;
public class SoundcloudChartsLinkHandlerFactory extends ListLinkHandlerFactory {
public final class SoundcloudChartsLinkHandlerFactory extends ListLinkHandlerFactory {
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)?/?([#?].*)?$";
private SoundcloudChartsLinkHandlerFactory() {
}
public static SoundcloudChartsLinkHandlerFactory getInstance() {
return INSTANCE;
}
@Override
public String getId(final String url) {
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
if (Parser.isMatch(TOP_URL_PATTERN, url.toLowerCase())) {
return "Top 50";
} else {
@ -23,7 +35,8 @@ public class SoundcloudChartsLinkHandlerFactory extends ListLinkHandlerFactory {
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter) {
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
if (id.equals("Top 50")) {
return "https://soundcloud.com/charts/top";
} else {

View File

@ -24,7 +24,8 @@ public final class SoundcloudCommentsLinkHandlerFactory extends ListLinkHandlerF
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter) throws ParsingException {
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
try {
return "https://api-v2.soundcloud.com/tracks/" + id + "/comments" + "?client_id="
+ clientId() + "&threaded=0" + "&filter_replies=1";
@ -37,7 +38,7 @@ public final class SoundcloudCommentsLinkHandlerFactory extends ListLinkHandlerF
}
@Override
public String getId(final String url) throws ParsingException {
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
// Delegation to avoid duplicate code, as we need the same id
return SoundcloudStreamLinkHandlerFactory.getInstance().getId(url);
}

View File

@ -22,7 +22,7 @@ public final class SoundcloudPlaylistLinkHandlerFactory extends ListLinkHandlerF
}
@Override
public String getId(final String url) throws ParsingException {
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
Utils.checkUrl(URL_PATTERN, url);
try {
@ -37,7 +37,7 @@ public final class SoundcloudPlaylistLinkHandlerFactory extends ListLinkHandlerF
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter)
throws ParsingException {
throws ParsingException, UnsupportedOperationException {
try {
return SoundcloudParsingHelper.resolveUrlWithEmbedPlayer(
"https://api.soundcloud.com/playlists/" + id);

View File

@ -13,7 +13,10 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
public class SoundcloudSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
public final class SoundcloudSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
private static final SoundcloudSearchQueryHandlerFactory INSTANCE =
new SoundcloudSearchQueryHandlerFactory();
public static final String TRACKS = "tracks";
public static final String USERS = "users";
@ -22,11 +25,18 @@ public class SoundcloudSearchQueryHandlerFactory extends SearchQueryHandlerFacto
public static final int ITEMS_PER_PAGE = 10;
private SoundcloudSearchQueryHandlerFactory() {
}
public static SoundcloudSearchQueryHandlerFactory getInstance() {
return INSTANCE;
}
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter)
throws ParsingException {
throws ParsingException, UnsupportedOperationException {
try {
String url = SOUNDCLOUD_API_V2_URL + "search";

View File

@ -21,7 +21,7 @@ public final class SoundcloudStreamLinkHandlerFactory extends LinkHandlerFactory
}
@Override
public String getUrl(final String id) throws ParsingException {
public String getUrl(final String id) throws ParsingException, UnsupportedOperationException {
try {
return SoundcloudParsingHelper.resolveUrlWithEmbedPlayer(
"https://api.soundcloud.com/tracks/" + id);
@ -31,7 +31,7 @@ public final class SoundcloudStreamLinkHandlerFactory extends LinkHandlerFactory
}
@Override
public String getId(final String url) throws ParsingException {
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
if (Parser.isMatch(API_URL_PATTERN, url)) {
return Parser.matchGroup1(API_URL_PATTERN, url);
}