Apply changes in all StreamExtractors except YouTube's one and fix extraction of PeerTube audio streams as video streams
Some code in these classes has been also refactored/improved/optimized. Also fix the extraction of PeerTube audio streams as video streams, which are now returned as audio streams.
This commit is contained in:
parent
d5f3637fc3
commit
881969f1da
|
@ -5,7 +5,6 @@ import com.grack.nanojson.JsonObject;
|
||||||
import com.grack.nanojson.JsonParser;
|
import com.grack.nanojson.JsonParser;
|
||||||
import com.grack.nanojson.JsonParserException;
|
import com.grack.nanojson.JsonParserException;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
@ -30,9 +29,12 @@ import java.util.List;
|
||||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
||||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
|
||||||
|
|
||||||
public class BandcampRadioStreamExtractor extends BandcampStreamExtractor {
|
public class BandcampRadioStreamExtractor extends BandcampStreamExtractor {
|
||||||
|
|
||||||
|
private static final String OPUS_LO = "opus_lo";
|
||||||
|
private static final String MP3_128 = "mp3-128";
|
||||||
private JsonObject showInfo;
|
private JsonObject showInfo;
|
||||||
|
|
||||||
public BandcampRadioStreamExtractor(final StreamingService service,
|
public BandcampRadioStreamExtractor(final StreamingService service,
|
||||||
|
@ -78,11 +80,9 @@ public class BandcampRadioStreamExtractor extends BandcampStreamExtractor {
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getUploaderName() throws ParsingException {
|
public String getUploaderName() {
|
||||||
return Jsoup.parse(showInfo.getString("image_caption")).getElementsByTag("a").stream()
|
return Jsoup.parse(showInfo.getString("image_caption"))
|
||||||
.map(Element::text)
|
.getElementsByTag("a").first().text();
|
||||||
.findFirst()
|
|
||||||
.orElseThrow(() -> new ParsingException("Could not get uploader name"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -116,23 +116,25 @@ public class BandcampRadioStreamExtractor extends BandcampStreamExtractor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AudioStream> getAudioStreams() {
|
public List<AudioStream> getAudioStreams() {
|
||||||
final ArrayList<AudioStream> list = new ArrayList<>();
|
final List<AudioStream> audioStreams = new ArrayList<>();
|
||||||
final JsonObject streams = showInfo.getObject("audio_stream");
|
final JsonObject streams = showInfo.getObject("audio_stream");
|
||||||
|
|
||||||
if (streams.has("opus-lo")) {
|
if (streams.has(MP3_128)) {
|
||||||
list.add(new AudioStream(
|
audioStreams.add(new AudioStream.Builder()
|
||||||
streams.getString("opus-lo"),
|
.setId(MP3_128)
|
||||||
MediaFormat.OPUS, 100
|
.setContent(streams.getString(MP3_128), true)
|
||||||
));
|
.setMediaFormat(MediaFormat.MP3)
|
||||||
}
|
.setAverageBitrate(128)
|
||||||
if (streams.has("mp3-128")) {
|
.build());
|
||||||
list.add(new AudioStream(
|
} else if (streams.has(OPUS_LO)) {
|
||||||
streams.getString("mp3-128"),
|
audioStreams.add(new AudioStream.Builder()
|
||||||
MediaFormat.MP3, 128
|
.setId(OPUS_LO)
|
||||||
));
|
.setContent(streams.getString(OPUS_LO), true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(100).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return audioStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
@ -156,14 +158,14 @@ public class BandcampRadioStreamExtractor extends BandcampStreamExtractor {
|
||||||
@Override
|
@Override
|
||||||
public String getLicence() {
|
public String getLicence() {
|
||||||
// Contrary to other Bandcamp streams, radio streams don't have a license
|
// Contrary to other Bandcamp streams, radio streams don't have a license
|
||||||
return "";
|
return EMPTY_STRING;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getCategory() {
|
public String getCategory() {
|
||||||
// Contrary to other Bandcamp streams, radio streams don't have categories
|
// Contrary to other Bandcamp streams, radio streams don't have categories
|
||||||
return "";
|
return EMPTY_STRING;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonObject;
|
import com.grack.nanojson.JsonObject;
|
||||||
import com.grack.nanojson.JsonParserException;
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
@ -10,7 +12,6 @@ import com.grack.nanojson.JsonParserException;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.jsoup.select.Elements;
|
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
import org.schabi.newpipe.extractor.MediaFormat;
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
@ -27,16 +28,15 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
public class BandcampStreamExtractor extends StreamExtractor {
|
public class BandcampStreamExtractor extends StreamExtractor {
|
||||||
|
|
||||||
private JsonObject albumJson;
|
private JsonObject albumJson;
|
||||||
private JsonObject current;
|
private JsonObject current;
|
||||||
private Document document;
|
private Document document;
|
||||||
|
@ -88,7 +88,7 @@ public class BandcampStreamExtractor extends StreamExtractor {
|
||||||
public String getUploaderUrl() throws ParsingException {
|
public String getUploaderUrl() throws ParsingException {
|
||||||
final String[] parts = getUrl().split("/");
|
final String[] parts = getUrl().split("/");
|
||||||
// https: (/) (/) * .bandcamp.com (/) and leave out the rest
|
// https: (/) (/) * .bandcamp.com (/) and leave out the rest
|
||||||
return "https://" + parts[2] + "/";
|
return HTTPS + parts[2] + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
@ -119,7 +119,7 @@ public class BandcampStreamExtractor extends StreamExtractor {
|
||||||
@Override
|
@Override
|
||||||
public String getThumbnailUrl() throws ParsingException {
|
public String getThumbnailUrl() throws ParsingException {
|
||||||
if (albumJson.isNull("art_id")) {
|
if (albumJson.isNull("art_id")) {
|
||||||
return Utils.EMPTY_STRING;
|
return EMPTY_STRING;
|
||||||
} else {
|
} else {
|
||||||
return getImageUrl(albumJson.getLong("art_id"), true);
|
return getImageUrl(albumJson.getLong("art_id"), true);
|
||||||
}
|
}
|
||||||
|
@ -139,24 +139,26 @@ public class BandcampStreamExtractor extends StreamExtractor {
|
||||||
public Description getDescription() {
|
public Description getDescription() {
|
||||||
final String s = Utils.nonEmptyAndNullJoin(
|
final String s = Utils.nonEmptyAndNullJoin(
|
||||||
"\n\n",
|
"\n\n",
|
||||||
new String[]{
|
new String[] {
|
||||||
current.getString("about"),
|
current.getString("about"),
|
||||||
current.getString("lyrics"),
|
current.getString("lyrics"),
|
||||||
current.getString("credits")
|
current.getString("credits")
|
||||||
}
|
});
|
||||||
);
|
|
||||||
return new Description(s, Description.PLAIN_TEXT);
|
return new Description(s, Description.PLAIN_TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AudioStream> getAudioStreams() {
|
public List<AudioStream> getAudioStreams() {
|
||||||
final List<AudioStream> audioStreams = new ArrayList<>();
|
final List<AudioStream> audioStreams = new ArrayList<>();
|
||||||
|
audioStreams.add(new AudioStream.Builder()
|
||||||
audioStreams.add(new AudioStream(
|
.setId("mp3-128")
|
||||||
albumJson.getArray("trackinfo").getObject(0)
|
.setContent(albumJson.getArray("trackinfo")
|
||||||
.getObject("file").getString("mp3-128"),
|
.getObject(0)
|
||||||
MediaFormat.MP3, 128
|
.getObject("file")
|
||||||
));
|
.getString("mp3-128"), true)
|
||||||
|
.setMediaFormat(MediaFormat.MP3)
|
||||||
|
.setAverageBitrate(128)
|
||||||
|
.build());
|
||||||
return audioStreams;
|
return audioStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,11 +186,11 @@ public class BandcampStreamExtractor extends StreamExtractor {
|
||||||
@Override
|
@Override
|
||||||
public PlaylistInfoItemsCollector getRelatedItems() {
|
public PlaylistInfoItemsCollector getRelatedItems() {
|
||||||
final PlaylistInfoItemsCollector collector = new PlaylistInfoItemsCollector(getServiceId());
|
final PlaylistInfoItemsCollector collector = new PlaylistInfoItemsCollector(getServiceId());
|
||||||
final Elements recommendedAlbums = document.getElementsByClass("recommended-album");
|
document.getElementsByClass("recommended-album")
|
||||||
|
.stream()
|
||||||
|
.map(BandcampRelatedPlaylistInfoItemExtractor::new)
|
||||||
|
.forEach(collector::commit);
|
||||||
|
|
||||||
for (final Element album : recommendedAlbums) {
|
|
||||||
collector.commit(new BandcampRelatedPlaylistInfoItemExtractor(album));
|
|
||||||
}
|
|
||||||
return collector;
|
return collector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,15 +202,17 @@ public class BandcampStreamExtractor extends StreamExtractor {
|
||||||
.flatMap(element -> element.getElementsByClass("tag").stream())
|
.flatMap(element -> element.getElementsByClass("tag").stream())
|
||||||
.map(Element::text)
|
.map(Element::text)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse("");
|
.orElse(EMPTY_STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getLicence() {
|
public String getLicence() {
|
||||||
/* Tests resulted in this mapping of ints to licence:
|
/*
|
||||||
|
Tests resulted in this mapping of ints to licence:
|
||||||
https://cloud.disroot.org/s/ZTWBxbQ9fKRmRWJ/preview (screenshot from a Bandcamp artist's
|
https://cloud.disroot.org/s/ZTWBxbQ9fKRmRWJ/preview (screenshot from a Bandcamp artist's
|
||||||
account) */
|
account)
|
||||||
|
*/
|
||||||
|
|
||||||
switch (current.getInt("license_type")) {
|
switch (current.getInt("license_type")) {
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -233,14 +237,9 @@ public class BandcampStreamExtractor extends StreamExtractor {
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public List<String> getTags() {
|
public List<String> getTags() {
|
||||||
final Elements tagElements = document.getElementsByAttributeValue("itemprop", "keywords");
|
return document.getElementsByAttributeValue("itemprop", "keywords")
|
||||||
|
.stream()
|
||||||
final List<String> tags = new ArrayList<>();
|
.map(Element::text)
|
||||||
|
.collect(Collectors.toList());
|
||||||
for (final Element e : tagElements) {
|
|
||||||
tags.add(e.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
return tags;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
@ -17,11 +18,21 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
|
||||||
|
|
||||||
public class MediaCCCLiveStreamExtractor extends StreamExtractor {
|
public class MediaCCCLiveStreamExtractor extends StreamExtractor {
|
||||||
|
private static final String STREAMS = "streams";
|
||||||
|
private static final String URLS = "urls";
|
||||||
|
private static final String URL = "url";
|
||||||
|
|
||||||
private JsonObject conference = null;
|
private JsonObject conference = null;
|
||||||
private String group = "";
|
private String group = "";
|
||||||
private JsonObject room = null;
|
private JsonObject room = null;
|
||||||
|
@ -34,19 +45,22 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor {
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
final JsonArray doc =
|
final JsonArray doc = MediaCCCParsingHelper.getLiveStreams(downloader,
|
||||||
MediaCCCParsingHelper.getLiveStreams(downloader, getExtractorLocalization());
|
getExtractorLocalization());
|
||||||
// find correct room
|
// Find the correct room
|
||||||
for (int c = 0; c < doc.size(); c++) {
|
for (int c = 0; c < doc.size(); c++) {
|
||||||
conference = doc.getObject(c);
|
final JsonObject conferenceObject = doc.getObject(c);
|
||||||
final JsonArray groups = conference.getArray("groups");
|
final JsonArray groups = conferenceObject.getArray("groups");
|
||||||
for (int g = 0; g < groups.size(); g++) {
|
for (int g = 0; g < groups.size(); g++) {
|
||||||
group = groups.getObject(g).getString("group");
|
final String groupObject = groups.getObject(g).getString("group");
|
||||||
final JsonArray rooms = groups.getObject(g).getArray("rooms");
|
final JsonArray rooms = groups.getObject(g).getArray("rooms");
|
||||||
for (int r = 0; r < rooms.size(); r++) {
|
for (int r = 0; r < rooms.size(); r++) {
|
||||||
room = rooms.getObject(r);
|
final JsonObject roomObject = rooms.getObject(r);
|
||||||
if (getId().equals(
|
if (getId().equals(conferenceObject.getString("slug") + "/"
|
||||||
conference.getString("slug") + "/" + room.getString("slug"))) {
|
+ roomObject.getString("slug"))) {
|
||||||
|
this.conference = conferenceObject;
|
||||||
|
this.group = groupObject;
|
||||||
|
this.room = roomObject;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,69 +105,136 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor {
|
||||||
return conference.getString("conference");
|
return conference.getString("conference");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL of the first DASH stream found.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* There can be several DASH streams, so the URL of the first found is returned by this method.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* You can find the other video DASH streams by using {@link #getVideoStreams()}
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getDashMpdUrl() throws ParsingException {
|
||||||
|
|
||||||
|
for (int s = 0; s < room.getArray(STREAMS).size(); s++) {
|
||||||
|
final JsonObject stream = room.getArray(STREAMS).getObject(s);
|
||||||
|
final JsonObject urls = stream.getObject(URLS);
|
||||||
|
if (urls.has("dash")) {
|
||||||
|
return urls.getObject("dash").getString(URL, EMPTY_STRING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL of the first HLS stream found.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* There can be several HLS streams, so the URL of the first found is returned by this method.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* You can find the other video HLS streams by using {@link #getVideoStreams()}
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getHlsUrl() {
|
public String getHlsUrl() {
|
||||||
// TODO: There are multiple HLS streams.
|
for (int s = 0; s < room.getArray(STREAMS).size(); s++) {
|
||||||
// Make getHlsUrl() and getDashMpdUrl() return lists of VideoStreams,
|
final JsonObject stream = room.getArray(STREAMS).getObject(s);
|
||||||
// so the user can choose a resolution.
|
final JsonObject urls = stream.getObject(URLS);
|
||||||
for (int s = 0; s < room.getArray("streams").size(); s++) {
|
if (urls.has("hls")) {
|
||||||
final JsonObject stream = room.getArray("streams").getObject(s);
|
return urls.getObject("hls").getString(URL, EMPTY_STRING);
|
||||||
if (stream.getString("type").equals("video")) {
|
|
||||||
if (stream.has("hls")) {
|
|
||||||
return stream.getObject("urls").getObject("hls").getString("url");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return EMPTY_STRING;
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
|
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
|
||||||
final List<AudioStream> audioStreams = new ArrayList<>();
|
final List<AudioStream> audioStreams = new ArrayList<>();
|
||||||
for (int s = 0; s < room.getArray("streams").size(); s++) {
|
IntStream.range(0, room.getArray(STREAMS).size())
|
||||||
final JsonObject stream = room.getArray("streams").getObject(s);
|
.mapToObj(s -> room.getArray(STREAMS).getObject(s))
|
||||||
if (stream.getString("type").equals("audio")) {
|
.filter(streamJsonObject -> streamJsonObject.getString("type").equals("audio"))
|
||||||
for (final String type : stream.getObject("urls").keySet()) {
|
.forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet()
|
||||||
final JsonObject url = stream.getObject("urls").getObject(type);
|
.forEach(type -> {
|
||||||
audioStreams.add(new AudioStream(url.getString("url"),
|
final JsonObject urlObject = streamJsonObject.getObject(URLS)
|
||||||
MediaFormat.getFromSuffix(type), -1));
|
.getObject(type);
|
||||||
}
|
// The DASH manifest will be extracted with getDashMpdUrl
|
||||||
|
if (!type.equals("dash")) {
|
||||||
|
final AudioStream.Builder builder = new AudioStream.Builder()
|
||||||
|
.setId(urlObject.getString("tech", ID_UNKNOWN))
|
||||||
|
.setContent(urlObject.getString(URL), true)
|
||||||
|
.setAverageBitrate(UNKNOWN_BITRATE);
|
||||||
|
if (type.equals("hls")) {
|
||||||
|
// We don't know with the type string what media format will
|
||||||
|
// have HLS streams.
|
||||||
|
// However, the tech string may contain some information
|
||||||
|
// about the media format used.
|
||||||
|
builder.setDeliveryMethod(DeliveryMethod.HLS);
|
||||||
|
} else {
|
||||||
|
builder.setMediaFormat(MediaFormat.getFromSuffix(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audioStreams.add(builder.build());
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
return audioStreams;
|
return audioStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<VideoStream> getVideoStreams() throws IOException, ExtractionException {
|
public List<VideoStream> getVideoStreams() throws IOException, ExtractionException {
|
||||||
final List<VideoStream> videoStreams = new ArrayList<>();
|
final List<VideoStream> videoStreams = new ArrayList<>();
|
||||||
for (int s = 0; s < room.getArray("streams").size(); s++) {
|
IntStream.range(0, room.getArray(STREAMS).size())
|
||||||
final JsonObject stream = room.getArray("streams").getObject(s);
|
.mapToObj(s -> room.getArray(STREAMS).getObject(s))
|
||||||
if (stream.getString("type").equals("video")) {
|
.filter(stream -> stream.getString("type").equals("video"))
|
||||||
final String resolution = stream.getArray("videoSize").getInt(0) + "x"
|
.forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet()
|
||||||
+ stream.getArray("videoSize").getInt(1);
|
.forEach(type -> {
|
||||||
for (final String type : stream.getObject("urls").keySet()) {
|
final String resolution =
|
||||||
if (!type.equals("hls")) {
|
streamJsonObject.getArray("videoSize").getInt(0)
|
||||||
final JsonObject url = stream.getObject("urls").getObject(type);
|
+ "x"
|
||||||
videoStreams.add(new VideoStream(
|
+ streamJsonObject.getArray("videoSize").getInt(1);
|
||||||
url.getString("url"),
|
final JsonObject urlObject = streamJsonObject.getObject(URLS)
|
||||||
MediaFormat.getFromSuffix(type),
|
.getObject(type);
|
||||||
resolution));
|
// The DASH manifest will be extracted with getDashMpdUrl
|
||||||
}
|
if (!type.equals("dash")) {
|
||||||
}
|
final VideoStream.Builder builder = new VideoStream.Builder()
|
||||||
|
.setId(urlObject.getString("tech", ID_UNKNOWN))
|
||||||
|
.setContent(urlObject.getString(URL), true)
|
||||||
|
.setIsVideoOnly(false)
|
||||||
|
.setResolution(resolution);
|
||||||
|
|
||||||
|
if (type.equals("hls")) {
|
||||||
|
// We don't know with the type string what media format will
|
||||||
|
// have HLS streams.
|
||||||
|
// However, the tech string may contain some information
|
||||||
|
// about the media format used.
|
||||||
|
builder.setDeliveryMethod(DeliveryMethod.HLS);
|
||||||
|
} else {
|
||||||
|
builder.setMediaFormat(MediaFormat.getFromSuffix(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
videoStreams.add(builder.build());
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
return videoStreams;
|
return videoStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<VideoStream> getVideoOnlyStreams() {
|
public List<VideoStream> getVideoOnlyStreams() {
|
||||||
return null;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamType getStreamType() throws ParsingException {
|
public StreamType getStreamType() throws ParsingException {
|
||||||
return StreamType.LIVE_STREAM; // TODO: video and audio only streams are both available
|
return StreamType.LIVE_STREAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package org.schabi.newpipe.extractor.services.media_ccc.extractors;
|
package org.schabi.newpipe.extractor.services.media_ccc.extractors;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN;
|
||||||
|
|
||||||
import com.grack.nanojson.JsonArray;
|
import com.grack.nanojson.JsonArray;
|
||||||
import com.grack.nanojson.JsonObject;
|
import com.grack.nanojson.JsonObject;
|
||||||
import com.grack.nanojson.JsonParser;
|
import com.grack.nanojson.JsonParser;
|
||||||
|
@ -99,7 +102,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
||||||
final JsonObject recording = recordings.getObject(i);
|
final JsonObject recording = recordings.getObject(i);
|
||||||
final String mimeType = recording.getString("mime_type");
|
final String mimeType = recording.getString("mime_type");
|
||||||
if (mimeType.startsWith("audio")) {
|
if (mimeType.startsWith("audio")) {
|
||||||
//first we need to resolve the actual video data from CDN
|
// First we need to resolve the actual video data from CDN
|
||||||
final MediaFormat mediaFormat;
|
final MediaFormat mediaFormat;
|
||||||
if (mimeType.endsWith("opus")) {
|
if (mimeType.endsWith("opus")) {
|
||||||
mediaFormat = MediaFormat.OPUS;
|
mediaFormat = MediaFormat.OPUS;
|
||||||
|
@ -108,11 +111,18 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
||||||
} else if (mimeType.endsWith("ogg")) {
|
} else if (mimeType.endsWith("ogg")) {
|
||||||
mediaFormat = MediaFormat.OGG;
|
mediaFormat = MediaFormat.OGG;
|
||||||
} else {
|
} else {
|
||||||
throw new ExtractionException("Unknown media format: " + mimeType);
|
mediaFormat = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
audioStreams.add(new AudioStream(recording.getString("recording_url"),
|
// Don't use the containsSimilarStream method because it will always return
|
||||||
mediaFormat, -1));
|
// false so if there are multiples audio streams available, only the first will
|
||||||
|
// be extracted in this case.
|
||||||
|
audioStreams.add(new AudioStream.Builder()
|
||||||
|
.setId(recording.getString("filename", ID_UNKNOWN))
|
||||||
|
.setContent(recording.getString("recording_url"), true)
|
||||||
|
.setMediaFormat(mediaFormat)
|
||||||
|
.setAverageBitrate(UNKNOWN_BITRATE)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return audioStreams;
|
return audioStreams;
|
||||||
|
@ -126,7 +136,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
||||||
final JsonObject recording = recordings.getObject(i);
|
final JsonObject recording = recordings.getObject(i);
|
||||||
final String mimeType = recording.getString("mime_type");
|
final String mimeType = recording.getString("mime_type");
|
||||||
if (mimeType.startsWith("video")) {
|
if (mimeType.startsWith("video")) {
|
||||||
//first we need to resolve the actual video data from CDN
|
// First we need to resolve the actual video data from CDN
|
||||||
|
|
||||||
final MediaFormat mediaFormat;
|
final MediaFormat mediaFormat;
|
||||||
if (mimeType.endsWith("webm")) {
|
if (mimeType.endsWith("webm")) {
|
||||||
|
@ -134,13 +144,21 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
||||||
} else if (mimeType.endsWith("mp4")) {
|
} else if (mimeType.endsWith("mp4")) {
|
||||||
mediaFormat = MediaFormat.MPEG_4;
|
mediaFormat = MediaFormat.MPEG_4;
|
||||||
} else {
|
} else {
|
||||||
throw new ExtractionException("Unknown media format: " + mimeType);
|
mediaFormat = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
videoStreams.add(new VideoStream(recording.getString("recording_url"),
|
// Don't use the containsSimilarStream method because it will remove the
|
||||||
mediaFormat, recording.getInt("height") + "p"));
|
// extraction of some video versions (mostly languages)
|
||||||
|
videoStreams.add(new VideoStream.Builder()
|
||||||
|
.setId(recording.getString("filename", ID_UNKNOWN))
|
||||||
|
.setContent(recording.getString("recording_url"), true)
|
||||||
|
.setIsVideoOnly(false)
|
||||||
|
.setMediaFormat(mediaFormat)
|
||||||
|
.setResolution(recording.getInt("height") + "p")
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return videoStreams;
|
return videoStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,7 +181,8 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
||||||
conferenceData = JsonParser.object()
|
conferenceData = JsonParser.object()
|
||||||
.from(downloader.get(data.getString("conference_url")).responseBody());
|
.from(downloader.get(data.getString("conference_url")).responseBody());
|
||||||
} catch (final JsonParserException jpe) {
|
} catch (final JsonParserException jpe) {
|
||||||
throw new ExtractionException("Could not parse json returned by url: " + videoUrl, jpe);
|
throw new ExtractionException("Could not parse json returned by URL: " + videoUrl,
|
||||||
|
jpe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
|
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
|
||||||
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeStreamLinkHandlerFactory;
|
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeStreamLinkHandlerFactory;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
import org.schabi.newpipe.extractor.stream.Stream;
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
|
@ -39,14 +40,30 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
|
||||||
|
|
||||||
public class PeertubeStreamExtractor extends StreamExtractor {
|
public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
|
private static final String ACCOUNT_HOST = "account.host";
|
||||||
|
private static final String ACCOUNT_NAME = "account.name";
|
||||||
|
private static final String FILES = "files";
|
||||||
|
private static final String FILE_DOWNLOAD_URL = "fileDownloadUrl";
|
||||||
|
private static final String FILE_URL = "fileUrl";
|
||||||
|
private static final String PLAYLIST_URL = "playlistUrl";
|
||||||
|
private static final String RESOLUTION_ID = "resolution.id";
|
||||||
|
private static final String STREAMING_PLAYLISTS = "streamingPlaylists";
|
||||||
|
|
||||||
private final String baseUrl;
|
private final String baseUrl;
|
||||||
private JsonObject json;
|
private JsonObject json;
|
||||||
|
|
||||||
private final List<SubtitlesStream> subtitles = new ArrayList<>();
|
private final List<SubtitlesStream> subtitles = new ArrayList<>();
|
||||||
|
private final List<AudioStream> audioStreams = new ArrayList<>();
|
||||||
|
private final List<VideoStream> videoStreams = new ArrayList<>();
|
||||||
|
|
||||||
public PeertubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler)
|
public PeertubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler)
|
||||||
throws ParsingException {
|
throws ParsingException {
|
||||||
|
@ -85,9 +102,8 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
} catch (final ParsingException e) {
|
} catch (final ParsingException e) {
|
||||||
return Description.EMPTY_DESCRIPTION;
|
return Description.EMPTY_DESCRIPTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text.length() == 250 && text.substring(247).equals("...")) {
|
if (text.length() == 250 && text.substring(247).equals("...")) {
|
||||||
//if description is shortened, get full description
|
// If description is shortened, get full description
|
||||||
final Downloader dl = NewPipe.getDownloader();
|
final Downloader dl = NewPipe.getDownloader();
|
||||||
try {
|
try {
|
||||||
final Response response = dl.get(baseUrl
|
final Response response = dl.get(baseUrl
|
||||||
|
@ -95,8 +111,8 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
+ getId() + "/description");
|
+ getId() + "/description");
|
||||||
final JsonObject jsonObject = JsonParser.object().from(response.responseBody());
|
final JsonObject jsonObject = JsonParser.object().from(response.responseBody());
|
||||||
text = JsonUtils.getString(jsonObject, "description");
|
text = JsonUtils.getString(jsonObject, "description");
|
||||||
} catch (ReCaptchaException | IOException | JsonParserException e) {
|
} catch (final IOException | ReCaptchaException | JsonParserException ignored) {
|
||||||
e.printStackTrace();
|
// Something went wrong when getting the full description, use the shortened one
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Description(text, Description.MARKDOWN);
|
return new Description(text, Description.MARKDOWN);
|
||||||
|
@ -119,8 +135,8 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getTimeStamp() throws ParsingException {
|
public long getTimeStamp() throws ParsingException {
|
||||||
final long timestamp =
|
final long timestamp = getTimestampSeconds(
|
||||||
getTimestampSeconds("((#|&|\\?)start=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)");
|
"((#|&|\\?)start=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)");
|
||||||
|
|
||||||
if (timestamp == -2) {
|
if (timestamp == -2) {
|
||||||
// regex for timestamp was not found
|
// regex for timestamp was not found
|
||||||
|
@ -148,10 +164,10 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getUploaderUrl() throws ParsingException {
|
public String getUploaderUrl() throws ParsingException {
|
||||||
final String name = JsonUtils.getString(json, "account.name");
|
final String name = JsonUtils.getString(json, ACCOUNT_NAME);
|
||||||
final String host = JsonUtils.getString(json, "account.host");
|
final String host = JsonUtils.getString(json, ACCOUNT_HOST);
|
||||||
return getService().getChannelLHFactory()
|
return getService().getChannelLHFactory().fromId("accounts/" + name + "@" + host, baseUrl)
|
||||||
.fromId("accounts/" + name + "@" + host, baseUrl).getUrl();
|
.getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
@ -199,75 +215,49 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getHlsUrl() {
|
public String getHlsUrl() {
|
||||||
return json.getArray("streamingPlaylists").getObject(0).getString("playlistUrl");
|
assertPageFetched();
|
||||||
|
|
||||||
|
if (getStreamType() == StreamType.VIDEO_STREAM
|
||||||
|
&& !isNullOrEmpty(json.getObject(FILES))) {
|
||||||
|
return json.getObject(FILES).getString(PLAYLIST_URL, EMPTY_STRING);
|
||||||
|
} else {
|
||||||
|
return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL,
|
||||||
|
EMPTY_STRING);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AudioStream> getAudioStreams() {
|
public List<AudioStream> getAudioStreams() throws ParsingException {
|
||||||
return Collections.emptyList();
|
assertPageFetched();
|
||||||
|
|
||||||
|
/*
|
||||||
|
Some videos have audio streams, some videos don't have audio streams.
|
||||||
|
So an audio stream may be available if a video stream is available.
|
||||||
|
Audio streams are also not returned as separated streams for livestreams.
|
||||||
|
That's why the extraction of audio streams is only run when there are video streams
|
||||||
|
extracted and when the content is not a livestream.
|
||||||
|
*/
|
||||||
|
if (audioStreams.isEmpty() && videoStreams.isEmpty()
|
||||||
|
&& getStreamType() == StreamType.VIDEO_STREAM) {
|
||||||
|
getStreams();
|
||||||
|
}
|
||||||
|
|
||||||
|
return audioStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<VideoStream> getVideoStreams() throws ExtractionException {
|
public List<VideoStream> getVideoStreams() throws ExtractionException {
|
||||||
assertPageFetched();
|
assertPageFetched();
|
||||||
final List<VideoStream> videoStreams = new ArrayList<>();
|
|
||||||
|
|
||||||
// mp4
|
if (videoStreams.isEmpty()) {
|
||||||
try {
|
if (getStreamType() == StreamType.VIDEO_STREAM) {
|
||||||
videoStreams.addAll(getVideoStreamsFromArray(json.getArray("files")));
|
getStreams();
|
||||||
} catch (final Exception ignored) { }
|
|
||||||
|
|
||||||
// HLS
|
|
||||||
try {
|
|
||||||
final JsonArray streamingPlaylists = json.getArray("streamingPlaylists");
|
|
||||||
for (final Object p : streamingPlaylists) {
|
|
||||||
if (!(p instanceof JsonObject)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final JsonObject playlist = (JsonObject) p;
|
|
||||||
videoStreams.addAll(getVideoStreamsFromArray(playlist.getArray("files")));
|
|
||||||
}
|
|
||||||
} catch (final Exception e) {
|
|
||||||
throw new ParsingException("Could not get video streams", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getStreamType() == StreamType.LIVE_STREAM) {
|
|
||||||
videoStreams.add(new VideoStream(getHlsUrl(), MediaFormat.MPEG_4, "720p"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return videoStreams;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<VideoStream> getVideoStreamsFromArray(final JsonArray streams)
|
|
||||||
throws ParsingException {
|
|
||||||
try {
|
|
||||||
final List<VideoStream> videoStreams = new ArrayList<>();
|
|
||||||
for (final Object s : streams) {
|
|
||||||
if (!(s instanceof JsonObject)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final JsonObject stream = (JsonObject) s;
|
|
||||||
final String url;
|
|
||||||
if (stream.has("fileDownloadUrl")) {
|
|
||||||
url = JsonUtils.getString(stream, "fileDownloadUrl");
|
|
||||||
} else {
|
} else {
|
||||||
url = JsonUtils.getString(stream, "fileUrl");
|
extractLiveVideoStreams();
|
||||||
}
|
}
|
||||||
final String torrentUrl = JsonUtils.getString(stream, "torrentUrl");
|
|
||||||
final String resolution = JsonUtils.getString(stream, "resolution.label");
|
|
||||||
final String extension = url.substring(url.lastIndexOf(".") + 1);
|
|
||||||
final MediaFormat format = MediaFormat.getFromSuffix(extension);
|
|
||||||
final VideoStream videoStream
|
|
||||||
= new VideoStream(url, torrentUrl, format, resolution);
|
|
||||||
if (!Stream.containSimilarStream(videoStream, videoStreams)) {
|
|
||||||
videoStreams.add(videoStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return videoStreams;
|
|
||||||
} catch (final Exception e) {
|
|
||||||
throw new ParsingException("Could not get video streams from array");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return videoStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -284,13 +274,9 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public List<SubtitlesStream> getSubtitles(final MediaFormat format) {
|
public List<SubtitlesStream> getSubtitles(final MediaFormat format) {
|
||||||
final List<SubtitlesStream> filteredSubs = new ArrayList<>();
|
return subtitles.stream()
|
||||||
for (final SubtitlesStream sub : subtitles) {
|
.filter(sub -> sub.getFormat() == format)
|
||||||
if (sub.getFormat() == format) {
|
.collect(Collectors.toList());
|
||||||
filteredSubs.add(sub);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filteredSubs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -304,8 +290,8 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
final List<String> tags = getTags();
|
final List<String> tags = getTags();
|
||||||
final String apiUrl;
|
final String apiUrl;
|
||||||
if (tags.isEmpty()) {
|
if (tags.isEmpty()) {
|
||||||
apiUrl = baseUrl + "/api/v1/accounts/" + JsonUtils.getString(json, "account.name")
|
apiUrl = baseUrl + "/api/v1/accounts/" + JsonUtils.getString(json, ACCOUNT_NAME)
|
||||||
+ "@" + JsonUtils.getString(json, "account.host")
|
+ "@" + JsonUtils.getString(json, ACCOUNT_HOST)
|
||||||
+ "/videos?start=0&count=8";
|
+ "/videos?start=0&count=8";
|
||||||
} else {
|
} else {
|
||||||
apiUrl = getRelatedItemsUrl(tags);
|
apiUrl = getRelatedItemsUrl(tags);
|
||||||
|
@ -314,7 +300,8 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
if (Utils.isBlank(apiUrl)) {
|
if (Utils.isBlank(apiUrl)) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(
|
||||||
|
getServiceId());
|
||||||
getStreamsFromApi(collector, apiUrl);
|
getStreamsFromApi(collector, apiUrl);
|
||||||
return collector;
|
return collector;
|
||||||
}
|
}
|
||||||
|
@ -332,11 +319,13 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
try {
|
try {
|
||||||
return JsonUtils.getString(json, "support");
|
return JsonUtils.getString(json, "support");
|
||||||
} catch (final ParsingException e) {
|
} catch (final ParsingException e) {
|
||||||
return "";
|
return EMPTY_STRING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getRelatedItemsUrl(final List<String> tags) throws UnsupportedEncodingException {
|
@Nonnull
|
||||||
|
private String getRelatedItemsUrl(@Nonnull final List<String> tags)
|
||||||
|
throws UnsupportedEncodingException {
|
||||||
final String url = baseUrl + PeertubeSearchQueryHandlerFactory.SEARCH_ENDPOINT;
|
final String url = baseUrl + PeertubeSearchQueryHandlerFactory.SEARCH_ENDPOINT;
|
||||||
final StringBuilder params = new StringBuilder();
|
final StringBuilder params = new StringBuilder();
|
||||||
params.append("start=0&count=8&sort=-createdAt");
|
params.append("start=0&count=8&sort=-createdAt");
|
||||||
|
@ -348,7 +337,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getStreamsFromApi(final StreamInfoItemsCollector collector, final String apiUrl)
|
private void getStreamsFromApi(final StreamInfoItemsCollector collector, final String apiUrl)
|
||||||
throws ReCaptchaException, IOException, ParsingException {
|
throws IOException, ReCaptchaException, ParsingException {
|
||||||
final Response response = getDownloader().get(apiUrl);
|
final Response response = getDownloader().get(apiUrl);
|
||||||
JsonObject relatedVideosJson = null;
|
JsonObject relatedVideosJson = null;
|
||||||
if (response != null && !Utils.isBlank(response.responseBody())) {
|
if (response != null && !Utils.isBlank(response.responseBody())) {
|
||||||
|
@ -365,21 +354,20 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectStreamsFrom(final StreamInfoItemsCollector collector,
|
private void collectStreamsFrom(final StreamInfoItemsCollector collector,
|
||||||
final JsonObject jsonObject)
|
final JsonObject jsonObject) throws ParsingException {
|
||||||
throws ParsingException {
|
|
||||||
final JsonArray contents;
|
final JsonArray contents;
|
||||||
try {
|
try {
|
||||||
contents = (JsonArray) JsonUtils.getValue(jsonObject, "data");
|
contents = (JsonArray) JsonUtils.getValue(jsonObject, "data");
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
throw new ParsingException("unable to extract related videos", e);
|
throw new ParsingException("Could not extract related videos", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final Object c : contents) {
|
for (final Object c : contents) {
|
||||||
if (c instanceof JsonObject) {
|
if (c instanceof JsonObject) {
|
||||||
final JsonObject item = (JsonObject) c;
|
final JsonObject item = (JsonObject) c;
|
||||||
final PeertubeStreamInfoItemExtractor extractor
|
final PeertubeStreamInfoItemExtractor extractor =
|
||||||
= new PeertubeStreamInfoItemExtractor(item, baseUrl);
|
new PeertubeStreamInfoItemExtractor(item, baseUrl);
|
||||||
//do not add the same stream in related streams
|
// Do not add the same stream in related streams
|
||||||
if (!extractor.getUrl().equals(getUrl())) {
|
if (!extractor.getUrl().equals(getUrl())) {
|
||||||
collector.commit(extractor);
|
collector.commit(extractor);
|
||||||
}
|
}
|
||||||
|
@ -395,7 +383,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
setInitialData(response.responseBody());
|
setInitialData(response.responseBody());
|
||||||
} else {
|
} else {
|
||||||
throw new ExtractionException("Unable to extract PeerTube channel data");
|
throw new ExtractionException("Could not extract PeerTube channel data");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSubtitles();
|
loadSubtitles();
|
||||||
|
@ -405,10 +393,10 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
try {
|
try {
|
||||||
json = JsonParser.object().from(responseBody);
|
json = JsonParser.object().from(responseBody);
|
||||||
} catch (final JsonParserException e) {
|
} catch (final JsonParserException e) {
|
||||||
throw new ExtractionException("Unable to extract PeerTube stream data", e);
|
throw new ExtractionException("Could not extract PeerTube stream data", e);
|
||||||
}
|
}
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
throw new ExtractionException("Unable to extract PeerTube stream data");
|
throw new ExtractionException("Could not extract PeerTube stream data");
|
||||||
}
|
}
|
||||||
PeertubeParsingHelper.validate(json);
|
PeertubeParsingHelper.validate(json);
|
||||||
}
|
}
|
||||||
|
@ -429,14 +417,251 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
final String ext = url.substring(url.lastIndexOf(".") + 1);
|
final String ext = url.substring(url.lastIndexOf(".") + 1);
|
||||||
final MediaFormat fmt = MediaFormat.getFromSuffix(ext);
|
final MediaFormat fmt = MediaFormat.getFromSuffix(ext);
|
||||||
if (fmt != null && !isNullOrEmpty(languageCode)) {
|
if (fmt != null && !isNullOrEmpty(languageCode)) {
|
||||||
subtitles.add(new SubtitlesStream(fmt, languageCode, url, false));
|
subtitles.add(new SubtitlesStream.Builder()
|
||||||
|
.setContent(url, true)
|
||||||
|
.setMediaFormat(fmt)
|
||||||
|
.setLanguageCode(languageCode)
|
||||||
|
.setAutoGenerated(false)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (final Exception ignored) {
|
||||||
|
// Ignore all exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extractLiveVideoStreams() throws ParsingException {
|
||||||
|
try {
|
||||||
|
final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS);
|
||||||
|
for (final Object s : streamingPlaylists) {
|
||||||
|
if (!(s instanceof JsonObject)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final JsonObject stream = (JsonObject) s;
|
||||||
|
// Don't use the containsSimilarStream method because it will always return false
|
||||||
|
// so if there are multiples HLS URLs returned, only the first will be extracted in
|
||||||
|
// this case.
|
||||||
|
videoStreams.add(new VideoStream.Builder()
|
||||||
|
.setId(String.valueOf(stream.getInt("id", -1)))
|
||||||
|
.setContent(stream.getString(PLAYLIST_URL, EMPTY_STRING), true)
|
||||||
|
.setIsVideoOnly(false)
|
||||||
|
.setResolution(EMPTY_STRING)
|
||||||
|
.setMediaFormat(MediaFormat.MPEG_4)
|
||||||
|
.setDeliveryMethod(DeliveryMethod.HLS)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new ParsingException("Could not get video streams", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getStreams() throws ParsingException {
|
||||||
|
// Progressive streams
|
||||||
|
getStreamsFromArray(json.getArray(FILES), EMPTY_STRING);
|
||||||
|
|
||||||
|
// HLS streams
|
||||||
|
try {
|
||||||
|
final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS);
|
||||||
|
for (final Object p : streamingPlaylists) {
|
||||||
|
if (!(p instanceof JsonObject)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final JsonObject playlist = (JsonObject) p;
|
||||||
|
final String playlistUrl = playlist.getString(PLAYLIST_URL);
|
||||||
|
getStreamsFromArray(playlist.getArray(FILES), playlistUrl);
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new ParsingException("Could not get streams", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getStreamsFromArray(@Nonnull final JsonArray streams,
|
||||||
|
final String playlistUrl) throws ParsingException {
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
Starting with version 3.4.0 of PeerTube, HLS playlist of stream resolutions contain the
|
||||||
|
UUID of the stream, so we can't use the same system to get HLS playlist URL of streams
|
||||||
|
without fetching the master playlist.
|
||||||
|
These UUIDs are the same that the ones returned into the fileUrl and fileDownloadUrl
|
||||||
|
strings.
|
||||||
|
*/
|
||||||
|
final boolean isInstanceUsingRandomUuidsForHlsStreams = !isNullOrEmpty(playlistUrl)
|
||||||
|
&& playlistUrl.endsWith("-master.m3u8");
|
||||||
|
|
||||||
|
for (final Object s : streams) {
|
||||||
|
if (!(s instanceof JsonObject)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonObject stream = (JsonObject) s;
|
||||||
|
final String resolution = JsonUtils.getString(stream, "resolution.label");
|
||||||
|
final String url;
|
||||||
|
final String idSuffix;
|
||||||
|
|
||||||
|
// Extract stream version of streams first
|
||||||
|
if (stream.has(FILE_URL)) {
|
||||||
|
url = JsonUtils.getString(stream, FILE_URL);
|
||||||
|
idSuffix = FILE_URL;
|
||||||
|
} else {
|
||||||
|
url = JsonUtils.getString(stream, FILE_DOWNLOAD_URL);
|
||||||
|
idSuffix = FILE_DOWNLOAD_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNullOrEmpty(url)) {
|
||||||
|
// Not a valid stream URL
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolution.toLowerCase().contains("audio")) {
|
||||||
|
// An audio stream
|
||||||
|
addNewAudioStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution,
|
||||||
|
idSuffix, url, playlistUrl);
|
||||||
|
} else {
|
||||||
|
// A video stream
|
||||||
|
addNewVideoStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution,
|
||||||
|
idSuffix, url, playlistUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
// ignore all exceptions
|
throw new ParsingException("Could not get streams from array", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private String getHlsPlaylistUrlFromFragmentedFileUrl(
|
||||||
|
@Nonnull final JsonObject streamJsonObject,
|
||||||
|
@Nonnull final String idSuffix,
|
||||||
|
@Nonnull final String format,
|
||||||
|
@Nonnull final String url) throws ParsingException {
|
||||||
|
final String streamUrl;
|
||||||
|
if (FILE_DOWNLOAD_URL.equals(idSuffix)) {
|
||||||
|
streamUrl = JsonUtils.getString(streamJsonObject, FILE_URL);
|
||||||
|
} else {
|
||||||
|
streamUrl = url;
|
||||||
|
}
|
||||||
|
return streamUrl.replace("-fragmented." + format, ".m3u8");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private String getHlsPlaylistUrlFromMasterPlaylist(@Nonnull final JsonObject streamJsonObject,
|
||||||
|
@Nonnull final String playlistUrl)
|
||||||
|
throws ParsingException {
|
||||||
|
return playlistUrl.replace("master", JsonUtils.getNumber(streamJsonObject,
|
||||||
|
RESOLUTION_ID).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNewAudioStream(@Nonnull final JsonObject streamJsonObject,
|
||||||
|
final boolean isInstanceUsingRandomUuidsForHlsStreams,
|
||||||
|
@Nonnull final String resolution,
|
||||||
|
@Nonnull final String idSuffix,
|
||||||
|
@Nonnull final String url,
|
||||||
|
@Nullable final String playlistUrl) throws ParsingException {
|
||||||
|
final String extension = url.substring(url.lastIndexOf(".") + 1);
|
||||||
|
final MediaFormat format = MediaFormat.getFromSuffix(extension);
|
||||||
|
final String id = resolution + "-" + extension;
|
||||||
|
|
||||||
|
// Add progressive HTTP streams first
|
||||||
|
audioStreams.add(new AudioStream.Builder()
|
||||||
|
.setId(id + "-" + idSuffix + "-" + DeliveryMethod.PROGRESSIVE_HTTP)
|
||||||
|
.setContent(url, true)
|
||||||
|
.setMediaFormat(format)
|
||||||
|
.setAverageBitrate(UNKNOWN_BITRATE)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Then add HLS streams
|
||||||
|
if (!isNullOrEmpty(playlistUrl)) {
|
||||||
|
final String hlsStreamUrl;
|
||||||
|
if (isInstanceUsingRandomUuidsForHlsStreams) {
|
||||||
|
hlsStreamUrl = getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix,
|
||||||
|
extension, url);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
hlsStreamUrl = getHlsPlaylistUrlFromMasterPlaylist(streamJsonObject, playlistUrl);
|
||||||
|
}
|
||||||
|
final AudioStream audioStream = new AudioStream.Builder()
|
||||||
|
.setId(id + "-" + DeliveryMethod.HLS)
|
||||||
|
.setContent(hlsStreamUrl, true)
|
||||||
|
.setDeliveryMethod(DeliveryMethod.HLS)
|
||||||
|
.setMediaFormat(format)
|
||||||
|
.setAverageBitrate(UNKNOWN_BITRATE)
|
||||||
|
.setBaseUrl(playlistUrl)
|
||||||
|
.build();
|
||||||
|
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
|
||||||
|
audioStreams.add(audioStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add finally torrent URLs
|
||||||
|
final String torrentUrl = JsonUtils.getString(streamJsonObject, "torrentUrl");
|
||||||
|
if (!isNullOrEmpty(torrentUrl)) {
|
||||||
|
audioStreams.add(new AudioStream.Builder()
|
||||||
|
.setId(id + "-" + idSuffix + "-" + DeliveryMethod.TORRENT)
|
||||||
|
.setContent(torrentUrl, true)
|
||||||
|
.setDeliveryMethod(DeliveryMethod.TORRENT)
|
||||||
|
.setMediaFormat(format)
|
||||||
|
.setAverageBitrate(UNKNOWN_BITRATE)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNewVideoStream(@Nonnull final JsonObject streamJsonObject,
|
||||||
|
final boolean isInstanceUsingRandomUuidsForHlsStreams,
|
||||||
|
@Nonnull final String resolution,
|
||||||
|
@Nonnull final String idSuffix,
|
||||||
|
@Nonnull final String url,
|
||||||
|
@Nullable final String playlistUrl) throws ParsingException {
|
||||||
|
final String extension = url.substring(url.lastIndexOf(".") + 1);
|
||||||
|
final MediaFormat format = MediaFormat.getFromSuffix(extension);
|
||||||
|
final String id = resolution + "-" + extension;
|
||||||
|
|
||||||
|
// Add progressive HTTP streams first
|
||||||
|
videoStreams.add(new VideoStream.Builder()
|
||||||
|
.setId(id + "-" + idSuffix + "-" + DeliveryMethod.PROGRESSIVE_HTTP)
|
||||||
|
.setContent(url, true)
|
||||||
|
.setIsVideoOnly(false)
|
||||||
|
.setResolution(resolution)
|
||||||
|
.setMediaFormat(format)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Then add HLS streams
|
||||||
|
if (!isNullOrEmpty(playlistUrl)) {
|
||||||
|
final String hlsStreamUrl;
|
||||||
|
if (isInstanceUsingRandomUuidsForHlsStreams) {
|
||||||
|
hlsStreamUrl = getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix,
|
||||||
|
extension, url);
|
||||||
|
} else {
|
||||||
|
hlsStreamUrl = playlistUrl.replace("master", JsonUtils.getNumber(
|
||||||
|
streamJsonObject, RESOLUTION_ID).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
final VideoStream videoStream = new VideoStream.Builder()
|
||||||
|
.setId(id + "-" + DeliveryMethod.HLS)
|
||||||
|
.setContent(hlsStreamUrl, true)
|
||||||
|
.setIsVideoOnly(false)
|
||||||
|
.setDeliveryMethod(DeliveryMethod.HLS)
|
||||||
|
.setResolution(resolution)
|
||||||
|
.setMediaFormat(format)
|
||||||
|
.setBaseUrl(playlistUrl)
|
||||||
|
.build();
|
||||||
|
if (!Stream.containSimilarStream(videoStream, videoStreams)) {
|
||||||
|
videoStreams.add(videoStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add finally torrent URLs
|
||||||
|
final String torrentUrl = JsonUtils.getString(streamJsonObject, "torrentUrl");
|
||||||
|
if (!isNullOrEmpty(torrentUrl)) {
|
||||||
|
videoStreams.add(new VideoStream.Builder()
|
||||||
|
.setId(id + "-" + idSuffix + "-" + DeliveryMethod.TORRENT)
|
||||||
|
.setContent(torrentUrl, true)
|
||||||
|
.setIsVideoOnly(false)
|
||||||
|
.setDeliveryMethod(DeliveryMethod.TORRENT)
|
||||||
|
.setResolution(resolution)
|
||||||
|
.setMediaFormat(format)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
@ -448,7 +673,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getHost() throws ParsingException {
|
public String getHost() throws ParsingException {
|
||||||
return JsonUtils.getString(json, "account.host");
|
return JsonUtils.getString(json, ACCOUNT_HOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package org.schabi.newpipe.extractor.services.soundcloud.extractors;
|
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.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL;
|
||||||
|
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.clientId;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
|
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
|
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
|
||||||
|
@ -26,6 +29,7 @@ import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||||
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
|
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.Description;
|
import org.schabi.newpipe.extractor.stream.Description;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
@ -58,13 +62,16 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
final String policy = track.getString("policy", EMPTY_STRING);
|
final String policy = track.getString("policy", EMPTY_STRING);
|
||||||
if (!policy.equals("ALLOW") && !policy.equals("MONETIZE")) {
|
if (!policy.equals("ALLOW") && !policy.equals("MONETIZE")) {
|
||||||
isAvailable = false;
|
isAvailable = false;
|
||||||
|
|
||||||
if (policy.equals("SNIP")) {
|
if (policy.equals("SNIP")) {
|
||||||
throw new SoundCloudGoPlusContentException();
|
throw new SoundCloudGoPlusContentException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (policy.equals("BLOCK")) {
|
if (policy.equals("BLOCK")) {
|
||||||
throw new GeographicRestrictionException(
|
throw new GeographicRestrictionException(
|
||||||
"This track is not available in user's country");
|
"This track is not available in user's country");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ContentNotAvailableException("Content not available: policy " + policy);
|
throw new ContentNotAvailableException("Content not available: policy " + policy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +79,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return track.getInt("id") + EMPTY_STRING;
|
return String.valueOf(track.getInt("id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
|
@ -162,17 +169,19 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
// Streams can be streamable and downloadable - or explicitly not.
|
// Streams can be streamable and downloadable - or explicitly not.
|
||||||
// For playing the track, it is only necessary to have a streamable track.
|
// For playing the track, it is only necessary to have a streamable track.
|
||||||
// If this is not the case, this track might not be published yet.
|
// If this is not the case, this track might not be published yet.
|
||||||
|
// If audio streams were calculated, return the calculated result
|
||||||
if (!track.getBoolean("streamable") || !isAvailable) {
|
if (!track.getBoolean("streamable") || !isAvailable) {
|
||||||
return audioStreams;
|
return audioStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final JsonArray transcodings = track.getObject("media").getArray("transcodings");
|
final JsonArray transcodings = track.getObject("media").getArray("transcodings");
|
||||||
if (transcodings != null) {
|
if (!isNullOrEmpty(transcodings)) {
|
||||||
// Get information about what stream formats are available
|
// Get information about what stream formats are available
|
||||||
extractAudioStreams(transcodings, checkMp3ProgressivePresence(transcodings),
|
extractAudioStreams(transcodings, checkMp3ProgressivePresence(transcodings),
|
||||||
audioStreams);
|
audioStreams);
|
||||||
}
|
}
|
||||||
|
extractDownloadableFileIfAvailable(audioStreams);
|
||||||
} catch (final NullPointerException e) {
|
} catch (final NullPointerException e) {
|
||||||
throw new ExtractionException("Could not get SoundCloud's tracks audio URL", e);
|
throw new ExtractionException("Could not get SoundCloud's tracks audio URL", e);
|
||||||
}
|
}
|
||||||
|
@ -180,7 +189,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
return audioStreams;
|
return audioStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean checkMp3ProgressivePresence(final JsonArray transcodings) {
|
private static boolean checkMp3ProgressivePresence(@Nonnull final JsonArray transcodings) {
|
||||||
boolean presence = false;
|
boolean presence = false;
|
||||||
for (final Object transcoding : transcodings) {
|
for (final Object transcoding : transcodings) {
|
||||||
final JsonObject transcodingJsonObject = (JsonObject) transcoding;
|
final JsonObject transcodingJsonObject = (JsonObject) transcoding;
|
||||||
|
@ -195,34 +204,53 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private static String getTranscodingUrl(final String endpointUrl,
|
private String getTranscodingUrl(final String endpointUrl,
|
||||||
final String protocol)
|
final String protocol)
|
||||||
throws IOException, ExtractionException {
|
throws IOException, ExtractionException {
|
||||||
final Downloader downloader = NewPipe.getDownloader();
|
final Downloader downloader = NewPipe.getDownloader();
|
||||||
final String apiStreamUrl = endpointUrl + "?client_id="
|
final String apiStreamUrl = endpointUrl + "?client_id="
|
||||||
+ SoundcloudParsingHelper.clientId();
|
+ clientId();
|
||||||
final String response = downloader.get(apiStreamUrl).responseBody();
|
final String response = downloader.get(apiStreamUrl).responseBody();
|
||||||
final JsonObject urlObject;
|
final JsonObject urlObject;
|
||||||
try {
|
try {
|
||||||
urlObject = JsonParser.object().from(response);
|
urlObject = JsonParser.object().from(response);
|
||||||
} catch (final JsonParserException e) {
|
} catch (final JsonParserException e) {
|
||||||
throw new ParsingException("Could not parse streamable url", e);
|
throw new ParsingException("Could not parse streamable URL", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String urlString = urlObject.getString("url");
|
final String urlString = urlObject.getString("url");
|
||||||
|
|
||||||
if (protocol.equals("progressive")) {
|
if (protocol.equals("progressive")) {
|
||||||
return urlString;
|
return urlString;
|
||||||
} else if (protocol.equals("hls")) {
|
} else if (protocol.equals("hls")) {
|
||||||
try {
|
|
||||||
return getSingleUrlFromHlsManifest(urlString);
|
return getSingleUrlFromHlsManifest(urlString);
|
||||||
} catch (final ParsingException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// else, unknown protocol
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void extractAudioStreams(final JsonArray transcodings,
|
// else, unknown protocol
|
||||||
|
return EMPTY_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String getDownloadUrl(@Nonnull final String trackId)
|
||||||
|
throws IOException, ExtractionException {
|
||||||
|
final Downloader dl = NewPipe.getDownloader();
|
||||||
|
final JsonObject downloadJsonObject;
|
||||||
|
|
||||||
|
final String response = dl.get(SOUNDCLOUD_API_V2_URL + "tracks/" + trackId
|
||||||
|
+ "/download" + "?client_id=" + clientId()).responseBody();
|
||||||
|
try {
|
||||||
|
downloadJsonObject = JsonParser.object().from(response);
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
throw new ParsingException("Could not parse download URL", e);
|
||||||
|
}
|
||||||
|
final String redirectUri = downloadJsonObject.getString("redirectUri");
|
||||||
|
if (!isNullOrEmpty(redirectUri)) {
|
||||||
|
return redirectUri;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extractAudioStreams(@Nonnull final JsonArray transcodings,
|
||||||
final boolean mp3ProgressiveInStreams,
|
final boolean mp3ProgressiveInStreams,
|
||||||
final List<AudioStream> audioStreams) {
|
final List<AudioStream> audioStreams) {
|
||||||
for (final Object transcoding : transcodings) {
|
for (final Object transcoding : transcodings) {
|
||||||
|
@ -231,12 +259,13 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
if (isNullOrEmpty(url)) {
|
if (isNullOrEmpty(url)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String mediaUrl;
|
final String mediaUrl;
|
||||||
final String preset = transcodingJsonObject.getString("preset");
|
final String preset = transcodingJsonObject.getString("preset", ID_UNKNOWN);
|
||||||
final String protocol = transcodingJsonObject.getObject("format")
|
final String protocol = transcodingJsonObject.getObject("format")
|
||||||
.getString("protocol");
|
.getString("protocol");
|
||||||
MediaFormat mediaFormat = null;
|
MediaFormat mediaFormat = null;
|
||||||
int bitrate = 0;
|
int averageBitrate = UNKNOWN_BITRATE;
|
||||||
if (preset.contains("mp3")) {
|
if (preset.contains("mp3")) {
|
||||||
// Don't add the MP3 HLS stream if there is a progressive stream present
|
// Don't add the MP3 HLS stream if there is a progressive stream present
|
||||||
// because the two have the same bitrate
|
// because the two have the same bitrate
|
||||||
|
@ -244,36 +273,75 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
mediaFormat = MediaFormat.MP3;
|
mediaFormat = MediaFormat.MP3;
|
||||||
bitrate = 128;
|
averageBitrate = 128;
|
||||||
} else if (preset.contains("opus")) {
|
} else if (preset.contains("opus")) {
|
||||||
mediaFormat = MediaFormat.OPUS;
|
mediaFormat = MediaFormat.OPUS;
|
||||||
bitrate = 64;
|
averageBitrate = 64;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaFormat != null) {
|
|
||||||
try {
|
try {
|
||||||
mediaUrl = getTranscodingUrl(url, protocol);
|
mediaUrl = getTranscodingUrl(url, protocol);
|
||||||
if (!mediaUrl.isEmpty()) {
|
if (!mediaUrl.isEmpty()) {
|
||||||
audioStreams.add(new AudioStream(mediaUrl, mediaFormat, bitrate));
|
final AudioStream audioStream = new AudioStream.Builder()
|
||||||
|
.setId(preset)
|
||||||
|
.setContent(mediaUrl, true)
|
||||||
|
.setMediaFormat(mediaFormat)
|
||||||
|
.setAverageBitrate(averageBitrate)
|
||||||
|
.build();
|
||||||
|
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
|
||||||
|
audioStreams.add(audioStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (final Exception ignored) {
|
} catch (final Exception ignored) {
|
||||||
// something went wrong when parsing this transcoding, don't add it to
|
// Something went wrong when parsing this transcoding, don't add it to the
|
||||||
// audioStreams
|
// audioStreams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the downloadable format if it is available.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A track can have the {@code downloadable} boolean set to {@code true}, but it doesn't mean
|
||||||
|
* we can download it: if the value of the {@code has_download_left} boolean is true, the track
|
||||||
|
* can be downloaded; otherwise not.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param audioStreams the audio streams to which add the downloadable file
|
||||||
|
*/
|
||||||
|
public void extractDownloadableFileIfAvailable(final List<AudioStream> audioStreams) {
|
||||||
|
if (track.getBoolean("downloadable") && track.getBoolean("has_downloads_left")) {
|
||||||
|
try {
|
||||||
|
final String downloadUrl = getDownloadUrl(getId());
|
||||||
|
if (!isNullOrEmpty(downloadUrl)) {
|
||||||
|
audioStreams.add(new AudioStream.Builder()
|
||||||
|
.setId("original-format")
|
||||||
|
.setContent(downloadUrl, true)
|
||||||
|
.setAverageBitrate(UNKNOWN_BITRATE)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
} catch (final Exception ignored) {
|
||||||
|
// If something went wrong when trying to get the download URL, ignore the
|
||||||
|
// exception throw because this "stream" is not necessary to play the track
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a SoundCloud HLS manifest to get a single URL of HLS streams.
|
* Parses a SoundCloud HLS manifest to get a single URL of HLS streams.
|
||||||
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* This method downloads the provided manifest URL, find all web occurrences in the manifest,
|
* This method downloads the provided manifest URL, find all web occurrences in the manifest,
|
||||||
* get the last segment URL, changes its segment range to {@code 0/track-length} and return
|
* get the last segment URL, changes its segment range to {@code 0/track-length} and return
|
||||||
* this string.
|
* this string.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
* @param hlsManifestUrl the URL of the manifest to be parsed
|
* @param hlsManifestUrl the URL of the manifest to be parsed
|
||||||
* @return a single URL that contains a range equal to the length of the track
|
* @return a single URL that contains a range equal to the length of the track
|
||||||
*/
|
*/
|
||||||
private static String getSingleUrlFromHlsManifest(final String hlsManifestUrl)
|
@Nonnull
|
||||||
|
private static String getSingleUrlFromHlsManifest(@Nonnull final String hlsManifestUrl)
|
||||||
throws ParsingException {
|
throws ParsingException {
|
||||||
final Downloader dl = NewPipe.getDownloader();
|
final Downloader dl = NewPipe.getDownloader();
|
||||||
final String hlsManifestResponse;
|
final String hlsManifestResponse;
|
||||||
|
@ -326,7 +394,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
|
|
||||||
final String apiUrl = SOUNDCLOUD_API_V2_URL + "tracks/" + urlEncode(getId())
|
final String apiUrl = SOUNDCLOUD_API_V2_URL + "tracks/" + urlEncode(getId())
|
||||||
+ "/related?client_id=" + urlEncode(SoundcloudParsingHelper.clientId());
|
+ "/related?client_id=" + urlEncode(clientId());
|
||||||
|
|
||||||
SoundcloudParsingHelper.getStreamsFromApi(collector, apiUrl);
|
SoundcloudParsingHelper.getStreamsFromApi(collector, apiUrl);
|
||||||
return collector;
|
return collector;
|
||||||
|
@ -355,19 +423,19 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
||||||
// Tags are separated by spaces, but they can be multiple words escaped by quotes "
|
// Tags are separated by spaces, but they can be multiple words escaped by quotes "
|
||||||
final String[] tagList = track.getString("tag_list").split(" ");
|
final String[] tagList = track.getString("tag_list").split(" ");
|
||||||
final List<String> tags = new ArrayList<>();
|
final List<String> tags = new ArrayList<>();
|
||||||
String escapedTag = "";
|
final StringBuilder escapedTag = new StringBuilder();
|
||||||
boolean isEscaped = false;
|
boolean isEscaped = false;
|
||||||
for (final String tag : tagList) {
|
for (final String tag : tagList) {
|
||||||
if (tag.startsWith("\"")) {
|
if (tag.startsWith("\"")) {
|
||||||
escapedTag += tag.replace("\"", "");
|
escapedTag.append(tag.replace("\"", ""));
|
||||||
isEscaped = true;
|
isEscaped = true;
|
||||||
} else if (isEscaped) {
|
} else if (isEscaped) {
|
||||||
if (tag.endsWith("\"")) {
|
if (tag.endsWith("\"")) {
|
||||||
escapedTag += " " + tag.replace("\"", "");
|
escapedTag.append(" ").append(tag.replace("\"", ""));
|
||||||
isEscaped = false;
|
isEscaped = false;
|
||||||
tags.add(escapedTag);
|
tags.add(escapedTag.toString());
|
||||||
} else {
|
} else {
|
||||||
escapedTag += " " + tag;
|
escapedTag.append(" ").append(tag);
|
||||||
}
|
}
|
||||||
} else if (!tag.isEmpty()) {
|
} else if (!tag.isEmpty()) {
|
||||||
tags.add(tag);
|
tags.add(tag);
|
||||||
|
|
Loading…
Reference in New Issue