NewPipeExtractor/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java

1385 lines
58 KiB
Java
Raw Normal View History

2018-05-08 21:19:03 +02:00
package org.schabi.newpipe.extractor.services.youtube.extractors;
2017-03-01 18:47:52 +01:00
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonWriter;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
2017-03-01 18:47:52 +01:00
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ScriptableObject;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager;
2018-05-08 21:19:03 +02:00
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptExtractor;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.services.youtube.YoutubeThrottlingDecrypter;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
2021-02-07 22:42:21 +01:00
import org.schabi.newpipe.extractor.stream.*;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.Utils;
2017-03-01 18:47:52 +01:00
2020-10-26 16:32:39 +01:00
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
2017-03-01 18:47:52 +01:00
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
2021-02-07 22:42:21 +01:00
import java.util.*;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
2020-02-27 17:39:23 +01:00
/*
2017-03-01 18:47:52 +01:00
* Created by Christian Schabesberger on 06.08.15.
*
* Copyright (C) Christian Schabesberger 2019 <chris.schabesberger@mailbox.org>
2017-03-01 18:47:52 +01:00
* YoutubeStreamExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class YoutubeStreamExtractor extends StreamExtractor {
/*//////////////////////////////////////////////////////////////////////////
// Exceptions
//////////////////////////////////////////////////////////////////////////*/
2017-03-01 18:47:52 +01:00
public static class DeobfuscateException extends ParsingException {
DeobfuscateException(final String message, final Throwable cause) {
2017-03-01 18:47:52 +01:00
super(message, cause);
}
}
/*//////////////////////////////////////////////////////////////////////////*/
2017-03-01 18:47:52 +01:00
2021-02-07 22:42:21 +01:00
@Nullable
private static String cachedDeobfuscationCode = null;
@Nullable
private String playerJsUrl = null;
@Nullable
private String sts = null;
@Nullable
private String playerCode = null;
2021-02-07 22:42:21 +01:00
@Nonnull
private final Map<String, String> videoInfoPage = new HashMap<>();
private JsonArray initialAjaxJson;
private JsonObject initialData;
private JsonObject playerResponse;
private JsonObject nextResponse;
@Nullable
private JsonObject streamingData;
private JsonObject videoPrimaryInfoRenderer;
private JsonObject videoSecondaryInfoRenderer;
private int ageLimit = -1;
2021-02-07 22:42:21 +01:00
@Nullable
private List<SubtitlesStream> subtitles = null;
public YoutubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler) {
super(service, linkHandler);
2017-03-01 18:47:52 +01:00
}
/*//////////////////////////////////////////////////////////////////////////
// Impl
//////////////////////////////////////////////////////////////////////////*/
2017-03-01 18:47:52 +01:00
@Nonnull
2017-03-01 18:47:52 +01:00
@Override
2017-08-11 03:23:09 +02:00
public String getName() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2020-07-18 09:50:22 +02:00
String title = null;
try {
title = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("title"));
} catch (final ParsingException ignored) {
2020-07-26 14:15:13 +02:00
// age-restricted videos cause a ParsingException here
}
2020-02-29 17:18:50 +01:00
if (isNullOrEmpty(title)) {
2020-04-16 16:08:14 +02:00
title = playerResponse.getObject("videoDetails").getString("title");
2020-02-29 17:18:50 +01:00
if (isNullOrEmpty(title)) throw new ParsingException("Could not get name");
}
2020-02-29 17:18:50 +01:00
return title;
2017-03-01 18:47:52 +01:00
}
@Nullable
2017-03-01 18:47:52 +01:00
@Override
public String getTextualUploadDate() throws ParsingException {
final JsonObject micro = playerResponse.getObject("microformat")
.getObject("playerMicroformatRenderer");
if (!micro.getString("uploadDate", EMPTY_STRING).isEmpty()) {
2020-04-16 16:08:14 +02:00
return micro.getString("uploadDate");
} else if (!micro.getString("publishDate", EMPTY_STRING).isEmpty()) {
2020-04-16 16:08:14 +02:00
return micro.getString("publishDate");
} else {
final JsonObject liveDetails = micro.getObject("liveBroadcastDetails");
if (!liveDetails.getString("endTimestamp", EMPTY_STRING).isEmpty()) {
// an ended live stream
return liveDetails.getString("endTimestamp");
} else if (!liveDetails.getString("startTimestamp", EMPTY_STRING).isEmpty()) {
// a running live stream
return liveDetails.getString("startTimestamp");
} else if (getStreamType() == StreamType.LIVE_STREAM) {
// this should never be reached, but a live stream without upload date is valid
return null;
}
2020-04-16 16:08:14 +02:00
}
if (getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText"))
.startsWith("Premiered")) {
String time = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText"))
.substring(10);
2020-04-16 16:08:14 +02:00
try { // Premiered 20 hours ago
final TimeAgoParser timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(
Localization.fromLocalizationCode("en"));
final OffsetDateTime parsedTime = timeAgoParser.parse(time).offsetDateTime();
return DateTimeFormatter.ISO_LOCAL_DATE.format(parsedTime);
} catch (final Exception ignored) {
2020-10-26 16:32:39 +01:00
}
2020-04-16 16:08:14 +02:00
try { // Premiered Feb 21, 2020
final LocalDate localDate = LocalDate.parse(time,
DateTimeFormatter.ofPattern("MMM dd, yyyy", Locale.ENGLISH));
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
} catch (final Exception ignored) {
2020-10-26 16:32:39 +01:00
}
2020-04-16 16:08:14 +02:00
}
2017-03-01 18:47:52 +01:00
try {
// TODO: this parses English formatted dates only, we need a better approach to parse
// the textual date
final LocalDate localDate = LocalDate.parse(getTextFromObject(
getVideoPrimaryInfoRenderer().getObject("dateText")),
DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH));
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
} catch (final Exception ignored) {
2020-10-26 16:32:39 +01:00
}
throw new ParsingException("Could not get upload date");
2017-03-01 18:47:52 +01:00
}
@Override
public DateWrapper getUploadDate() throws ParsingException {
final String textualUploadDate = getTextualUploadDate();
if (isNullOrEmpty(textualUploadDate)) {
return null;
}
return new DateWrapper(YoutubeParsingHelper.parseDateFrom(textualUploadDate), true);
}
@Nonnull
2017-03-01 18:47:52 +01:00
@Override
2017-08-11 03:23:09 +02:00
public String getThumbnailUrl() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-03-01 18:47:52 +01:00
try {
JsonArray thumbnails = playerResponse.getObject("videoDetails").getObject("thumbnail")
.getArray("thumbnails");
// the last thumbnail is the one with the highest resolution
2020-02-27 19:08:46 +01:00
String url = thumbnails.getObject(thumbnails.size() - 1).getString("url");
return fixThumbnailUrl(url);
} catch (final Exception e) {
throw new ParsingException("Could not get thumbnail url");
2017-08-11 03:23:09 +02:00
}
2017-08-11 03:23:09 +02:00
}
@Nonnull
2017-08-11 03:23:09 +02:00
@Override
2021-02-12 22:22:11 +01:00
public Description getDescription() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
// Description with more info on links
2020-07-18 09:50:22 +02:00
try {
String description = getTextFromObject(getVideoSecondaryInfoRenderer()
.getObject("description"), true);
if (!isNullOrEmpty(description)) return new Description(description, Description.HTML);
} catch (final ParsingException ignored) {
// Age-restricted videos cause a ParsingException here
2020-07-26 14:15:13 +02:00
}
2020-02-25 18:27:39 +01:00
String description = playerResponse.getObject("videoDetails")
.getString("shortDescription");
2021-02-12 22:22:11 +01:00
if (description == null) {
final JsonObject descriptionObject = playerResponse.getObject("microformat")
.getObject("playerMicroformatRenderer").getObject("description");
description = getTextFromObject(descriptionObject);
}
// Raw non-html description
2021-02-12 22:22:11 +01:00
return new Description(description, Description.PLAIN_TEXT);
2017-08-11 03:23:09 +02:00
}
@Override
public int getAgeLimit() throws ParsingException {
if (ageLimit == -1) {
ageLimit = NO_AGE_LIMIT;
2020-02-28 17:14:26 +01:00
final JsonArray metadataRows = getVideoSecondaryInfoRenderer()
.getObject("metadataRowContainer").getObject("metadataRowContainerRenderer")
.getArray("rows");
for (final Object metadataRow : metadataRows) {
final JsonArray contents = ((JsonObject) metadataRow)
.getObject("metadataRowRenderer").getArray("contents");
for (final Object content : contents) {
final JsonArray runs = ((JsonObject) content).getArray("runs");
for (final Object run : runs) {
final String rowText = ((JsonObject) run).getString("text", EMPTY_STRING);
if (rowText.contains("Age-restricted")) {
ageLimit = 18;
return ageLimit;
}
}
}
}
}
2020-02-28 17:14:26 +01:00
return ageLimit;
2017-03-01 18:47:52 +01:00
}
@Override
public long getLength() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
try {
final String duration = playerResponse
.getObject("videoDetails")
.getString("lengthSeconds");
return Long.parseLong(duration);
} catch (final Exception e) {
try {
final JsonArray formats = streamingData.getArray("formats");
final String durationMs = formats.getObject(formats.size() - 1)
.getString("approxDurationMs");
return Math.round(Long.parseLong(durationMs) / 1000f);
} catch (final Exception ignored) {
throw new ParsingException("Could not get duration", e);
}
2017-03-01 18:47:52 +01:00
}
}
2017-08-11 03:23:09 +02:00
/**
* Attempts to parse (and return) the offset to start playing the video from.
*
* @return the offset (in seconds), or 0 if no timestamp is found.
*/
@Override
public long getTimeStamp() throws ParsingException {
2020-04-11 17:18:17 +02:00
final long timestamp =
getTimestampSeconds("((#|&|\\?)t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)");
if (timestamp == -2) {
// Regex for timestamp was not found
return 0;
} else {
return timestamp;
}
2017-08-11 03:23:09 +02:00
}
2017-03-01 18:47:52 +01:00
@Override
public long getViewCount() throws ParsingException {
String views = null;
try {
views = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("viewCount")
.getObject("videoViewCountRenderer").getObject("viewCount"));
} catch (final ParsingException ignored) {
// Age-restricted videos cause a ParsingException here
}
if (isNullOrEmpty(views)) {
views = playerResponse.getObject("videoDetails").getString("viewCount");
if (isNullOrEmpty(views)) throw new ParsingException("Could not get view count");
}
if (views.toLowerCase().contains("no views")) return 0;
2020-03-17 15:00:07 +01:00
2020-02-29 17:18:50 +01:00
return Long.parseLong(Utils.removeNonDigitCharacters(views));
2020-02-24 16:04:01 +01:00
}
2017-03-01 18:47:52 +01:00
@Override
2017-08-11 03:23:09 +02:00
public long getLikeCount() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-08-11 03:23:09 +02:00
String likesString = "";
2017-03-01 18:47:52 +01:00
try {
2017-08-11 03:23:09 +02:00
try {
2020-02-24 16:04:01 +01:00
likesString = getVideoPrimaryInfoRenderer().getObject("sentimentBar")
.getObject("sentimentBarRenderer").getString("tooltip").split("/")[0];
} catch (final NullPointerException e) {
// If this kicks in our button has no content and therefore ratings must be disabled
if (playerResponse.getObject("videoDetails").getBoolean("allowRatings")) {
throw new ParsingException(
"Ratings are enabled even though the like button is missing", e);
}
2017-08-11 03:23:09 +02:00
return -1;
}
return Integer.parseInt(Utils.removeNonDigitCharacters(likesString));
} catch (final NumberFormatException nfe) {
throw new ParsingException("Could not parse \"" + likesString + "\" as an Integer",
nfe);
} catch (final Exception e) {
if (getAgeLimit() == NO_AGE_LIMIT) {
throw new ParsingException("Could not get like count", e);
}
return -1;
2017-03-01 18:47:52 +01:00
}
}
@Override
2017-08-11 03:23:09 +02:00
public long getDislikeCount() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-08-11 03:23:09 +02:00
String dislikesString = "";
2017-03-01 18:47:52 +01:00
try {
2017-08-11 03:23:09 +02:00
try {
2020-02-24 16:04:01 +01:00
dislikesString = getVideoPrimaryInfoRenderer().getObject("sentimentBar")
.getObject("sentimentBarRenderer").getString("tooltip").split("/")[1];
} catch (final NullPointerException e) {
// If this kicks in our button has no content and therefore ratings must be disabled
if (playerResponse.getObject("videoDetails").getBoolean("allowRatings")) {
throw new ParsingException(
"Ratings are enabled even though the dislike button is missing", e);
}
2017-08-11 03:23:09 +02:00
return -1;
}
return Integer.parseInt(Utils.removeNonDigitCharacters(dislikesString));
} catch (final NumberFormatException nfe) {
throw new ParsingException("Could not parse \"" + dislikesString + "\" as an Integer",
nfe);
} catch (final Exception e) {
if (getAgeLimit() == NO_AGE_LIMIT) {
throw new ParsingException("Could not get dislike count", e);
}
return -1;
2017-08-11 03:23:09 +02:00
}
}
@Nonnull
2017-08-11 03:23:09 +02:00
@Override
public String getUploaderUrl() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2020-04-16 16:08:14 +02:00
// Don't use the id in the videoSecondaryRenderer object to get real id of the uploader
// The difference between the real id of the channel and the displayed id is especially
// visible for music channels and autogenerated channels.
final String uploaderId = playerResponse.getObject("videoDetails").getString("channelId");
2020-07-18 09:50:22 +02:00
if (!isNullOrEmpty(uploaderId)) {
2020-04-16 16:08:14 +02:00
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("channel/" + uploaderId);
2020-07-18 09:50:22 +02:00
}
2020-04-16 16:08:14 +02:00
throw new ParsingException("Could not get uploader url");
}
@Nonnull
2017-08-11 03:23:09 +02:00
@Override
public String getUploaderName() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2020-07-18 09:50:22 +02:00
// Don't use the name in the videoSecondaryRenderer object to get real name of the uploader
// The difference between the real name of the channel and the displayed name is especially
// visible for music channels and autogenerated channels.
final String uploaderName = playerResponse.getObject("videoDetails").getString("author");
if (isNullOrEmpty(uploaderName)) throw new ParsingException("Could not get uploader name");
2020-02-29 17:18:50 +01:00
return uploaderName;
2017-03-01 18:47:52 +01:00
}
@Override
public boolean isUploaderVerified() throws ParsingException {
final JsonArray badges = getVideoSecondaryInfoRenderer().getObject("owner")
.getObject("videoOwnerRenderer").getArray("badges");
return YoutubeParsingHelper.isVerified(badges);
}
@Nonnull
2017-03-01 18:47:52 +01:00
@Override
2017-08-08 23:36:11 +02:00
public String getUploaderAvatarUrl() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2020-07-18 09:50:22 +02:00
String url = null;
try {
url = getVideoSecondaryInfoRenderer().getObject("owner")
.getObject("videoOwnerRenderer").getObject("thumbnail")
.getArray("thumbnails").getObject(0).getString("url");
} catch (final ParsingException ignored) {
// Age-restricted videos cause a ParsingException here
2020-07-26 14:15:13 +02:00
}
2020-02-27 17:39:23 +01:00
2020-07-18 09:50:22 +02:00
if (isNullOrEmpty(url)) {
if (ageLimit == NO_AGE_LIMIT) {
throw new ParsingException("Could not get uploader avatar URL");
}
return "";
2017-03-01 18:47:52 +01:00
}
2020-07-18 09:50:22 +02:00
return fixThumbnailUrl(url);
2017-03-01 18:47:52 +01:00
}
@Nonnull
@Override
public String getSubChannelUrl() {
return "";
}
@Nonnull
@Override
public String getSubChannelName() {
return "";
}
@Nonnull
@Override
public String getSubChannelAvatarUrl() {
return "";
}
@Nonnull
2017-03-01 18:47:52 +01:00
@Override
public String getDashMpdUrl() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-03-01 18:47:52 +01:00
try {
String dashManifestUrl;
if (streamingData.isString("dashManifestUrl")) {
return streamingData.getString("dashManifestUrl");
} else if (videoInfoPage.containsKey("dashmpd")) {
2017-03-01 18:47:52 +01:00
dashManifestUrl = videoInfoPage.get("dashmpd");
} else {
return "";
}
if (!dashManifestUrl.contains("/signature/")) {
String obfuscatedSig = Parser.matchGroup1("/s/([a-fA-F0-9\\.]+)",
dashManifestUrl);
final String deobfuscatedSig;
2017-03-01 18:47:52 +01:00
deobfuscatedSig = deobfuscateSignature(obfuscatedSig);
dashManifestUrl = dashManifestUrl.replace("/s/" + obfuscatedSig,
"/signature/" + deobfuscatedSig);
2017-03-01 18:47:52 +01:00
}
2017-03-01 18:47:52 +01:00
return dashManifestUrl;
} catch (final Exception e) {
throw new ParsingException("Could not get dash manifest url", e);
2017-03-01 18:47:52 +01:00
}
}
@Nonnull
@Override
public String getHlsUrl() throws ParsingException {
assertPageFetched();
2019-01-19 13:50:02 +01:00
try {
return streamingData.getString("hlsManifestUrl");
} catch (final Exception e) {
throw new ParsingException("Could not get hls manifest url", e);
}
}
2017-03-01 18:47:52 +01:00
@Override
public List<AudioStream> getAudioStreams() throws ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
final List<AudioStream> audioStreams = new ArrayList<>();
final YoutubeThrottlingDecrypter throttlingDecrypter = new YoutubeThrottlingDecrypter(getId());
2017-03-01 18:47:52 +01:00
try {
for (final Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS,
ItagItem.ItagType.AUDIO).entrySet()) {
final ItagItem itag = entry.getValue();
String url = entry.getKey();
url = throttlingDecrypter.apply(url);
final AudioStream audioStream = new AudioStream(url, itag);
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
audioStreams.add(audioStream);
2017-03-01 18:47:52 +01:00
}
}
} catch (final Exception e) {
throw new ParsingException("Could not get audio streams", e);
2017-03-01 18:47:52 +01:00
}
2017-03-01 18:47:52 +01:00
return audioStreams;
}
@Override
public List<VideoStream> getVideoStreams() throws ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
final List<VideoStream> videoStreams = new ArrayList<>();
final YoutubeThrottlingDecrypter throttlingDecrypter = new YoutubeThrottlingDecrypter(getId());
try {
for (final Map.Entry<String, ItagItem> entry : getItags(FORMATS,
ItagItem.ItagType.VIDEO).entrySet()) {
final ItagItem itag = entry.getValue();
String url = entry.getKey();
url = throttlingDecrypter.apply(url);
final VideoStream videoStream = new VideoStream(url, false, itag);
if (!Stream.containSimilarStream(videoStream, videoStreams)) {
videoStreams.add(videoStream);
2017-03-01 18:47:52 +01:00
}
}
} catch (final Exception e) {
throw new ParsingException("Could not get video streams", e);
2017-03-01 18:47:52 +01:00
}
return videoStreams;
}
@Override
public List<VideoStream> getVideoOnlyStreams() throws ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
final List<VideoStream> videoOnlyStreams = new ArrayList<>();
final YoutubeThrottlingDecrypter throttlingDecrypter = new YoutubeThrottlingDecrypter(getId());
2017-04-12 02:55:53 +02:00
try {
for (final Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS,
ItagItem.ItagType.VIDEO_ONLY).entrySet()) {
final ItagItem itag = entry.getValue();
String url = entry.getKey();
url = throttlingDecrypter.apply(url);
2017-04-12 02:55:53 +02:00
final VideoStream videoStream = new VideoStream(url, true, itag);
if (!Stream.containSimilarStream(videoStream, videoOnlyStreams)) {
videoOnlyStreams.add(videoStream);
2017-04-12 02:55:53 +02:00
}
}
} catch (final Exception e) {
throw new ParsingException("Could not get video only streams", e);
2017-04-12 02:55:53 +02:00
}
return videoOnlyStreams;
2017-03-01 18:47:52 +01:00
}
@Override
@Nonnull
public List<SubtitlesStream> getSubtitlesDefault() throws ParsingException {
return getSubtitles(MediaFormat.TTML);
}
@Override
@Nonnull
public List<SubtitlesStream> getSubtitles(final MediaFormat format) throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
// If the video is age restricted getSubtitles will fail
if (getAgeLimit() != NO_AGE_LIMIT) {
return Collections.emptyList();
}
if (subtitles != null) {
// Already calculated
return subtitles;
}
final JsonObject renderer = playerResponse.getObject("captions")
.getObject("playerCaptionsTracklistRenderer");
final JsonArray captionsArray = renderer.getArray("captionTracks");
// TODO: use this to apply auto translation to different language from a source language
// final JsonArray autoCaptionsArray = renderer.getArray("translationLanguages");
subtitles = new ArrayList<>();
for (int i = 0; i < captionsArray.size(); i++) {
final String languageCode = captionsArray.getObject(i).getString("languageCode");
final String baseUrl = captionsArray.getObject(i).getString("baseUrl");
final String vssId = captionsArray.getObject(i).getString("vssId");
if (languageCode != null && baseUrl != null && vssId != null) {
final boolean isAutoGenerated = vssId.startsWith("a.");
final String cleanUrl = baseUrl
.replaceAll("&fmt=[^&]*", "") // Remove preexisting format if exists
.replaceAll("&tlang=[^&]*", ""); // Remove translation language
subtitles.add(new SubtitlesStream(format, languageCode,
cleanUrl + "&fmt=" + format.getSuffix(), isAutoGenerated));
}
}
return subtitles;
}
2017-03-01 18:47:52 +01:00
@Override
public StreamType getStreamType() {
assertPageFetched();
return streamingData.has(FORMATS) ? StreamType.VIDEO_STREAM : StreamType.LIVE_STREAM;
2017-03-01 18:47:52 +01:00
}
@Nullable
2017-03-01 18:47:52 +01:00
@Override
public StreamInfoItemsCollector getRelatedItems() throws ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
if (getAgeLimit() != NO_AGE_LIMIT) {
return null;
}
2017-03-01 18:47:52 +01:00
try {
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(
getServiceId());
final JsonArray results = nextResponse.getObject("contents")
.getObject("twoColumnWatchNextResults").getObject("secondaryResults")
.getObject("secondaryResults").getArray("results");
final TimeAgoParser timeAgoParser = getTimeAgoParser();
for (final Object ul : results) {
2020-04-16 16:08:14 +02:00
if (((JsonObject) ul).has("compactVideoRenderer")) {
collector.commit(new YoutubeStreamInfoItemExtractor(((JsonObject) ul)
.getObject("compactVideoRenderer"), timeAgoParser));
2020-04-16 16:08:14 +02:00
}
2017-03-01 18:47:52 +01:00
}
return collector;
} catch (final Exception e) {
2017-03-01 18:47:52 +01:00
throw new ParsingException("Could not get related videos", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public String getErrorMessage() {
try {
return getTextFromObject(playerResponse.getObject("playabilityStatus")
.getObject("errorScreen").getObject("playerErrorMessageRenderer")
.getObject("reason"));
} catch (final ParsingException | NullPointerException e) {
return null; // No error message
}
}
2017-03-01 18:47:52 +01:00
/*//////////////////////////////////////////////////////////////////////////
// Fetch page
//////////////////////////////////////////////////////////////////////////*/
2017-03-01 18:47:52 +01:00
private static final String FORMATS = "formats";
private static final String ADAPTIVE_FORMATS = "adaptiveFormats";
private static final String HTTPS = "https:";
private static final String DEOBFUSCATION_FUNC_NAME = "deobfuscate";
private static final String[] REGEXES = {
"(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*\"\"\\s*\\)",
"([\\w$]+)\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;",
"\\b([\\w$]{2})\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(\"\"\\)\\s*;",
"\\bc\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\("
};
private static final String STS_REGEX = "signatureTimestamp[=:](\\d+)";
2017-08-06 22:20:15 +02:00
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
final String videoId = getId();
final Localization localization = getExtractorLocalization();
final ContentCountry contentCountry = getExtractorContentCountry();
final byte[] body = JsonWriter.string(prepareJsonBuilder(localization, contentCountry)
.value("videoId", videoId)
.done())
.getBytes(UTF_8);
2020-02-28 15:17:47 +01:00
// This boolean is needed if we don't want to fetch again the JSON player if the sts string
// is not null.
boolean stsKnown = false;
// Put the sts string if we already know it so we don't have to fetch again the player
// endpoint of the desktop internal API if something went wrong when parsing the Android
// API.
if (sts != null) {
playerResponse = getJsonPostResponse("player", createPlayerBodyWithSts(localization,
contentCountry, videoId), localization);
stsKnown = true;
} else {
playerResponse = getJsonPostResponse("player", body, localization);
}
// Save the playerResponse from the player endpoint of the desktop internal API because
// there can be restrictions on the embedded player.
// E.g. if a video is age-restricted, the embedded player's playabilityStatus says that
// the video cannot be played outside of YouTube, but does not show the original message.
JsonObject youtubePlayerResponse = playerResponse;
2021-02-24 12:06:19 +01:00
if (playerResponse == null || !playerResponse.has("streamingData")) {
// Try to get the player response by fetching video info page
fetchVideoInfoPage();
}
if (playerResponse == null && youtubePlayerResponse == null) {
throw new ExtractionException("Could not get playerResponse");
} else if (youtubePlayerResponse == null) {
youtubePlayerResponse = playerResponse;
}
final JsonObject playabilityStatus = (playerResponse == null ? youtubePlayerResponse
: playerResponse).getObject("playabilityStatus");
checkPlayabilityStatus(youtubePlayerResponse, playabilityStatus);
nextResponse = getJsonPostResponse("next", body, localization);
streamingData = playerResponse.getObject("streamingData");
if (hasOtfStreams() || isCipherProtectedContent()) {
fetchAndroidMobileJsonPlayer(contentCountry, localization, videoId, stsKnown);
}
}
private void checkPlayabilityStatus(final JsonObject youtubePlayerResponse,
JsonObject playabilityStatus) throws ParsingException {
2021-02-24 12:06:19 +01:00
String status = playabilityStatus.getString("status");
// If status exist, and is not "OK", throw the specific exception based on error message
// or a ContentNotAvailableException with the reason text if it's an unknown reason.
if (status != null && !status.equalsIgnoreCase("ok")) {
2021-02-24 12:06:19 +01:00
playabilityStatus = youtubePlayerResponse.getObject("playabilityStatus");
status = playabilityStatus.getString("status");
final String reason = playabilityStatus.getString("reason");
if (status.equalsIgnoreCase("login_required")) {
if (reason == null) {
final String message = playabilityStatus.getArray("messages").getString(0);
if (message != null && message.equals(
"This is a private video. Please sign in to verify that you may see it.")) {
throw new PrivateContentException("This video is private.");
}
2021-02-24 12:06:19 +01:00
} else if (reason.equals("Sign in to confirm your age")) {
// No streams can be fetched, therefore throw an AgeRestrictedContentException
// explicitly.
throw new AgeRestrictedContentException(
"This age-restricted video cannot be watched.");
}
}
if (status.equalsIgnoreCase("unplayable")) {
if (reason != null) {
if (reason.equals("This video is only available to Music Premium members")) {
throw new YoutubeMusicPremiumContentException();
}
if (reason.equals("This video requires payment to watch.")) {
throw new PaidContentException("This video is a paid video");
}
if (reason.equals("Join this channel to get access to members-only content like this video, and other exclusive perks.")
|| reason.equals("Join this channel to get access to members-only content like this video and other exclusive perks.")) {
throw new PaidContentException("This video is only available for members of the channel of this video");
}
if (reason.equals("Video unavailable")) {
final String detailedErrorMessage = getTextFromObject(playabilityStatus
.getObject("errorScreen").getObject("playerErrorMessageRenderer")
.getObject("subreason"));
if (detailedErrorMessage != null) {
if (detailedErrorMessage.equals(
"The uploader has not made this video available in your country.")) {
throw new GeographicRestrictionException(
"This video is not available in user's country.");
}
}
}
}
}
2021-02-21 17:43:15 +01:00
throw new ContentNotAvailableException("Got error: \"" + reason + "\"");
}
}
private void fetchAndroidMobileJsonPlayer(final ContentCountry contentCountry,
final Localization localization,
final String videoId,
final boolean stsKnown) throws ExtractionException,
IOException {
JsonObject mobilePlayerResponse = null;
final byte[] mobileBody = JsonWriter.string(prepareMobileJsonBuilder(localization,
contentCountry)
.value("videoId", videoId)
.done())
.getBytes(UTF_8);
try {
mobilePlayerResponse = getJsonMobilePostResponse("player", mobileBody,
contentCountry, localization);
} catch (final IOException | ExtractionException ignored) {
}
if (mobilePlayerResponse != null && mobilePlayerResponse.has("streamingData")) {
final JsonObject mobileStreamingData = mobilePlayerResponse.getObject(
"streamingData");
if (!isNullOrEmpty(mobileStreamingData)) streamingData = mobileStreamingData;
} else {
// Fallback to the desktop JSON player endpoint
// The cipher signatures from the player endpoint without a timestamp are invalid so
// download it again only if we didn't have a signatureTimestamp before fetching the
// data of this video (the sts string).
if (!stsKnown && isCipherProtectedContent()) {
sts = getStsFromPlayerJs();
final JsonObject playerResponseWithSignatureTimestamp = getJsonPostResponse(
"player", createPlayerBodyWithSts(localization, contentCountry, videoId),
localization);
if (playerResponseWithSignatureTimestamp.has("streamingData")) {
streamingData = playerResponseWithSignatureTimestamp.getObject(
"streamingData");
}
}
}
}
private void fetchVideoInfoPage() throws ParsingException, ReCaptchaException, IOException {
if (sts == null) {
sts = getStsFromPlayerJs();
}
final String videoInfoUrl = getVideoInfoUrl(getId(), sts);
final String infoPageResponse = NewPipe.getDownloader()
.get(videoInfoUrl, getExtractorLocalization()).responseBody();
videoInfoPage.putAll(Parser.compatParseMap(infoPageResponse));
2017-03-01 18:47:52 +01:00
2019-09-11 19:05:41 +02:00
try {
playerResponse = JsonParser.object().from(videoInfoPage.get("player_response"));
} catch (final JsonParserException e) {
throw new ParsingException(
"Could not parse YouTube player response from video info page", e);
2019-09-11 19:05:41 +02:00
}
}
private byte[] createPlayerBodyWithSts(final Localization localization,
final ContentCountry contentCountry,
final String videoId) throws ExtractionException,
IOException {
// @formatter:off
return JsonWriter.string(prepareJsonBuilder(localization,
contentCountry)
.value("videoId", videoId)
.object("playbackContext")
.object("contentPlaybackContext")
.value("signatureTimestamp", sts)
.end()
.end()
.done())
.getBytes(UTF_8);
// @formatter:on
}
private void storePlayerJs() throws ParsingException {
try {
// The JavaScript player was not found in any page fetched so far and there is
// nothing cached, so try fetching embedded info.
// Don't provide a video id to get a smaller response (around 9kb instead of 21 kb
// with a video)
final String embedUrl = "https://www.youtube.com/embed/";
final String embedPageContent = NewPipe.getDownloader()
.get(embedUrl, getExtractorLocalization()).responseBody();
try {
final String assetsPattern = "\"assets\":.+?\"js\":\\s*(\"[^\"]+\")";
playerJsUrl = Parser.matchGroup1(assetsPattern, embedPageContent)
.replace("\\", "").replace("\"", "");
} catch (final Parser.RegexException ex) {
// playerJsUrl is still available in the file, just somewhere else TODO
// It is ok not to find it, see how that's handled in getDeobfuscationCode()
final Document doc = Jsoup.parse(embedPageContent);
final Elements elems = doc.select("script").attr("name", "player_ias/base");
for (final Element elem : elems) {
if (elem.attr("src").contains("base.js")) {
playerJsUrl = elem.attr("src");
break;
}
}
}
if (playerJsUrl != null) {
if (playerJsUrl.startsWith("//")) {
playerJsUrl = HTTPS + playerJsUrl;
} else if (playerJsUrl.startsWith("/")) {
// Sometimes https://www.youtube.com part has to be added manually
playerJsUrl = HTTPS + "//www.youtube.com" + playerJsUrl;
}
playerCode = NewPipe.getDownloader().get(playerJsUrl, getExtractorLocalization())
.responseBody();
} else {
throw new ExtractionException("Could not extract JS player URL");
}
} catch (final Exception e) {
throw new ParsingException("Could not store JavaScript player", e);
}
}
private boolean hasOtfStreams() {
if (streamingData != null) {
boolean hasOtfStreamsValue = false;
if (streamingData.has("adaptiveFormats")) {
final JsonArray adaptiveFormats = streamingData.getArray("adaptiveFormats");
for (final Object adaptiveFormat : adaptiveFormats) {
final JsonObject jsonAdaptiveFormat = (JsonObject) adaptiveFormat;
if (jsonAdaptiveFormat.has("type")) {
final String streamTypeFormat = jsonAdaptiveFormat.getString("type",
EMPTY_STRING);
if (streamTypeFormat.equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF")) {
hasOtfStreamsValue = true;
break;
}
}
}
}
return hasOtfStreamsValue;
}
return false;
}
private boolean isCipherProtectedContent() {
if (streamingData != null) {
if (streamingData.has("adaptiveFormats")) {
final JsonArray adaptiveFormats = streamingData.getArray("adaptiveFormats");
if (!isNullOrEmpty(adaptiveFormats)) {
final JsonObject firstAdaptiveFormat = adaptiveFormats.getObject(0);
return firstAdaptiveFormat.has("cipher") || firstAdaptiveFormat.has("signatureCipher");
}
} else if (streamingData.has("formats")) {
final JsonArray formats = streamingData.getArray("formats");
if (!isNullOrEmpty(formats)) {
final JsonObject firstFormat = formats.getObject(0);
return firstFormat.has("cipher") || firstFormat.has("signatureCipher");
}
}
}
return false;
}
private String getDeobfuscationFuncName(final String playerCode) throws DeobfuscateException {
Parser.RegexException exception = null;
for (final String regex : REGEXES) {
try {
return Parser.matchGroup1(regex, playerCode);
} catch (final Parser.RegexException re) {
if (exception == null) {
exception = re;
}
}
}
throw new DeobfuscateException(
"Could not find deobfuscate function with any of the given patterns.", exception);
}
2017-03-01 18:47:52 +01:00
private String loadDeobfuscationCode() throws DeobfuscateException {
2017-03-01 18:47:52 +01:00
try {
2020-10-26 16:32:39 +01:00
final String deobfuscationFunctionName = getDeobfuscationFuncName(playerCode);
2017-03-01 18:47:52 +01:00
final String functionPattern = "("
2020-10-26 16:32:39 +01:00
+ deobfuscationFunctionName.replace("$", "\\$")
2017-03-01 18:47:52 +01:00
+ "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})";
final String deobfuscateFunction = "var " + Parser.matchGroup1(functionPattern,
playerCode) + ";";
2017-03-01 18:47:52 +01:00
final String helperObjectName =
Parser.matchGroup1(";([A-Za-z0-9_\\$]{2})\\...\\(",
deobfuscateFunction);
final String helperPattern =
"(var " + helperObjectName.replace("$", "\\$")
+ "=\\{.+?\\}\\};)";
final String helperObject =
Parser.matchGroup1(helperPattern, playerCode.replace("\n", ""));
2017-03-01 18:47:52 +01:00
final String callerFunction =
"function " + DEOBFUSCATION_FUNC_NAME + "(a){return "
+ deobfuscationFunctionName + "(a);}";
2017-03-01 18:47:52 +01:00
2020-10-26 16:32:39 +01:00
return helperObject + deobfuscateFunction + callerFunction;
} catch (final Exception e) {
2020-10-26 16:32:39 +01:00
throw new DeobfuscateException("Could not parse deobfuscate function ", e);
2017-03-01 18:47:52 +01:00
}
}
@Nonnull
private String getDeobfuscationCode() throws ParsingException {
if (cachedDeobfuscationCode == null) {
if (playerCode == null) {
storePlayerJs();
if (playerCode == null) {
throw new ParsingException("Could not get YouTube's JavaScript player");
}
}
cachedDeobfuscationCode = loadDeobfuscationCode();
}
return cachedDeobfuscationCode;
}
private String getStsFromPlayerJs() throws ParsingException {
if (!isNullOrEmpty(sts)) return sts;
if (playerCode == null) {
storePlayerJs();
if (playerCode == null) throw new ParsingException("playerCode is null");
}
sts = Parser.matchGroup1(STS_REGEX, playerCode);
return sts;
}
private String deobfuscateSignature(final String obfuscatedSig) throws ParsingException {
final String deobfuscationCode = getDeobfuscationCode();
2020-08-15 17:08:07 +02:00
final Context context = Context.enter();
2017-03-01 18:47:52 +01:00
context.setOptimizationLevel(-1);
2020-08-15 17:08:07 +02:00
final Object result;
2017-03-01 18:47:52 +01:00
try {
2020-08-15 17:08:07 +02:00
final ScriptableObject scope = context.initSafeStandardObjects();
context.evaluateString(scope, deobfuscationCode, "deobfuscationCode", 1, null);
final Function deobfuscateFunc = (Function) scope.get(DEOBFUSCATION_FUNC_NAME, scope);
2020-10-26 16:32:39 +01:00
result = deobfuscateFunc.call(context, scope, scope, new Object[]{obfuscatedSig});
} catch (final Exception e) {
2020-10-26 16:32:39 +01:00
throw new DeobfuscateException("Could not get deobfuscate signature", e);
2017-03-01 18:47:52 +01:00
} finally {
Context.exit();
}
2020-10-19 13:47:41 +02:00
return Objects.toString(result, "");
2017-03-01 18:47:52 +01:00
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private JsonObject getVideoPrimaryInfoRenderer() throws ParsingException {
if (this.videoPrimaryInfoRenderer != null) return this.videoPrimaryInfoRenderer;
final JsonArray contents = nextResponse.getObject("contents")
.getObject("twoColumnWatchNextResults").getObject("results").getObject("results")
.getArray("contents");
JsonObject videoPrimaryInfoRenderer = null;
for (final Object content : contents) {
2020-04-16 16:08:14 +02:00
if (((JsonObject) content).has("videoPrimaryInfoRenderer")) {
videoPrimaryInfoRenderer = ((JsonObject) content)
.getObject("videoPrimaryInfoRenderer");
break;
}
}
2020-07-18 09:50:22 +02:00
if (isNullOrEmpty(videoPrimaryInfoRenderer)) {
throw new ParsingException("Could not find videoPrimaryInfoRenderer");
}
this.videoPrimaryInfoRenderer = videoPrimaryInfoRenderer;
return videoPrimaryInfoRenderer;
}
private JsonObject getVideoSecondaryInfoRenderer() throws ParsingException {
if (this.videoSecondaryInfoRenderer != null) return this.videoSecondaryInfoRenderer;
final JsonArray contents = nextResponse.getObject("contents")
.getObject("twoColumnWatchNextResults").getObject("results").getObject("results")
.getArray("contents");
JsonObject videoSecondaryInfoRenderer = null;
for (final Object content : contents) {
2020-04-16 16:08:14 +02:00
if (((JsonObject) content).has("videoSecondaryInfoRenderer")) {
videoSecondaryInfoRenderer = ((JsonObject) content)
.getObject("videoSecondaryInfoRenderer");
break;
}
}
2020-07-18 09:50:22 +02:00
if (isNullOrEmpty(videoSecondaryInfoRenderer)) {
throw new ParsingException("Could not find videoSecondaryInfoRenderer");
}
this.videoSecondaryInfoRenderer = videoSecondaryInfoRenderer;
return videoSecondaryInfoRenderer;
}
@Nonnull
private static String getVideoInfoUrl(final String id, final String sts) {
2020-07-18 09:50:22 +02:00
// TODO: Try parsing embedded_player_response first
return "https://www.youtube.com/get_video_info?" + "video_id=" + id +
"&html5=1&eurl=https://youtube.googleapis.com/v/" + id +
"&sts=" + sts + "&ps=default&gl=US&hl=en";
}
private Map<String, ItagItem> getItags(final String streamingDataKey,
final ItagItem.ItagType itagTypeWanted)
throws ParsingException {
final Map<String, ItagItem> urlAndItags = new LinkedHashMap<>();
if (streamingData == null || !streamingData.has(streamingDataKey)) {
return urlAndItags;
}
final JsonArray formats = streamingData.getArray(streamingDataKey);
2019-09-11 19:05:41 +02:00
for (int i = 0; i != formats.size(); ++i) {
JsonObject formatData = formats.getObject(i);
int itag = formatData.getInt("itag");
2019-09-11 19:05:41 +02:00
if (ItagItem.isSupported(itag)) {
try {
ItagItem itagItem = ItagItem.getItag(itag);
if (itagItem.itagType == itagTypeWanted) {
2020-09-29 10:48:02 +02:00
// Ignore streams that are delivered using YouTube's OTF format,
// as those only work with DASH and not with progressive HTTP.
if (formatData.getString("type", EMPTY_STRING)
.equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF")) {
continue;
}
final String streamUrl;
if (formatData.has("url")) {
streamUrl = formatData.getString("url");
} else {
// This url has an obfuscated signature
2020-05-09 20:06:50 +02:00
final String cipherString = formatData.has("cipher")
? formatData.getString("cipher")
: formatData.getString("signatureCipher");
final Map<String, String> cipher = Parser.compatParseMap(cipherString);
streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "="
+ deobfuscateSignature(cipher.get("s"));
}
final JsonObject initRange = formatData.getObject("initRange");
final JsonObject indexRange = formatData.getObject("indexRange");
final String mimeType = formatData.getString("mimeType", EMPTY_STRING);
final String codec = mimeType.contains("codecs")
? mimeType.split("\"")[1] : EMPTY_STRING;
2021-02-10 07:39:35 +01:00
2021-02-11 17:12:24 +01:00
itagItem.setBitrate(formatData.getInt("bitrate"));
itagItem.setWidth(formatData.getInt("width"));
itagItem.setHeight(formatData.getInt("height"));
itagItem.setInitStart(Integer.parseInt(initRange.getString("start",
"-1")));
itagItem.setInitEnd(Integer.parseInt(initRange.getString("end",
"-1")));
itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start",
"-1")));
itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end",
"-1")));
2021-02-11 16:17:58 +01:00
itagItem.fps = formatData.getInt("fps");
2021-02-18 20:25:03 +01:00
itagItem.setQuality(formatData.getString("quality"));
2021-02-11 17:12:24 +01:00
itagItem.setCodec(codec);
2021-02-10 07:39:35 +01:00
urlAndItags.put(streamUrl, itagItem);
}
} catch (final UnsupportedEncodingException ignored) {
2020-10-26 16:32:39 +01:00
}
}
}
return urlAndItags;
}
@Nonnull
@Override
public List<Frameset> getFrames() throws ExtractionException {
try {
final JsonObject storyboards = playerResponse.getObject("storyboards");
final JsonObject storyboardsRenderer;
if (storyboards.has("playerLiveStoryboardSpecRenderer")) {
storyboardsRenderer = storyboards.getObject("playerLiveStoryboardSpecRenderer");
} else {
storyboardsRenderer = storyboards.getObject("playerStoryboardSpecRenderer");
}
if (storyboardsRenderer == null) {
2021-06-15 21:58:00 +02:00
return Collections.emptyList();
}
final String storyboardsRendererSpec = storyboardsRenderer.getString("spec");
if (storyboardsRendererSpec == null) {
2021-06-15 21:58:00 +02:00
return Collections.emptyList();
}
final String[] spec = storyboardsRendererSpec.split("\\|");
final String url = spec[0];
final ArrayList<Frameset> result = new ArrayList<>(spec.length - 1);
for (int i = 1; i < spec.length; ++i) {
final String[] parts = spec[i].split("#");
if (parts.length != 8 || Integer.parseInt(parts[5]) == 0) {
continue;
}
final int frameWidth = Integer.parseInt(parts[0]);
final int frameHeight = Integer.parseInt(parts[1]);
final int totalCount = Integer.parseInt(parts[2]);
final int framesPerPageX = Integer.parseInt(parts[3]);
final int framesPerPageY = Integer.parseInt(parts[4]);
final String baseUrl = url.replace("$L", String.valueOf(i - 1))
.replace("$N", parts[6]) + "&sigh=" + parts[7];
final List<String> urls;
if (baseUrl.contains("$M")) {
final int totalPages = (int) Math.ceil(totalCount / (double)
(framesPerPageX * framesPerPageY));
urls = new ArrayList<>(totalPages);
for (int j = 0; j < totalPages; j++) {
urls.add(baseUrl.replace("$M", String.valueOf(j)));
}
} else {
urls = Collections.singletonList(baseUrl);
}
result.add(new Frameset(
urls,
frameWidth,
frameHeight,
totalCount,
Integer.parseInt(parts[5]),
framesPerPageX,
framesPerPageY
));
}
result.trimToSize();
return result;
} catch (final Exception e) {
throw new ExtractionException("Could not get frames", e);
}
}
2020-02-25 09:07:22 +01:00
@Nonnull
@Override
2020-02-25 09:07:22 +01:00
public String getHost() {
return "";
}
2020-02-25 09:07:22 +01:00
@Nonnull
@Override
public Privacy getPrivacy() {
final boolean isUnlisted = playerResponse.getObject("microformat")
.getObject("playerMicroformatRenderer").getBoolean("isUnlisted");
return isUnlisted ? Privacy.UNLISTED : Privacy.PUBLIC;
}
2020-02-25 09:07:22 +01:00
@Nonnull
@Override
2020-02-25 09:07:22 +01:00
public String getCategory() {
return playerResponse.getObject("microformat").getObject("playerMicroformatRenderer")
.getString("category");
}
2020-02-25 09:07:22 +01:00
@Nonnull
@Override
public String getLicence() throws ParsingException {
final JsonObject metadataRowRenderer = getVideoSecondaryInfoRenderer()
.getObject("metadataRowContainer").getObject("metadataRowContainerRenderer")
.getArray("rows")
.getObject(0).getObject("metadataRowRenderer");
final JsonArray contents = metadataRowRenderer.getArray("contents");
final String license = getTextFromObject(contents.getObject(0));
return license != null && "Licence".equals(getTextFromObject(metadataRowRenderer
.getObject("title"))) ? license : "YouTube licence";
}
@Override
2020-02-25 09:07:22 +01:00
public Locale getLanguageInfo() {
return null;
}
@Nonnull
@Override
2020-02-25 09:07:22 +01:00
public List<String> getTags() {
return JsonUtils.getStringListFromJsonArray(playerResponse.getObject("videoDetails")
.getArray("keywords"));
}
@Nonnull
@Override
2020-02-25 09:07:22 +01:00
public String getSupportInfo() {
return "";
}
2020-12-12 10:24:29 +01:00
@Nonnull
@Override
public List<StreamSegment> getStreamSegments() throws ParsingException {
final ArrayList<StreamSegment> segments = new ArrayList<>();
if (nextResponse.has("engagementPanels")) {
final JsonArray panels = nextResponse.getArray("engagementPanels");
2020-12-12 10:24:29 +01:00
JsonArray segmentsArray = null;
// Search for correct panel containing the data
for (int i = 0; i < panels.size(); i++) {
final String panelIdentifier = panels.getObject(i)
.getObject("engagementPanelSectionListRenderer")
.getString("panelIdentifier");
if (panelIdentifier.equals(
"engagement-panel-macro-markers-description-chapters")) {
segmentsArray = panels.getObject(i)
.getObject("engagementPanelSectionListRenderer").getObject("content")
.getObject("macroMarkersListRenderer").getArray("contents");
2020-12-12 10:24:29 +01:00
break;
}
}
if (segmentsArray != null) {
final long duration = getLength();
for (final Object object : segmentsArray) {
final JsonObject segmentJson = ((JsonObject) object)
.getObject("macroMarkersListItemRenderer");
2020-12-12 10:24:29 +01:00
final int startTimeSeconds = segmentJson.getObject("onTap")
.getObject("watchEndpoint").getInt("startTimeSeconds", -1);
2020-12-12 10:24:29 +01:00
if (startTimeSeconds == -1) {
throw new ParsingException("Could not get stream segment start time.");
}
if (startTimeSeconds > duration) {
break;
}
final String title = getTextFromObject(segmentJson.getObject("title"));
if (isNullOrEmpty(title)) {
throw new ParsingException("Could not get stream segment title.");
}
final StreamSegment segment = new StreamSegment(title, startTimeSeconds);
segment.setUrl(getUrl() + "?t=" + startTimeSeconds);
if (segmentJson.has("thumbnail")) {
final JsonArray previewsArray = segmentJson.getObject("thumbnail")
.getArray("thumbnails");
2020-12-12 10:24:29 +01:00
if (!previewsArray.isEmpty()) {
// Assume that the thumbnail with the highest resolution is at the
// last position
final String url = previewsArray
.getObject(previewsArray.size() - 1).getString("url");
segment.setPreviewUrl(fixThumbnailUrl(url));
2020-12-12 10:24:29 +01:00
}
}
segments.add(segment);
}
}
}
return segments;
}
@Nonnull
@Override
public List<MetaInfo> getMetaInfo() throws ParsingException {
return YoutubeParsingHelper.getMetaInfo(
nextResponse.getObject("contents").getObject("twoColumnWatchNextResults")
2021-02-07 22:42:21 +01:00
.getObject("results").getObject("results").getArray("contents"));
}
2017-03-01 18:47:52 +01:00
}