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

866 lines
33 KiB
Java
Raw Normal View History

2017-03-01 18:47:52 +01:00
package org.schabi.newpipe.extractor.services.youtube;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
2017-03-01 18:47:52 +01:00
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ScriptableObject;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.NewPipe;
2017-08-06 22:20:15 +02:00
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.Subtitles;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
2017-04-12 02:55:53 +02:00
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.stream.*;
import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.Utils;
2017-03-01 18:47:52 +01:00
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
2017-03-01 18:47:52 +01:00
import java.io.IOException;
import java.util.*;
2017-03-01 18:47:52 +01:00
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/*
2017-03-01 18:47:52 +01:00
* Created by Christian Schabesberger on 06.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* 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 {
private static final String TAG = YoutubeStreamExtractor.class.getSimpleName();
2017-03-01 18:47:52 +01:00
/*//////////////////////////////////////////////////////////////////////////
// Exceptions
//////////////////////////////////////////////////////////////////////////*/
2017-03-01 18:47:52 +01:00
public class DecryptException extends ParsingException {
DecryptException(String message, Throwable cause) {
super(message, cause);
}
}
public class GemaException extends ContentNotAvailableException {
GemaException(String message) {
super(message);
}
}
public class LiveStreamException extends ContentNotAvailableException {
LiveStreamException(String message) {
super(message);
}
}
/*//////////////////////////////////////////////////////////////////////////*/
2017-03-01 18:47:52 +01:00
private Document doc;
@Nullable
private JsonObject playerArgs;
@Nonnull
private final Map<String, String> videoInfoPage = new HashMap<>();
private boolean isAgeRestricted;
2017-03-01 18:47:52 +01:00
2017-08-06 22:20:15 +02:00
public YoutubeStreamExtractor(StreamingService service, String url) throws IOException, ExtractionException {
super(service, url);
2017-03-01 18:47:52 +01:00
}
/*//////////////////////////////////////////////////////////////////////////
// Impl
//////////////////////////////////////////////////////////////////////////*/
2017-03-01 18:47:52 +01:00
@Nonnull
@Override
public String getId() throws ParsingException {
2017-03-01 18:47:52 +01:00
try {
2017-08-06 22:20:15 +02:00
return getUrlIdHandler().getId(getCleanUrl());
} catch (Exception e) {
throw new ParsingException("Could not get stream id");
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();
String name = getStringFromMetaData("title");
if(name == null) {
2017-08-11 03:23:09 +02:00
// Fallback to HTML method
try {
name = doc.select("meta[name=title]").attr(CONTENT);
} catch (Exception e) {
throw new ParsingException("Could not get the title", e);
}
}
if(name == null || name.isEmpty()) {
throw new ParsingException("Could not get the title");
2017-03-01 18:47:52 +01:00
}
return name;
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 getUploadDate() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-03-01 18:47:52 +01:00
try {
2017-08-11 03:23:09 +02:00
return doc.select("meta[itemprop=datePublished]").attr(CONTENT);
} catch (Exception e) {//todo: add fallback method
throw new ParsingException("Could not get upload date", e);
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 getThumbnailUrl() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-08-11 03:23:09 +02:00
// Try to get high resolution thumbnail first, if it fails, use low res from the player instead
2017-03-01 18:47:52 +01:00
try {
2017-08-11 03:23:09 +02:00
return doc.select("link[itemprop=\"thumbnailUrl\"]").first().attr("abs:href");
} catch (Exception ignored) {
// Try other method...
}
try {
if (playerArgs != null && playerArgs.isString("thumbnail_url")) return playerArgs.getString("thumbnail_url");
} catch (Exception ignored) {
// Try other method...
}
try {
2017-08-11 03:23:09 +02:00
return videoInfoPage.get("thumbnail_url");
2017-03-01 18:47:52 +01:00
} catch (Exception e) {
2017-08-11 03:23:09 +02:00
throw new ParsingException("Could not get thumbnail url", e);
}
}
@Nonnull
2017-08-11 03:23:09 +02:00
@Override
public String getDescription() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-08-11 03:23:09 +02:00
try {
return doc.select("p[id=\"eow-description\"]").first().html();
} catch (Exception e) {//todo: add fallback method <-- there is no ... as long as i know
throw new ParsingException("Could not get the description", e);
}
}
@Override
public int getAgeLimit() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-08-11 03:23:09 +02:00
if (!isAgeRestricted) {
2017-11-25 02:03:30 +01:00
return NO_AGE_LIMIT;
2017-08-11 03:23:09 +02:00
}
try {
return Integer.valueOf(doc.select("meta[property=\"og:restrictions:age\"]")
.attr(CONTENT).replace("+", ""));
} catch (Exception e) {
throw new ParsingException("Could not get age restriction");
2017-03-01 18:47:52 +01:00
}
}
@Override
public long getLength() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
if(playerArgs != null) {
try {
long returnValue = Long.parseLong(playerArgs.get("length_seconds") + "");
if (returnValue >= 0) return returnValue;
} catch (Exception ignored) {
// Try other method...
}
}
String lengthString = videoInfoPage.get("length_seconds");
try {
return Long.parseLong(lengthString);
} catch (Exception ignored) {
// Try other method...
}
// TODO: 25.11.17 Implement a way to get the length for age restricted videos #44
try {
// Fallback to HTML method
return Long.parseLong(doc.select("div[class~=\"ytp-progress-bar\"][role=\"slider\"]").first()
.attr("aria-valuemax"));
} catch (Exception e) {
throw new ParsingException("Could not get video length", 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 {
2017-11-22 18:45:49 +01:00
return getTimestampSeconds("((#|&|\\?)t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)");
2017-08-11 03:23:09 +02:00
}
2017-03-01 18:47:52 +01:00
@Override
public long getViewCount() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-03-01 18:47:52 +01:00
try {
return Long.parseLong(doc.select("meta[itemprop=interactionCount]").attr(CONTENT));
2017-03-01 18:47:52 +01:00
} catch (Exception e) {//todo: find fallback method
throw new ParsingException("Could not get number of views", e);
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
Element button = doc.select("button.like-button-renderer-like-button").first();
try {
likesString = button.select("span.yt-uix-button-content").first().text();
} catch (NullPointerException e) {
//if this kicks in our button has no content and therefore likes/dislikes are disabled
2017-08-11 03:23:09 +02:00
return -1;
}
return Integer.parseInt(Utils.removeNonDigitCharacters(likesString));
} catch (NumberFormatException nfe) {
throw new ParsingException("Could not parse \"" + likesString + "\" as an Integer", nfe);
} catch (Exception e) {
throw new ParsingException("Could not get like count", e);
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
Element button = doc.select("button.like-button-renderer-dislike-button").first();
try {
dislikesString = button.select("span.yt-uix-button-content").first().text();
} catch (NullPointerException e) {
//if this kicks in our button has no content and therefore likes/dislikes are disabled
return -1;
}
return Integer.parseInt(Utils.removeNonDigitCharacters(dislikesString));
} catch (NumberFormatException nfe) {
throw new ParsingException("Could not parse \"" + dislikesString + "\" as an Integer", nfe);
} catch (Exception e) {
throw new ParsingException("Could not get dislike count", e);
}
}
@Nonnull
2017-08-11 03:23:09 +02:00
@Override
public String getUploaderUrl() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-08-11 03:23:09 +02:00
try {
return doc.select("div[class=\"yt-user-info\"]").first().children()
.select("a").first().attr("abs:href");
} catch (Exception e) {
throw new ParsingException("Could not get channel link", e);
}
}
@Nullable
private String getStringFromMetaData(String field) {
2017-11-30 10:49:27 +01:00
assertPageFetched();
String value = null;
if(playerArgs != null) {
// This can not fail
value = playerArgs.getString(field);
}
if(value == null) {
// This can not fail too
value = videoInfoPage.get(field);
}
return value;
}
@Nonnull
2017-08-11 03:23:09 +02:00
@Override
public String getUploaderName() throws ParsingException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
String name = getStringFromMetaData("author");
if(name == null) {
try {
// Fallback to HTML method
name = doc.select("div.yt-user-info").first().text();
} catch (Exception e) {
throw new ParsingException("Could not get uploader name", e);
}
}
if(name == null || name.isEmpty()) {
throw new ParsingException("Could not get uploader name");
2017-03-01 18:47:52 +01:00
}
return name;
2017-03-01 18:47:52 +01:00
}
@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();
2017-03-01 18:47:52 +01:00
try {
return doc.select("a[class*=\"yt-user-photo\"]").first()
.select("img").first()
.attr("abs:data-thumb");
} catch (Exception e) {//todo: add fallback method
throw new ParsingException("Could not get uploader thumbnail URL.", e);
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 (videoInfoPage.containsKey("dashmpd")) {
2017-03-01 18:47:52 +01:00
dashManifestUrl = videoInfoPage.get("dashmpd");
} else if (playerArgs != null && playerArgs.isString("dashmpd")) {
dashManifestUrl = playerArgs.getString("dashmpd", "");
2017-03-01 18:47:52 +01:00
} else {
return "";
}
if (!dashManifestUrl.contains("/signature/")) {
2017-03-01 18:47:52 +01:00
String encryptedSig = Parser.matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifestUrl);
String decryptedSig;
decryptedSig = decryptSignature(encryptedSig, decryptionCode);
dashManifestUrl = dashManifestUrl.replace("/s/" + encryptedSig, "/signature/" + decryptedSig);
}
2017-03-01 18:47:52 +01:00
return dashManifestUrl;
} catch (Exception e) {
throw new ParsingException("Could not get dash manifest url", e);
2017-03-01 18:47:52 +01:00
}
}
@Override
2017-08-06 22:20:15 +02:00
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-08-06 22:20:15 +02:00
List<AudioStream> audioStreams = new ArrayList<>();
try {
for (Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FMTS, ItagItem.ItagType.AUDIO).entrySet()) {
ItagItem itag = entry.getValue();
2017-03-01 18:47:52 +01:00
AudioStream audioStream = new AudioStream(entry.getKey(), itag.getMediaFormat(), itag.avgBitrate);
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
audioStreams.add(audioStream);
2017-03-01 18:47:52 +01:00
}
}
} catch (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
2017-08-06 22:20:15 +02:00
public List<VideoStream> getVideoStreams() throws IOException, ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-08-06 22:20:15 +02:00
List<VideoStream> videoStreams = new ArrayList<>();
try {
for (Map.Entry<String, ItagItem> entry : getItags(URL_ENCODED_FMT_STREAM_MAP, ItagItem.ItagType.VIDEO).entrySet()) {
ItagItem itag = entry.getValue();
VideoStream videoStream = new VideoStream(entry.getKey(), itag.getMediaFormat(), itag.resolutionString);
if (!Stream.containSimilarStream(videoStream, videoStreams)) {
videoStreams.add(videoStream);
2017-03-01 18:47:52 +01:00
}
}
} catch (Exception e) {
throw new ParsingException("Could not get video streams", e);
2017-03-01 18:47:52 +01:00
}
return videoStreams;
}
@Override
2017-08-06 22:20:15 +02:00
public List<VideoStream> getVideoOnlyStreams() throws IOException, ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-08-06 22:20:15 +02:00
List<VideoStream> videoOnlyStreams = new ArrayList<>();
2017-04-12 02:55:53 +02:00
try {
for (Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FMTS, ItagItem.ItagType.VIDEO_ONLY).entrySet()) {
ItagItem itag = entry.getValue();
2017-04-12 02:55:53 +02:00
VideoStream videoStream = new VideoStream(entry.getKey(), itag.getMediaFormat(), itag.resolutionString, true);
if (!Stream.containSimilarStream(videoStream, videoOnlyStreams)) {
videoOnlyStreams.add(videoStream);
2017-04-12 02:55:53 +02:00
}
}
} catch (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
2017-11-25 02:20:16 +01:00
@Nullable
2017-11-24 13:57:54 +01:00
public List<Subtitles> getSubtitlesDefault() throws IOException, ExtractionException {
return getSubtitles(SubtitlesFormat.TTML);
}
@Override
2017-11-25 02:20:16 +01:00
@Nullable
2017-11-24 13:57:54 +01:00
public List<Subtitles> getSubtitles(SubtitlesFormat format) throws IOException, ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
if(isAgeRestricted) {
// If the video is age restricted getPlayerConfig will fail
return null;
}
2017-11-28 13:37:01 +01:00
// TODO: This should be done in onFetchPage()
JsonObject playerConfig = getPlayerConfig(getPageHtml(NewPipe.getDownloader()));
String playerResponse = playerConfig.getObject("args").getString("player_response");
2017-11-24 13:57:54 +01:00
JsonObject captions;
try {
// Captions does not exist, return null
if (!JsonParser.object().from(playerResponse).has("captions")) return null;
2017-11-24 13:57:54 +01:00
captions = JsonParser.object().from(playerResponse).getObject("captions");
} catch (JsonParserException e) {
// Failed to parse subtitles
return null;
}
JsonArray captionsArray = captions.getObject("playerCaptionsTracklistRenderer").getArray("captionTracks");
int captionsSize = captionsArray.size();
// Should not happen, if there is the "captions" object, it should always has some captions in it
if(captionsSize == 0) return null;
List<Subtitles> result = new ArrayList<>();
for (int x = 0; x < captionsSize; x++) {
String baseUrl = captionsArray.getObject(x).getString("baseUrl");
String extension = format.getExtension();
String URL = baseUrl.replaceAll("&fmt=[^&]*", "&fmt=" + extension);
String captionsLangCode = captionsArray.getObject(x).getString("vssId");
boolean isAutoGenerated = captionsLangCode.startsWith("a.");
String languageCode = captionsLangCode.replaceFirst((isAutoGenerated) ? "a." : ".", "");
result.add(new Subtitles(format, languageCode, URL, isAutoGenerated));
}
return result;
}
2017-03-01 18:47:52 +01:00
@Override
2017-08-11 03:23:09 +02:00
public StreamType getStreamType() throws ParsingException {
//todo: if implementing livestream support this value should be generated dynamically
return StreamType.VIDEO_STREAM;
2017-03-01 18:47:52 +01:00
}
@Override
public StreamInfoItem getNextVideo() throws IOException, ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-03-01 18:47:52 +01:00
try {
StreamInfoItemCollector collector = new StreamInfoItemCollector(getServiceId());
collector.commit(extractVideoPreviewInfo(doc.select("div[class=\"watch-sidebar-section\"]")
.first().select("li").first()));
return ((StreamInfoItem) collector.getItemList().get(0));
} catch (Exception e) {
2017-03-01 18:47:52 +01:00
throw new ParsingException("Could not get next video", e);
}
}
@Override
2017-08-06 22:20:15 +02:00
public StreamInfoItemCollector getRelatedVideos() throws IOException, ExtractionException {
2017-11-30 10:49:27 +01:00
assertPageFetched();
2017-03-01 18:47:52 +01:00
try {
2017-08-06 22:20:15 +02:00
StreamInfoItemCollector collector = new StreamInfoItemCollector(getServiceId());
2017-03-01 18:47:52 +01:00
Element ul = doc.select("ul[id=\"watch-related\"]").first();
if (ul != null) {
2017-03-01 18:47:52 +01:00
for (Element li : ul.children()) {
// first check if we have a playlist. If so leave them out
if (li.select("a[class*=\"content-link\"]").first() != null) {
collector.commit(extractVideoPreviewInfo(li));
}
}
}
return collector;
} catch (Exception e) {
2017-03-01 18:47:52 +01:00
throw new ParsingException("Could not get related videos", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public String getErrorMessage() {
String errorMessage = doc.select("h1[id=\"unavailable-message\"]").first().text();
StringBuilder errorReason;
2017-03-01 18:47:52 +01:00
if (errorMessage == null || errorMessage.isEmpty()) {
errorReason = null;
} else if (errorMessage.contains("GEMA")) {
// Gema sometimes blocks youtube music content in germany:
// https://www.gema.de/en/
// Detailed description:
// https://en.wikipedia.org/wiki/GEMA_%28German_organization%29
errorReason = new StringBuilder("GEMA");
} else {
errorReason = new StringBuilder(errorMessage);
errorReason.append(" ");
errorReason.append(doc.select("[id=\"unavailable-submessage\"]").first().text());
}
2017-03-01 18:47:52 +01:00
return errorReason != null ? errorReason.toString() : null;
}
2017-03-01 18:47:52 +01:00
/*//////////////////////////////////////////////////////////////////////////
// Fetch page
//////////////////////////////////////////////////////////////////////////*/
2017-03-01 18:47:52 +01:00
private static final String URL_ENCODED_FMT_STREAM_MAP = "url_encoded_fmt_stream_map";
private static final String ADAPTIVE_FMTS = "adaptive_fmts";
private static final String HTTPS = "https:";
private static final String CONTENT = "content";
private static final String DECRYPTION_FUNC_NAME = "decrypt";
private volatile String decryptionCode = "";
private String pageHtml = null;
2017-11-28 13:37:01 +01:00
private String getPageHtml(Downloader downloader) throws IOException, ExtractionException{
if (pageHtml == null) {
2017-11-28 13:37:01 +01:00
pageHtml = downloader.download(getCleanUrl());
}
return pageHtml;
}
2017-08-06 22:20:15 +02:00
@Override
2017-11-28 13:37:01 +01:00
public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException {
final String pageContent = getPageHtml(downloader);
2017-08-06 22:20:15 +02:00
doc = Jsoup.parse(pageContent, getCleanUrl());
final String playerUrl;
// TODO: use embedded videos to fetch DASH manifest for all videos
// Check if the video is age restricted
if (pageContent.contains("<meta property=\"og:restrictions:age")) {
final EmbeddedInfo info = getEmbeddedInfo();
final String videoInfoUrl = getVideoInfoUrl(getId(), info.sts);
final String infoPageResponse = downloader.download(videoInfoUrl);
videoInfoPage.putAll(Parser.compatParseMap(infoPageResponse));
playerUrl = info.url;
isAgeRestricted = true;
} else {
final JsonObject ytPlayerConfig = getPlayerConfig(pageContent);
playerArgs = getPlayerArgs(ytPlayerConfig);
playerUrl = getPlayerUrl(ytPlayerConfig);
isAgeRestricted = false;
}
if (decryptionCode.isEmpty()) {
decryptionCode = loadDecryptionCode(playerUrl);
}
}
private JsonObject getPlayerConfig(String pageContent) throws ParsingException {
try {
String ytPlayerConfigRaw = Parser.matchGroup1("ytplayer.config\\s*=\\s*(\\{.*?\\});", pageContent);
return JsonParser.object().from(ytPlayerConfigRaw);
} catch (Parser.RegexException e) {
String errorReason = getErrorMessage();
switch (errorReason) {
case "GEMA":
throw new GemaException(errorReason);
case "":
throw new ContentNotAvailableException("Content not available: player config empty", e);
default:
throw new ContentNotAvailableException("Content not available", e);
2017-03-01 18:47:52 +01:00
}
} catch (Exception e) {
throw new ParsingException("Could not parse yt player config", e);
}
}
2017-03-01 18:47:52 +01:00
private JsonObject getPlayerArgs(JsonObject playerConfig) throws ParsingException {
JsonObject playerArgs;
2017-03-01 18:47:52 +01:00
//attempt to load the youtube js player JSON arguments
boolean isLiveStream = false; //used to determine if this is a livestream or not
try {
playerArgs = playerConfig.getObject("args");
2017-03-01 18:47:52 +01:00
// check if we have a live stream. We need to filter it, since its not yet supported.
if ((playerArgs.has("ps") && playerArgs.get("ps").toString().equals("live"))
|| (playerArgs.get(URL_ENCODED_FMT_STREAM_MAP).toString().isEmpty())) {
isLiveStream = true;
2017-03-01 18:47:52 +01:00
}
} catch (Exception e) {
throw new ParsingException("Could not parse yt player config", e);
}
if (isLiveStream) {
2017-08-06 22:20:15 +02:00
throw new LiveStreamException("This is a Live stream. Can't use those right now.");
}
2017-03-01 18:47:52 +01:00
return playerArgs;
}
private String getPlayerUrl(JsonObject playerConfig) throws ParsingException {
try {
// The Youtube service needs to be initialized by downloading the
// js-Youtube-player. This is done in order to get the algorithm
// for decrypting cryptic signatures inside certain stream urls.
String playerUrl;
JsonObject ytAssets = playerConfig.getObject("assets");
playerUrl = ytAssets.getString("js");
if (playerUrl.startsWith("//")) {
playerUrl = HTTPS + playerUrl;
2017-03-01 18:47:52 +01:00
}
return playerUrl;
} catch (Exception e) {
throw new ParsingException("Could not load decryption code for the Youtube service.", e);
}
2017-03-01 18:47:52 +01:00
}
@Nonnull
private EmbeddedInfo getEmbeddedInfo() throws ParsingException, ReCaptchaException {
try {
final Downloader downloader = NewPipe.getDownloader();
final String embedUrl = "https://www.youtube.com/embed/" + getId();
final String embedPageContent = downloader.download(embedUrl);
// Get player url
final String assetsPattern = "\"assets\":.+?\"js\":\\s*(\"[^\"]+\")";
String playerUrl = Parser.matchGroup1(assetsPattern, embedPageContent)
.replace("\\", "").replace("\"", "");
if (playerUrl.startsWith("//")) {
playerUrl = HTTPS + playerUrl;
}
// Get embed sts
final String stsPattern = "\"sts\"\\s*:\\s*(\\d+)";
final String sts = Parser.matchGroup1(stsPattern, embedPageContent);
return new EmbeddedInfo(playerUrl, sts);
} catch (IOException e) {
throw new ParsingException(
"Could load decryption code form restricted video for the Youtube service.", e);
} catch (ReCaptchaException e) {
throw new ReCaptchaException("reCaptcha Challenge requested");
}
}
2017-03-01 18:47:52 +01:00
private String loadDecryptionCode(String playerUrl) throws DecryptException {
String decryptionFuncName;
String decryptionFunc;
String helperObjectName;
String helperObject;
String callerFunc = "function " + DECRYPTION_FUNC_NAME + "(a){return %%(a);}";
String decryptionCode;
try {
Downloader downloader = NewPipe.getDownloader();
if (!playerUrl.contains("https://youtube.com")) {
2017-03-01 18:47:52 +01:00
//sometimes the https://youtube.com part does not get send with
//than we have to add it by hand
playerUrl = "https://youtube.com" + playerUrl;
}
String playerCode = downloader.download(playerUrl);
decryptionFuncName =
Parser.matchGroup("([\"\\'])signature\\1\\s*,\\s*([a-zA-Z0-9$]+)\\(", playerCode, 2);
String functionPattern = "("
+ decryptionFuncName.replace("$", "\\$")
+ "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})";
decryptionFunc = "var " + Parser.matchGroup1(functionPattern, playerCode) + ";";
helperObjectName = Parser
.matchGroup1(";([A-Za-z0-9_\\$]{2})\\...\\(", decryptionFunc);
String helperPattern = "(var "
+ helperObjectName.replace("$", "\\$") + "=\\{.+?\\}\\};)";
helperObject = Parser.matchGroup1(helperPattern, playerCode);
callerFunc = callerFunc.replace("%%", decryptionFuncName);
decryptionCode = helperObject + decryptionFunc + callerFunc;
} catch (IOException ioe) {
2017-03-01 18:47:52 +01:00
throw new DecryptException("Could not load decrypt function", ioe);
} catch (Exception e) {
2017-03-01 18:47:52 +01:00
throw new DecryptException("Could not parse decrypt function ", e);
}
return decryptionCode;
}
private String decryptSignature(String encryptedSig, String decryptionCode) throws DecryptException {
2017-03-01 18:47:52 +01:00
Context context = Context.enter();
context.setOptimizationLevel(-1);
Object result;
2017-03-01 18:47:52 +01:00
try {
ScriptableObject scope = context.initStandardObjects();
context.evaluateString(scope, decryptionCode, "decryptionCode", 1, null);
Function decryptionFunc = (Function) scope.get("decrypt", scope);
result = decryptionFunc.call(context, scope, scope, new Object[]{encryptedSig});
} catch (Exception e) {
throw new DecryptException("could not get decrypt signature", e);
} finally {
Context.exit();
}
return result == null ? "" : result.toString();
}
/*//////////////////////////////////////////////////////////////////////////
// Data Class
//////////////////////////////////////////////////////////////////////////*/
private class EmbeddedInfo {
final String url;
final String sts;
EmbeddedInfo(final String url, final String sts) {
this.url = url;
this.sts = sts;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Nonnull
private String getVideoInfoUrl(final String id, final String sts) {
return "https://www.youtube.com/get_video_info?" + "video_id=" + id +
"&eurl=https://youtube.googleapis.com/v/" + id +
"&sts=" + sts + "&ps=default&gl=US&hl=en";
}
private Map<String, ItagItem> getItags(String encodedUrlMapKey, ItagItem.ItagType itagTypeWanted) throws ParsingException {
Map<String, ItagItem> urlAndItags = new LinkedHashMap<>();
String encodedUrlMap = "";
if (playerArgs != null && playerArgs.isString(encodedUrlMapKey)) {
encodedUrlMap = playerArgs.getString(encodedUrlMapKey, "");
} else if (videoInfoPage.containsKey(encodedUrlMapKey)) {
encodedUrlMap = videoInfoPage.get(encodedUrlMapKey);
}
for (String url_data_str : encodedUrlMap.split(",")) {
try {
// This loop iterates through multiple streams, therefore tags
// is related to one and the same stream at a time.
Map<String, String> tags = Parser.compatParseMap(
org.jsoup.parser.Parser.unescapeEntities(url_data_str, true));
int itag = Integer.parseInt(tags.get("itag"));
if (ItagItem.isSupported(itag)) {
ItagItem itagItem = ItagItem.getItag(itag);
if (itagItem.itagType == itagTypeWanted) {
String streamUrl = tags.get("url");
// if video has a signature: decrypt it and add it to the url
if (tags.get("s") != null) {
streamUrl = streamUrl + "&signature=" + decryptSignature(tags.get("s"), decryptionCode);
}
urlAndItags.put(streamUrl, itagItem);
}
}
} catch (DecryptException e) {
throw e;
} catch (Exception ignored) {
}
}
return urlAndItags;
}
/**
* Provides information about links to other videos on the video page, such as related videos.
* This is encapsulated in a StreamInfoItem object, which is a subset of the fields in a full StreamInfo.
*/
private StreamInfoItemExtractor extractVideoPreviewInfo(final Element li) {
return new YoutubeStreamInfoItemExtractor(li) {
@Override
2017-08-11 20:21:49 +02:00
public String getUrl() throws ParsingException {
return li.select("a.content-link").first().attr("abs:href");
}
@Override
2017-08-11 20:21:49 +02:00
public String getName() throws ParsingException {
//todo: check NullPointerException causing
return li.select("span.title").first().text();
//this page causes the NullPointerException, after finding it by searching for "tjvg":
//https://www.youtube.com/watch?v=Uqg0aEhLFAg
}
@Override
public String getUploaderName() throws ParsingException {
return li.select("span[class*=\"attribution\"").first()
.select("span").first().text();
}
2017-11-26 17:12:20 +01:00
@Override
public String getUploaderUrl() throws ParsingException {
return ""; // The uploader is not linked
}
@Override
public String getUploadDate() throws ParsingException {
return "";
}
@Override
public long getViewCount() throws ParsingException {
try {
if (getStreamType() == StreamType.LIVE_STREAM) return -1;
return Long.parseLong(Utils.removeNonDigitCharacters(
li.select("span.view-count").first().text()));
} catch (Exception e) {
//related videos sometimes have no view count
return 0;
}
}
@Override
public String getThumbnailUrl() throws ParsingException {
Element img = li.select("img").first();
String thumbnailUrl = img.attr("abs:src");
// Sometimes youtube sends links to gif files which somehow seem to not exist
// anymore. Items with such gif also offer a secondary image source. So we are going
// to use that if we caught such an item.
if (thumbnailUrl.contains(".gif")) {
thumbnailUrl = img.attr("data-thumb");
}
if (thumbnailUrl.startsWith("//")) {
thumbnailUrl = HTTPS + thumbnailUrl;
}
return thumbnailUrl;
}
};
2017-03-01 18:47:52 +01:00
}
}