Skip to content

Commit d4bfe79

Browse files
AudricVTheta-DevStypox
committed
[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>
1 parent 6f7d1f0 commit d4bfe79

11 files changed

Lines changed: 265 additions & 51 deletions

extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudParsingHelper.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.jsoup.nodes.Document;
99
import org.jsoup.nodes.Element;
1010
import org.jsoup.select.Elements;
11+
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
1112
import org.schabi.newpipe.extractor.NewPipe;
1213
import org.schabi.newpipe.extractor.channel.ChannelInfoItemsCollector;
1314
import org.schabi.newpipe.extractor.downloader.Downloader;
@@ -16,6 +17,7 @@
1617
import org.schabi.newpipe.extractor.exceptions.ParsingException;
1718
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
1819
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChannelInfoItemExtractor;
20+
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudPlaylistInfoItemExtractor;
1921
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudStreamInfoItemExtractor;
2022
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
2123
import org.schabi.newpipe.extractor.utils.JsonUtils;
@@ -300,6 +302,54 @@ public static String getStreamsFromApi(final StreamInfoItemsCollector collector,
300302
return getStreamsFromApi(collector, apiUrl, false);
301303
}
302304

305+
public static String getInfoItemsFromApi(final MultiInfoItemsCollector collector,
306+
final String apiUrl) throws ReCaptchaException,
307+
ParsingException, IOException {
308+
final Response response = NewPipe.getDownloader().get(apiUrl, SoundCloud.getLocalization());
309+
if (response.responseCode() >= 400) {
310+
throw new IOException("Could not get streams from API, HTTP "
311+
+ response.responseCode());
312+
}
313+
314+
final JsonObject responseObject;
315+
try {
316+
responseObject = JsonParser.object().from(response.responseBody());
317+
} catch (final JsonParserException e) {
318+
throw new ParsingException("Could not parse json response", e);
319+
}
320+
321+
responseObject.getArray("collection")
322+
.stream()
323+
.filter(JsonObject.class::isInstance)
324+
.map(JsonObject.class::cast)
325+
.forEach(searchResult -> {
326+
final String kind = searchResult.getString("kind", "");
327+
switch (kind) {
328+
case "user":
329+
collector.commit(new SoundcloudChannelInfoItemExtractor(searchResult));
330+
break;
331+
case "track":
332+
collector.commit(new SoundcloudStreamInfoItemExtractor(searchResult));
333+
break;
334+
case "playlist":
335+
collector.commit(new SoundcloudPlaylistInfoItemExtractor(searchResult));
336+
break;
337+
}
338+
});
339+
340+
String nextPageUrl;
341+
try {
342+
nextPageUrl = responseObject.getString("next_href");
343+
if (!nextPageUrl.contains("client_id=")) {
344+
nextPageUrl += "&client_id=" + SoundcloudParsingHelper.clientId();
345+
}
346+
} catch (final Exception ignored) {
347+
nextPageUrl = "";
348+
}
349+
350+
return nextPageUrl;
351+
}
352+
303353
@Nonnull
304354
public static String getUploaderUrl(final JsonObject object) {
305355
final String url = object.getObject("user").getString("permalink_url", "");
@@ -312,6 +362,7 @@ public static String getAvatarUrl(final JsonObject object) {
312362
return replaceHttpWithHttps(url);
313363
}
314364

365+
@Nonnull
315366
public static String getUploaderName(final JsonObject object) {
316367
return object.getObject("user").getString("username", "");
317368
}

extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudService.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import org.schabi.newpipe.extractor.StreamingService;
88
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
9+
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
910
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
1011
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
1112
import org.schabi.newpipe.extractor.kiosk.KioskList;
@@ -19,6 +20,7 @@
1920
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
2021
import org.schabi.newpipe.extractor.search.SearchExtractor;
2122
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChannelExtractor;
23+
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChannelTabExtractor;
2224
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudChartsExtractor;
2325
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudCommentsExtractor;
2426
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudPlaylistExtractor;
@@ -27,6 +29,7 @@
2729
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudSubscriptionExtractor;
2830
import org.schabi.newpipe.extractor.services.soundcloud.extractors.SoundcloudSuggestionExtractor;
2931
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudChannelLinkHandlerFactory;
32+
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudChannelTabLinkHandlerFactory;
3033
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudChartsLinkHandlerFactory;
3134
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudCommentsLinkHandlerFactory;
3235
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudPlaylistLinkHandlerFactory;
@@ -50,7 +53,7 @@ public String getBaseUrl() {
5053

5154
@Override
5255
public SearchQueryHandlerFactory getSearchQHFactory() {
53-
return new SoundcloudSearchQueryHandlerFactory();
56+
return SoundcloudSearchQueryHandlerFactory.getInstance();
5457
}
5558

5659
@Override
@@ -63,6 +66,11 @@ public ListLinkHandlerFactory getChannelLHFactory() {
6366
return SoundcloudChannelLinkHandlerFactory.getInstance();
6467
}
6568

69+
@Override
70+
public ListLinkHandlerFactory getChannelTabLHFactory() {
71+
return SoundcloudChannelTabLinkHandlerFactory.getInstance();
72+
}
73+
6674
@Override
6775
public ListLinkHandlerFactory getPlaylistLHFactory() {
6876
return SoundcloudPlaylistLinkHandlerFactory.getInstance();
@@ -86,6 +94,11 @@ public ChannelExtractor getChannelExtractor(final ListLinkHandler linkHandler) {
8694
return new SoundcloudChannelExtractor(this, linkHandler);
8795
}
8896

97+
@Override
98+
public ChannelTabExtractor getChannelTabExtractor(final ListLinkHandler linkHandler) {
99+
return new SoundcloudChannelTabExtractor(this, linkHandler);
100+
}
101+
89102
@Override
90103
public PlaylistExtractor getPlaylistExtractor(final ListLinkHandler linkHandler) {
91104
return new SoundcloudPlaylistExtractor(this, linkHandler);
@@ -103,14 +116,15 @@ public SoundcloudSuggestionExtractor getSuggestionExtractor() {
103116

104117
@Override
105118
public KioskList getKioskList() throws ExtractionException {
119+
final KioskList list = new KioskList(this);
120+
121+
final SoundcloudChartsLinkHandlerFactory h =
122+
SoundcloudChartsLinkHandlerFactory.getInstance();
106123
final KioskList.KioskExtractorFactory chartsFactory = (streamingService, url, id) ->
107124
new SoundcloudChartsExtractor(SoundcloudService.this,
108-
new SoundcloudChartsLinkHandlerFactory().fromUrl(url), id);
109-
110-
final KioskList list = new KioskList(this);
125+
h.fromUrl(url), id);
111126

112127
// add kiosks here e.g.:
113-
final SoundcloudChartsLinkHandlerFactory h = new SoundcloudChartsLinkHandlerFactory();
114128
try {
115129
list.addKioskEntry(chartsFactory, h, "Top 50");
116130
list.addKioskEntry(chartsFactory, h, "New & hot");

extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudChannelExtractor.java

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
package org.schabi.newpipe.extractor.services.soundcloud.extractors;
22

33
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL;
4-
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
54

65
import com.grack.nanojson.JsonObject;
76
import com.grack.nanojson.JsonParser;
87
import com.grack.nanojson.JsonParserException;
98

10-
import org.schabi.newpipe.extractor.Page;
119
import org.schabi.newpipe.extractor.StreamingService;
1210
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
11+
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
1312
import org.schabi.newpipe.extractor.downloader.Downloader;
1413
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
1514
import org.schabi.newpipe.extractor.exceptions.ParsingException;
1615
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
1716
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
18-
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
19-
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
17+
import org.schabi.newpipe.extractor.services.soundcloud.linkHandler.SoundcloudChannelTabLinkHandlerFactory;
2018

2119
import java.io.IOException;
20+
import java.util.List;
2221

2322
import javax.annotation.Nonnull;
2423

@@ -108,34 +107,22 @@ public boolean isVerified() throws ParsingException {
108107

109108
@Nonnull
110109
@Override
111-
public InfoItemsPage<StreamInfoItem> getInitialPage() throws ExtractionException {
112-
try {
113-
final StreamInfoItemsCollector streamInfoItemsCollector =
114-
new StreamInfoItemsCollector(getServiceId());
115-
116-
final String apiUrl = USERS_ENDPOINT + getId() + "/tracks" + "?client_id="
117-
+ SoundcloudParsingHelper.clientId() + "&limit=20" + "&linked_partitioning=1";
118-
119-
final String nextPageUrl = SoundcloudParsingHelper.getStreamsFromApiMinItems(15,
120-
streamInfoItemsCollector, apiUrl);
121-
122-
return new InfoItemsPage<>(streamInfoItemsCollector, new Page(nextPageUrl));
123-
} catch (final Exception e) {
124-
throw new ExtractionException("Could not get next page", e);
125-
}
126-
}
127-
128-
@Override
129-
public InfoItemsPage<StreamInfoItem> getPage(final Page page) throws IOException,
130-
ExtractionException {
131-
if (page == null || isNullOrEmpty(page.getUrl())) {
132-
throw new IllegalArgumentException("Page doesn't contain an URL");
133-
}
134-
135-
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
136-
final String nextPageUrl = SoundcloudParsingHelper.getStreamsFromApiMinItems(15, collector,
137-
page.getUrl());
138-
139-
return new InfoItemsPage<>(collector, new Page(nextPageUrl));
110+
public List<ListLinkHandler> getTabs() throws ParsingException {
111+
final String url = getUrl();
112+
final String urlTracks = url
113+
+ SoundcloudChannelTabLinkHandlerFactory.getUrlSuffix(ChannelTabs.TRACKS);
114+
final String urlPlaylists = url
115+
+ SoundcloudChannelTabLinkHandlerFactory.getUrlSuffix(ChannelTabs.PLAYLISTS);
116+
final String urlAlbums = url
117+
+ SoundcloudChannelTabLinkHandlerFactory.getUrlSuffix(ChannelTabs.ALBUMS);
118+
final String id = getId();
119+
120+
return List.of(
121+
new ListLinkHandler(urlTracks, urlTracks, id,
122+
List.of(ChannelTabs.TRACKS), ""),
123+
new ListLinkHandler(urlPlaylists, urlPlaylists, id,
124+
List.of(ChannelTabs.PLAYLISTS), ""),
125+
new ListLinkHandler(urlAlbums, urlAlbums, id,
126+
List.of(ChannelTabs.ALBUMS), ""));
140127
}
141128
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.schabi.newpipe.extractor.services.soundcloud.extractors;
2+
3+
import org.schabi.newpipe.extractor.InfoItem;
4+
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
5+
import org.schabi.newpipe.extractor.Page;
6+
import org.schabi.newpipe.extractor.StreamingService;
7+
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor;
8+
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
9+
import org.schabi.newpipe.extractor.downloader.Downloader;
10+
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
11+
import org.schabi.newpipe.extractor.exceptions.ParsingException;
12+
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
13+
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
14+
15+
import javax.annotation.Nonnull;
16+
import java.io.IOException;
17+
18+
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL;
19+
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
20+
21+
public class SoundcloudChannelTabExtractor extends ChannelTabExtractor {
22+
23+
private static final String USERS_ENDPOINT = SOUNDCLOUD_API_V2_URL + "users/";
24+
25+
private final String userId;
26+
27+
public SoundcloudChannelTabExtractor(final StreamingService service,
28+
final ListLinkHandler linkHandler) {
29+
super(service, linkHandler);
30+
userId = getLinkHandler().getId();
31+
}
32+
33+
@Nonnull
34+
private String getEndpoint() throws ParsingException {
35+
switch (getName()) {
36+
case ChannelTabs.TRACKS:
37+
return "/tracks";
38+
case ChannelTabs.PLAYLISTS:
39+
return "/playlists_without_albums";
40+
case ChannelTabs.ALBUMS:
41+
return "/albums";
42+
}
43+
throw new ParsingException("Unsupported tab: " + getName());
44+
}
45+
46+
@Override
47+
public void onFetchPage(@Nonnull final Downloader downloader) {
48+
}
49+
50+
@Nonnull
51+
@Override
52+
public String getId() {
53+
return userId;
54+
}
55+
56+
@Nonnull
57+
@Override
58+
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
59+
return getPage(new Page(USERS_ENDPOINT + userId + getEndpoint() + "?client_id="
60+
+ SoundcloudParsingHelper.clientId() + "&limit=20" + "&linked_partitioning=1"));
61+
}
62+
63+
@Override
64+
public InfoItemsPage<InfoItem> getPage(final Page page)
65+
throws IOException, ExtractionException {
66+
if (page == null || isNullOrEmpty(page.getUrl())) {
67+
throw new IllegalArgumentException("Page doesn't contain an URL");
68+
}
69+
70+
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
71+
final String nextPageUrl = SoundcloudParsingHelper.getInfoItemsFromApi(
72+
collector, page.getUrl());
73+
74+
return new InfoItemsPage<>(collector, new Page(nextPageUrl));
75+
}
76+
}

extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/linkHandler/SoundcloudChannelLinkHandlerFactory.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public static SoundcloudChannelLinkHandlerFactory getInstance() {
2323

2424

2525
@Override
26-
public String getId(final String url) throws ParsingException {
26+
public String getId(final String url) throws ParsingException, UnsupportedOperationException {
2727
Utils.checkUrl(URL_PATTERN, url);
2828

2929
try {
@@ -36,7 +36,8 @@ public String getId(final String url) throws ParsingException {
3636
@Override
3737
public String getUrl(final String id,
3838
final List<String> contentFilter,
39-
final String sortFilter) throws ParsingException {
39+
final String sortFilter)
40+
throws ParsingException, UnsupportedOperationException {
4041
try {
4142
return SoundcloudParsingHelper.resolveUrlWithEmbedPlayer(
4243
"https://api.soundcloud.com/users/" + id);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.schabi.newpipe.extractor.services.soundcloud.linkHandler;
2+
3+
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
4+
import org.schabi.newpipe.extractor.exceptions.ParsingException;
5+
import org.schabi.newpipe.extractor.exceptions.UnsupportedTabException;
6+
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
7+
8+
import javax.annotation.Nonnull;
9+
import java.util.List;
10+
11+
public final class SoundcloudChannelTabLinkHandlerFactory extends ListLinkHandlerFactory {
12+
private static final SoundcloudChannelTabLinkHandlerFactory INSTANCE
13+
= new SoundcloudChannelTabLinkHandlerFactory();
14+
15+
private SoundcloudChannelTabLinkHandlerFactory() {
16+
}
17+
18+
public static SoundcloudChannelTabLinkHandlerFactory getInstance() {
19+
return INSTANCE;
20+
}
21+
22+
@Nonnull
23+
public static String getUrlSuffix(final String tab) throws UnsupportedOperationException {
24+
switch (tab) {
25+
case ChannelTabs.TRACKS:
26+
return "/tracks";
27+
case ChannelTabs.PLAYLISTS:
28+
return "/sets";
29+
case ChannelTabs.ALBUMS:
30+
return "/albums";
31+
}
32+
throw new UnsupportedTabException(tab);
33+
}
34+
35+
@Override
36+
public String getId(final String url) throws ParsingException {
37+
return SoundcloudChannelLinkHandlerFactory.getInstance().getId(url);
38+
}
39+
40+
@Override
41+
public String getUrl(final String id,
42+
final List<String> contentFilter,
43+
final String sortFilter) throws ParsingException {
44+
return SoundcloudChannelLinkHandlerFactory.getInstance().getUrl(id)
45+
+ getUrlSuffix(contentFilter.get(0));
46+
}
47+
48+
@Override
49+
public boolean onAcceptUrl(final String url) throws ParsingException {
50+
return SoundcloudChannelLinkHandlerFactory.getInstance().onAcceptUrl(url);
51+
}
52+
53+
@Override
54+
public String[] getAvailableContentFilter() {
55+
return new String[] {
56+
ChannelTabs.TRACKS,
57+
ChannelTabs.PLAYLISTS,
58+
ChannelTabs.ALBUMS,
59+
};
60+
}
61+
}

0 commit comments

Comments
 (0)