package org.schabi.newpipe.extractor.services.soundcloud.extractors; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; 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.playlist.PlaylistExtractor; import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import javax.annotation.Nonnull; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; public class SoundcloudPlaylistExtractor extends PlaylistExtractor { private static final int STREAMS_PER_REQUESTED_PAGE = 15; private String playlistId; private JsonObject playlist; public SoundcloudPlaylistExtractor(final StreamingService service, final ListLinkHandler linkHandler) { super(service, linkHandler); } @Override public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException { playlistId = getLinkHandler().getId(); final String apiUrl = SOUNDCLOUD_API_V2_URL + "playlists/" + playlistId + "?client_id=" + SoundcloudParsingHelper.clientId() + "&representation=compact"; final String response = downloader.get(apiUrl, getExtractorLocalization()).responseBody(); try { playlist = JsonParser.object().from(response); } catch (final JsonParserException e) { throw new ParsingException("Could not parse json response", e); } } @Nonnull @Override public String getId() { return playlistId; } @Nonnull @Override public String getName() { return playlist.getString("title"); } @Nonnull @Override public String getThumbnailUrl() { String artworkUrl = playlist.getString("artwork_url"); if (artworkUrl == null) { // If the thumbnail is null, traverse the items list and get a valid one, // if it also fails, return null try { final InfoItemsPage infoItems = getInitialPage(); for (final StreamInfoItem item : infoItems.getItems()) { artworkUrl = item.getThumbnailUrl(); if (!isNullOrEmpty(artworkUrl)) { break; } } } catch (final Exception ignored) { } if (artworkUrl == null) { return ""; } } return artworkUrl.replace("large.jpg", "crop.jpg"); } @Override public String getUploaderUrl() { return SoundcloudParsingHelper.getUploaderUrl(playlist); } @Override public String getUploaderName() { return SoundcloudParsingHelper.getUploaderName(playlist); } @Override public String getUploaderAvatarUrl() { return SoundcloudParsingHelper.getAvatarUrl(playlist); } @Override public boolean isUploaderVerified() throws ParsingException { return playlist.getObject("user").getBoolean("verified"); } @Override public long getStreamCount() { return playlist.getLong("track_count"); } @Nonnull @Override public Description getDescription() throws ParsingException { final String description = playlist.getString("description"); if (isNullOrEmpty(description)) { return Description.EMPTY_DESCRIPTION; } return new Description(description, Description.PLAIN_TEXT); } @Nonnull @Override public InfoItemsPage getInitialPage() { final StreamInfoItemsCollector streamInfoItemsCollector = new StreamInfoItemsCollector(getServiceId()); final List ids = new ArrayList<>(); playlist.getArray("tracks") .stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) .forEachOrdered(track -> { // i.e. if full info is available if (track.has("title")) { streamInfoItemsCollector.commit( new SoundcloudStreamInfoItemExtractor(track)); } else { // %09d would be enough, but a 0 before the number does not create // problems, so let's be sure ids.add(String.format("%010d", track.getInt("id"))); } }); return new InfoItemsPage<>(streamInfoItemsCollector, new Page(ids)); } @Override public InfoItemsPage getPage(final Page page) throws IOException, ExtractionException { if (page == null || isNullOrEmpty(page.getIds())) { throw new IllegalArgumentException("Page doesn't contain IDs"); } final List currentIds; final List nextIds; if (page.getIds().size() <= STREAMS_PER_REQUESTED_PAGE) { // Fetch every remaining stream, there are less than the max currentIds = page.getIds(); nextIds = null; } else { currentIds = page.getIds().subList(0, STREAMS_PER_REQUESTED_PAGE); nextIds = page.getIds().subList(STREAMS_PER_REQUESTED_PAGE, page.getIds().size()); } final String currentPageUrl = SOUNDCLOUD_API_V2_URL + "tracks?client_id=" + SoundcloudParsingHelper.clientId() + "&ids=" + String.join(",", currentIds); final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); final String response = NewPipe.getDownloader().get(currentPageUrl, getExtractorLocalization()).responseBody(); try { final JsonArray tracks = JsonParser.array().from(response); // Response may not contain tracks in the same order as currentIds. // The streams are displayed in the order which is used in currentIds on SoundCloud. final HashMap idToTrack = new HashMap<>(); for (final Object track : tracks) { if (track instanceof JsonObject) { final JsonObject o = (JsonObject) track; idToTrack.put(o.getInt("id"), o); } } for (final String strId : currentIds) { final int id = Integer.parseInt(strId); try { collector.commit(new SoundcloudStreamInfoItemExtractor( Objects.requireNonNull( idToTrack.get(id), "no track with id " + id + " in response" ) )); } catch (final NullPointerException e) { throw new ParsingException("Could not parse json response", e); } } } catch (final JsonParserException e) { throw new ParsingException("Could not parse json response", e); } return new InfoItemsPage<>(collector, new Page(nextIds)); } }