2018-05-08 21:19:03 +02:00
|
|
|
package org.schabi.newpipe.extractor.services.youtube.extractors;
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2017-11-22 18:39:38 +01:00
|
|
|
import com.grack.nanojson.JsonArray;
|
2017-08-16 04:40:03 +02:00
|
|
|
import com.grack.nanojson.JsonObject;
|
|
|
|
import com.grack.nanojson.JsonParser;
|
2020-10-29 18:44:05 +01:00
|
|
|
import com.grack.nanojson.JsonParserException;
|
2020-10-27 13:48:58 +01:00
|
|
|
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;
|
2019-04-28 22:03:16 +02:00
|
|
|
import org.schabi.newpipe.extractor.MediaFormat;
|
2020-12-20 19:54:12 +01:00
|
|
|
import org.schabi.newpipe.extractor.MetaInfo;
|
2019-04-28 22:03:16 +02:00
|
|
|
import org.schabi.newpipe.extractor.NewPipe;
|
|
|
|
import org.schabi.newpipe.extractor.StreamingService;
|
|
|
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
2021-02-23 19:03:34 +01:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException;
|
2020-03-01 02:00:33 +01:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
2017-04-12 02:55:53 +02:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
2021-02-14 15:22:45 +01:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
|
|
|
|
import org.schabi.newpipe.extractor.exceptions.PaidContentException;
|
2017-04-12 02:55:53 +02:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
2021-02-14 15:22:45 +01:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
|
2017-04-12 02:55:53 +02:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
2021-02-14 15:22:45 +01:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
2018-07-13 18:02:40 +02:00
|
|
|
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
2019-11-03 19:45:25 +01:00
|
|
|
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
2020-02-25 21:19:53 +01:00
|
|
|
import org.schabi.newpipe.extractor.localization.Localization;
|
2019-12-16 08:35:43 +01:00
|
|
|
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
2020-02-25 21:19:53 +01:00
|
|
|
import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager;
|
2018-05-08 21:19:03 +02:00
|
|
|
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
|
2020-04-10 10:51:05 +02:00
|
|
|
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
2020-05-30 10:25:43 +02:00
|
|
|
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory;
|
2021-02-07 22:42:21 +01:00
|
|
|
import org.schabi.newpipe.extractor.stream.*;
|
2020-02-09 11:59:23 +01:00
|
|
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
2017-06-29 20:12:55 +02:00
|
|
|
import org.schabi.newpipe.extractor.utils.Parser;
|
2017-07-11 05:08:03 +02:00
|
|
|
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;
|
2020-12-20 19:54:12 +01:00
|
|
|
import java.io.UnsupportedEncodingException;
|
2020-10-18 05:48:14 +02:00
|
|
|
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;
|
2020-05-11 11:40:24 +02:00
|
|
|
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
2020-02-27 17:39:23 +01:00
|
|
|
|
2017-06-29 20:12:55 +02:00
|
|
|
/*
|
2017-03-01 18:47:52 +01:00
|
|
|
* Created by Christian Schabesberger on 06.08.15.
|
|
|
|
*
|
2019-03-14 08:49:11 +01:00
|
|
|
* 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 {
|
2017-07-11 05:08:03 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Exceptions
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
public static class DeobfuscateException extends ParsingException {
|
2021-02-24 17:06:38 +01:00
|
|
|
DeobfuscateException(final String message, final Throwable cause) {
|
2017-03-01 18:47:52 +01:00
|
|
|
super(message, cause);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-11 05:08:03 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////*/
|
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;
|
2020-10-29 18:44:05 +01:00
|
|
|
|
2020-02-28 15:17:47 +01:00
|
|
|
private JsonArray initialAjaxJson;
|
2020-02-22 23:51:02 +01:00
|
|
|
private JsonObject initialData;
|
2021-02-07 22:42:21 +01:00
|
|
|
@Nonnull
|
|
|
|
private final Map<String, String> videoInfoPage = new HashMap<>();
|
2020-10-29 18:44:05 +01:00
|
|
|
private JsonObject playerResponse;
|
2020-02-28 17:03:21 +01:00
|
|
|
private JsonObject videoPrimaryInfoRenderer;
|
|
|
|
private JsonObject videoSecondaryInfoRenderer;
|
2020-10-29 18:44:05 +01:00
|
|
|
private int ageLimit = -1;
|
2021-02-07 22:42:21 +01:00
|
|
|
@Nullable
|
|
|
|
private List<SubtitlesStream> subtitles = null;
|
2017-08-10 19:50:59 +02:00
|
|
|
|
2021-02-24 17:06:38 +01:00
|
|
|
public YoutubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler) {
|
2019-04-28 22:03:16 +02:00
|
|
|
super(service, linkHandler);
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
|
2017-07-11 05:08:03 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Impl
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2017-11-25 01:10:04 +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"));
|
2021-02-24 17:06:38 +01:00
|
|
|
} 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
|
|
|
|
2020-05-11 11:40:24 +02: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
|
|
|
|
2020-05-11 11:40:24 +02:00
|
|
|
if (isNullOrEmpty(title)) throw new ParsingException("Could not get name");
|
2017-11-25 01:10:04 +01:00
|
|
|
}
|
2020-02-29 17:18:50 +01:00
|
|
|
|
|
|
|
return title;
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
|
2020-11-04 14:50:35 +01:00
|
|
|
@Nullable
|
2017-03-01 18:47:52 +01:00
|
|
|
@Override
|
2019-04-28 22:03:16 +02:00
|
|
|
public String getTextualUploadDate() throws ParsingException {
|
2020-11-04 14:50:35 +01:00
|
|
|
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");
|
2020-11-04 14:50:35 +01:00
|
|
|
} else if (!micro.getString("publishDate", EMPTY_STRING).isEmpty()) {
|
2020-04-16 16:08:14 +02:00
|
|
|
return micro.getString("publishDate");
|
2020-11-04 14:50:35 +01:00
|
|
|
} 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
|
|
|
}
|
2020-02-25 09:50:22 +01:00
|
|
|
|
2020-04-16 16:08:14 +02:00
|
|
|
if (getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText")).startsWith("Premiered")) {
|
|
|
|
String time = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText")).substring(10);
|
|
|
|
|
|
|
|
try { // Premiered 20 hours ago
|
|
|
|
TimeAgoParser timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(Localization.fromLocalizationCode("en"));
|
2020-10-18 05:48:14 +02:00
|
|
|
OffsetDateTime parsedTime = timeAgoParser.parse(time).offsetDateTime();
|
|
|
|
return DateTimeFormatter.ISO_LOCAL_DATE.format(parsedTime);
|
2021-02-24 17:06:38 +01:00
|
|
|
} 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
|
2020-10-29 18:44:05 +01:00
|
|
|
final LocalDate localDate = LocalDate.parse(time,
|
|
|
|
DateTimeFormatter.ofPattern("MMM dd, yyyy", Locale.ENGLISH));
|
2020-10-18 05:48:14 +02:00
|
|
|
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception ignored) {
|
2020-10-26 16:32:39 +01:00
|
|
|
}
|
2020-04-16 16:08:14 +02:00
|
|
|
}
|
2020-02-25 21:19:53 +01:00
|
|
|
|
2017-03-01 18:47:52 +01:00
|
|
|
try {
|
2020-04-16 16:08:14 +02:00
|
|
|
// TODO: this parses English formatted dates only, we need a better approach to parse the textual date
|
2020-10-18 05:48:14 +02:00
|
|
|
LocalDate localDate = LocalDate.parse(getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText")),
|
|
|
|
DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH));
|
|
|
|
return DateTimeFormatter.ISO_LOCAL_DATE.format(localDate);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception ignored) {
|
2020-10-26 16:32:39 +01:00
|
|
|
}
|
2020-11-04 14:50:35 +01:00
|
|
|
|
2020-02-25 16:24:18 +01:00
|
|
|
throw new ParsingException("Could not get upload date");
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
|
2019-04-28 22:03:16 +02:00
|
|
|
@Override
|
2019-11-03 19:45:25 +01:00
|
|
|
public DateWrapper getUploadDate() throws ParsingException {
|
2019-04-28 22:03:16 +02:00
|
|
|
final String textualUploadDate = getTextualUploadDate();
|
|
|
|
|
2020-05-11 11:40:24 +02:00
|
|
|
if (isNullOrEmpty(textualUploadDate)) {
|
2019-04-28 22:03:16 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-12-16 08:35:43 +01:00
|
|
|
return new DateWrapper(YoutubeParsingHelper.parseDateFrom(textualUploadDate), true);
|
2019-04-28 22:03:16 +02:00
|
|
|
}
|
|
|
|
|
2017-11-25 01:10:04 +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-03-01 18:47:52 +01:00
|
|
|
try {
|
2020-01-06 20:45:57 +01:00
|
|
|
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");
|
2017-08-10 19:50:59 +02:00
|
|
|
|
2020-02-28 09:36:33 +01:00
|
|
|
return fixThumbnailUrl(url);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2020-02-25 09:50:22 +01:00
|
|
|
throw new ParsingException("Could not get thumbnail url");
|
2017-08-11 03:23:09 +02:00
|
|
|
}
|
2020-01-06 20:45:57 +01:00
|
|
|
|
2017-08-11 03:23:09 +02:00
|
|
|
}
|
|
|
|
|
2017-11-25 01:10:04 +01: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();
|
2020-02-25 18:27:39 +01:00
|
|
|
// description with more info on links
|
2020-07-18 09:50:22 +02:00
|
|
|
try {
|
|
|
|
String description = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("description"), true);
|
2020-02-09 11:59:23 +01:00
|
|
|
if (!isNullOrEmpty(description)) return new Description(description, Description.HTML);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final ParsingException ignored) {
|
2020-07-26 14:15:13 +02:00
|
|
|
// age-restricted videos cause a ParsingException here
|
|
|
|
}
|
2020-02-25 18:27:39 +01:00
|
|
|
|
2021-02-12 22:22:11 +01:00
|
|
|
String description = playerResponse.getObject("videoDetails").getString("shortDescription");
|
|
|
|
if (description == null) {
|
|
|
|
final JsonObject descriptionObject = playerResponse.getObject("microformat")
|
|
|
|
.getObject("playerMicroformatRenderer").getObject("description");
|
|
|
|
description = getTextFromObject(descriptionObject);
|
|
|
|
}
|
|
|
|
|
2020-02-25 18:27:39 +01:00
|
|
|
// 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
|
2020-10-29 18:44:05 +01:00
|
|
|
public int getAgeLimit() throws ParsingException {
|
|
|
|
if (ageLimit == -1) {
|
|
|
|
ageLimit = NO_AGE_LIMIT;
|
2020-02-28 17:14:26 +01:00
|
|
|
|
2020-10-29 18:44:05 +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
|
2017-08-10 19:50:59 +02:00
|
|
|
public long getLength() throws ParsingException {
|
2017-11-30 10:49:27 +01:00
|
|
|
assertPageFetched();
|
2019-07-30 20:53:23 +02:00
|
|
|
|
2017-08-10 19:50:59 +02:00
|
|
|
try {
|
2019-07-30 20:53:23 +02:00
|
|
|
String duration = playerResponse
|
|
|
|
.getObject("videoDetails")
|
|
|
|
.getString("lengthSeconds");
|
|
|
|
return Long.parseLong(duration);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2020-01-06 20:45:57 +01:00
|
|
|
try {
|
|
|
|
String durationMs = playerResponse
|
|
|
|
.getObject("streamingData")
|
|
|
|
.getArray("formats")
|
|
|
|
.getObject(0)
|
|
|
|
.getString("approxDurationMs");
|
2020-01-20 22:52:48 +01:00
|
|
|
return Math.round(Long.parseLong(durationMs) / 1000f);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception ignored) {
|
2020-01-06 20:45:57 +01:00
|
|
|
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?)");
|
2020-04-09 14:45:33 +02:00
|
|
|
|
|
|
|
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 {
|
2017-11-30 10:49:27 +01:00
|
|
|
assertPageFetched();
|
2020-07-18 09:50:22 +02:00
|
|
|
String views = null;
|
|
|
|
|
|
|
|
try {
|
|
|
|
views = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("viewCount")
|
2020-02-27 17:39:23 +01:00
|
|
|
.getObject("videoViewCountRenderer").getObject("viewCount"));
|
2021-02-24 17:06:38 +01:00
|
|
|
} 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
|
|
|
|
2020-05-11 11:40:24 +02:00
|
|
|
if (isNullOrEmpty(views)) {
|
2020-04-16 16:08:14 +02:00
|
|
|
views = playerResponse.getObject("videoDetails").getString("viewCount");
|
2020-02-29 17:18:50 +01:00
|
|
|
|
2020-05-11 11:40:24 +02:00
|
|
|
if (isNullOrEmpty(views)) throw new ParsingException("Could not get view count");
|
2020-02-24 16:04:01 +01:00
|
|
|
}
|
2020-02-29 17:18:50 +01:00
|
|
|
|
2020-03-17 15:00:07 +01:00
|
|
|
if (views.toLowerCase().contains("no views")) return 0;
|
|
|
|
|
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];
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final NullPointerException e) {
|
|
|
|
// if this kicks in our button has no content and therefore ratings must be disabled
|
2020-01-06 20:45:57 +01:00
|
|
|
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));
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final NumberFormatException nfe) {
|
2017-08-11 03:23:09 +02:00
|
|
|
throw new ParsingException("Could not parse \"" + likesString + "\" as an Integer", nfe);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2020-10-29 18:44:05 +01:00
|
|
|
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];
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final NullPointerException e) {
|
|
|
|
// if this kicks in our button has no content and therefore ratings must be disabled
|
2020-01-06 20:45:57 +01:00
|
|
|
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));
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final NumberFormatException nfe) {
|
2017-08-11 03:23:09 +02:00
|
|
|
throw new ParsingException("Could not parse \"" + dislikesString + "\" as an Integer", nfe);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2020-10-29 18:44:05 +01:00
|
|
|
if (getAgeLimit() == NO_AGE_LIMIT) {
|
|
|
|
throw new ParsingException("Could not get dislike count", e);
|
|
|
|
}
|
|
|
|
return -1;
|
2017-08-11 03:23:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-25 01:10:04 +01: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
|
|
|
|
2020-07-18 09:50:22 +02:00
|
|
|
try {
|
2021-02-24 17:06:38 +01:00
|
|
|
final String uploaderUrl = getUrlFromNavigationEndpoint(getVideoSecondaryInfoRenderer()
|
2020-02-27 17:39:23 +01:00
|
|
|
.getObject("owner").getObject("videoOwnerRenderer").getObject("navigationEndpoint"));
|
2020-07-18 09:50:22 +02:00
|
|
|
if (!isNullOrEmpty(uploaderUrl)) {
|
|
|
|
return uploaderUrl;
|
|
|
|
}
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final ParsingException ignored) {
|
2020-07-26 14:15:13 +02:00
|
|
|
// age-restricted videos cause a ParsingException here
|
|
|
|
}
|
2020-04-16 16:08:14 +02:00
|
|
|
|
2021-02-24 17:06:38 +01:00
|
|
|
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
|
|
|
|
2020-02-25 10:05:53 +01:00
|
|
|
throw new ParsingException("Could not get uploader url");
|
2017-11-25 01:10:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
String uploaderName = null;
|
|
|
|
|
|
|
|
try {
|
|
|
|
uploaderName = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("owner")
|
2020-02-27 17:39:23 +01:00
|
|
|
.getObject("videoOwnerRenderer").getObject("title"));
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final ParsingException ignored) {
|
2020-10-26 16:32:39 +01:00
|
|
|
}
|
2020-02-29 17:18:50 +01:00
|
|
|
|
2020-05-11 11:40:24 +02:00
|
|
|
if (isNullOrEmpty(uploaderName)) {
|
2020-04-16 16:08:14 +02:00
|
|
|
uploaderName = playerResponse.getObject("videoDetails").getString("author");
|
2020-02-29 17:18:50 +01:00
|
|
|
|
2020-05-11 11:40:24 +02:00
|
|
|
if (isNullOrEmpty(uploaderName)) throw new ParsingException("Could not get uploader name");
|
2017-08-10 19:50:59 +02:00
|
|
|
}
|
2020-02-29 17:18:50 +01:00
|
|
|
|
|
|
|
return uploaderName;
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
|
2021-01-22 01:44:58 +01:00
|
|
|
@Override
|
|
|
|
public boolean isUploaderVerified() throws ParsingException {
|
|
|
|
final JsonArray badges = getVideoSecondaryInfoRenderer().getObject("owner")
|
|
|
|
.getObject("videoOwnerRenderer").getArray("badges");
|
|
|
|
|
|
|
|
return YoutubeParsingHelper.isVerified(badges);
|
|
|
|
}
|
|
|
|
|
2017-11-25 01:10:04 +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();
|
2020-07-18 09:50:22 +02:00
|
|
|
|
|
|
|
String url = null;
|
|
|
|
|
2020-02-18 13:05:11 +01:00
|
|
|
try {
|
2020-07-18 09:50:22 +02:00
|
|
|
url = getVideoSecondaryInfoRenderer().getObject("owner").getObject("videoOwnerRenderer")
|
2020-02-25 17:40:23 +01:00
|
|
|
.getObject("thumbnail").getArray("thumbnails").getObject(0).getString("url");
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final ParsingException ignored) {
|
2020-07-26 14:15:13 +02:00
|
|
|
// age-restricted videos cause a ParsingException here
|
|
|
|
}
|
2020-02-27 17:39:23 +01:00
|
|
|
|
2020-07-18 09:50:22 +02:00
|
|
|
if (isNullOrEmpty(url)) {
|
2020-10-29 18:44:05 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2020-04-13 22:34:20 +02:00
|
|
|
@Nonnull
|
|
|
|
@Override
|
2020-10-29 18:44:05 +01:00
|
|
|
public String getSubChannelUrl() {
|
2020-04-13 22:34:20 +02:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
@Nonnull
|
|
|
|
@Override
|
2020-10-29 18:44:05 +01:00
|
|
|
public String getSubChannelName() {
|
2020-04-13 22:34:20 +02:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
@Nonnull
|
|
|
|
@Override
|
2020-10-29 18:44:05 +01:00
|
|
|
public String getSubChannelAvatarUrl() {
|
2020-04-13 22:34:20 +02:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2018-02-25 22:03:32 +01:00
|
|
|
@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 {
|
2017-07-11 05:08:03 +02:00
|
|
|
String dashManifestUrl;
|
2020-05-15 12:04:21 +02:00
|
|
|
if (playerResponse.getObject("streamingData").isString("dashManifestUrl")) {
|
|
|
|
return playerResponse.getObject("streamingData").getString("dashManifestUrl");
|
|
|
|
} else if (videoInfoPage.containsKey("dashmpd")) {
|
2017-03-01 18:47:52 +01:00
|
|
|
dashManifestUrl = videoInfoPage.get("dashmpd");
|
|
|
|
} else {
|
|
|
|
return "";
|
|
|
|
}
|
2017-08-10 19:50:59 +02:00
|
|
|
|
2017-06-29 20:12:55 +02:00
|
|
|
if (!dashManifestUrl.contains("/signature/")) {
|
2020-10-26 16:32:39 +01:00
|
|
|
String obfuscatedSig = Parser.matchGroup1("/s/([a-fA-F0-9\\.]+)", dashManifestUrl);
|
2021-02-24 17:06:38 +01:00
|
|
|
final String deobfuscatedSig;
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
deobfuscatedSig = deobfuscateSignature(obfuscatedSig);
|
2020-10-26 16:32:39 +01:00
|
|
|
dashManifestUrl = dashManifestUrl.replace("/s/" + obfuscatedSig, "/signature/" + deobfuscatedSig);
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
2017-08-10 19:50:59 +02:00
|
|
|
|
2017-03-01 18:47:52 +01:00
|
|
|
return dashManifestUrl;
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2017-08-10 19:50:59 +02:00
|
|
|
throw new ParsingException("Could not get dash manifest url", e);
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-25 22:03:32 +01:00
|
|
|
@Nonnull
|
|
|
|
@Override
|
|
|
|
public String getHlsUrl() throws ParsingException {
|
|
|
|
assertPageFetched();
|
2019-01-19 13:50:02 +01:00
|
|
|
|
2019-09-11 20:12:30 +02:00
|
|
|
try {
|
|
|
|
return playerResponse.getObject("streamingData").getString("hlsManifestUrl");
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2020-10-29 18:44:05 +01:00
|
|
|
throw new ParsingException("Could not get hls manifest url", e);
|
2018-02-25 22:03:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-01 18:47:52 +01:00
|
|
|
@Override
|
2019-09-11 20:04:28 +02:00
|
|
|
public List<AudioStream> getAudioStreams() throws ExtractionException {
|
2017-11-30 10:49:27 +01:00
|
|
|
assertPageFetched();
|
2021-02-24 17:06:38 +01:00
|
|
|
final List<AudioStream> audioStreams = new ArrayList<>();
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2021-02-24 17:06:38 +01:00
|
|
|
try {
|
|
|
|
for (final Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.AUDIO).entrySet()) {
|
|
|
|
final ItagItem itag = entry.getValue();
|
|
|
|
final AudioStream audioStream = new AudioStream(entry.getKey(), itag);
|
2017-08-10 19:50:59 +02:00
|
|
|
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
|
|
|
|
audioStreams.add(audioStream);
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
}
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2017-08-10 19:50:59 +02:00
|
|
|
throw new ParsingException("Could not get audio streams", e);
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
2017-08-10 19:50:59 +02:00
|
|
|
|
2017-03-01 18:47:52 +01:00
|
|
|
return audioStreams;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2019-09-11 20:04:28 +02:00
|
|
|
public List<VideoStream> getVideoStreams() throws ExtractionException {
|
2017-11-30 10:49:27 +01:00
|
|
|
assertPageFetched();
|
2021-02-24 17:06:38 +01:00
|
|
|
final List<VideoStream> videoStreams = new ArrayList<>();
|
2017-08-10 19:50:59 +02:00
|
|
|
|
2021-02-24 17:06:38 +01:00
|
|
|
try {
|
|
|
|
for (final Map.Entry<String, ItagItem> entry : getItags(FORMATS, ItagItem.ItagType.VIDEO).entrySet()) {
|
|
|
|
final ItagItem itag = entry.getValue();
|
|
|
|
final VideoStream videoStream = new VideoStream(entry.getKey(), false, itag);
|
2017-08-10 19:50:59 +02:00
|
|
|
if (!Stream.containSimilarStream(videoStream, videoStreams)) {
|
|
|
|
videoStreams.add(videoStream);
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
}
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2017-08-10 19:50:59 +02:00
|
|
|
throw new ParsingException("Could not get video streams", e);
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return videoStreams;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2018-08-21 17:23:56 +02:00
|
|
|
public List<VideoStream> getVideoOnlyStreams() throws ExtractionException {
|
2017-11-30 10:49:27 +01:00
|
|
|
assertPageFetched();
|
2021-02-24 17:06:38 +01:00
|
|
|
final List<VideoStream> videoOnlyStreams = new ArrayList<>();
|
2017-04-12 02:55:53 +02:00
|
|
|
try {
|
2021-02-24 17:06:38 +01:00
|
|
|
for (final Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.VIDEO_ONLY).entrySet()) {
|
|
|
|
final ItagItem itag = entry.getValue();
|
2017-04-12 02:55:53 +02:00
|
|
|
|
2021-02-24 17:06:38 +01:00
|
|
|
final VideoStream videoStream = new VideoStream(entry.getKey(), true, itag);
|
2017-08-10 19:50:59 +02:00
|
|
|
if (!Stream.containSimilarStream(videoStream, videoOnlyStreams)) {
|
|
|
|
videoOnlyStreams.add(videoStream);
|
2017-04-12 02:55:53 +02:00
|
|
|
}
|
|
|
|
}
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2017-08-10 19:50:59 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2017-11-22 18:39:38 +01:00
|
|
|
@Override
|
2018-02-01 22:27:14 +01:00
|
|
|
@Nonnull
|
2020-10-29 18:44:05 +01:00
|
|
|
public List<SubtitlesStream> getSubtitlesDefault() throws ParsingException {
|
2018-09-24 21:04:22 +02:00
|
|
|
return getSubtitles(MediaFormat.TTML);
|
2017-11-23 16:33:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2018-02-01 22:27:14 +01:00
|
|
|
@Nonnull
|
2020-10-29 18:44:05 +01:00
|
|
|
public List<SubtitlesStream> getSubtitles(final MediaFormat format) throws ParsingException {
|
2017-11-30 10:49:27 +01:00
|
|
|
assertPageFetched();
|
2021-02-24 17:06:38 +01:00
|
|
|
// if the video is age restricted getPlayerConfig will fail
|
2020-10-29 18:44:05 +01:00
|
|
|
if (getAgeLimit() != NO_AGE_LIMIT) {
|
|
|
|
return Collections.emptyList();
|
|
|
|
}
|
|
|
|
if (subtitles != null) {
|
|
|
|
// already calculated
|
|
|
|
return subtitles;
|
2017-11-23 11:47:05 +01:00
|
|
|
}
|
2020-10-29 18:44:05 +01:00
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-01 22:27:14 +01:00
|
|
|
return subtitles;
|
2017-11-22 18:39:38 +01:00
|
|
|
}
|
|
|
|
|
2017-03-01 18:47:52 +01:00
|
|
|
@Override
|
2020-11-04 14:50:35 +01:00
|
|
|
public StreamType getStreamType() {
|
2018-02-25 23:31:42 +01:00
|
|
|
assertPageFetched();
|
2020-11-04 14:50:35 +01:00
|
|
|
return playerResponse.getObject("streamingData").has(FORMATS)
|
|
|
|
? StreamType.VIDEO_STREAM : StreamType.LIVE_STREAM;
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
|
2020-11-04 14:50:35 +01:00
|
|
|
@Nullable
|
2020-05-30 10:25:43 +02:00
|
|
|
private StreamInfoItemExtractor getNextStream() throws ExtractionException {
|
2017-03-01 18:47:52 +01:00
|
|
|
try {
|
2020-03-14 14:02:48 +01:00
|
|
|
final JsonObject firstWatchNextItem = initialData.getObject("contents")
|
|
|
|
.getObject("twoColumnWatchNextResults").getObject("secondaryResults")
|
|
|
|
.getObject("secondaryResults").getArray("results").getObject(0);
|
|
|
|
|
|
|
|
if (!firstWatchNextItem.has("compactAutoplayRenderer")) {
|
|
|
|
// there is no "next" stream
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
final JsonObject videoInfo = firstWatchNextItem.getObject("compactAutoplayRenderer")
|
|
|
|
.getArray("contents").getObject(0).getObject("compactVideoRenderer");
|
|
|
|
|
2020-05-30 10:25:43 +02:00
|
|
|
return new YoutubeStreamInfoItemExtractor(videoInfo, getTimeAgoParser());
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2017-03-01 18:47:52 +01:00
|
|
|
throw new ParsingException("Could not get next video", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-24 21:17:58 +02:00
|
|
|
@Nullable
|
2017-03-01 18:47:52 +01:00
|
|
|
@Override
|
2021-03-31 20:21:49 +02:00
|
|
|
public StreamInfoItemsCollector getRelatedItems() throws ExtractionException {
|
2017-11-30 10:49:27 +01:00
|
|
|
assertPageFetched();
|
2020-02-28 16:40:50 +01:00
|
|
|
|
2020-10-24 21:17:58 +02:00
|
|
|
if (getAgeLimit() != NO_AGE_LIMIT) {
|
|
|
|
return null;
|
|
|
|
}
|
2020-02-28 16:40:50 +01:00
|
|
|
|
2017-03-01 18:47:52 +01:00
|
|
|
try {
|
2020-05-30 10:25:43 +02:00
|
|
|
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
|
|
|
|
|
|
|
final StreamInfoItemExtractor nextStream = getNextStream();
|
|
|
|
if (nextStream != null) {
|
|
|
|
collector.commit(nextStream);
|
|
|
|
}
|
|
|
|
|
|
|
|
final JsonArray results = initialData.getObject("contents").getObject("twoColumnWatchNextResults")
|
2020-02-18 17:04:22 +01:00
|
|
|
.getObject("secondaryResults").getObject("secondaryResults").getArray("results");
|
|
|
|
|
2019-04-28 22:03:16 +02:00
|
|
|
final TimeAgoParser timeAgoParser = getTimeAgoParser();
|
|
|
|
|
2020-05-30 10:25:43 +02:00
|
|
|
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));
|
|
|
|
}
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|
|
|
|
return collector;
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2017-03-01 18:47:52 +01:00
|
|
|
throw new ParsingException("Could not get related videos", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-29 20:12:55 +02:00
|
|
|
/**
|
2017-07-11 05:08:03 +02:00
|
|
|
* {@inheritDoc}
|
2017-06-29 20:12:55 +02:00
|
|
|
*/
|
2017-07-11 05:08:03 +02:00
|
|
|
@Override
|
|
|
|
public String getErrorMessage() {
|
2020-02-29 22:57:25 +01:00
|
|
|
try {
|
2020-04-11 17:26:31 +02:00
|
|
|
return getTextFromObject(initialAjaxJson.getObject(2).getObject("playerResponse")
|
|
|
|
.getObject("playabilityStatus").getObject("errorScreen")
|
|
|
|
.getObject("playerErrorMessageRenderer").getObject("reason"));
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final ParsingException | NullPointerException e) {
|
2020-04-11 17:26:31 +02:00
|
|
|
return null; // no error message
|
2020-02-29 22:57:25 +01:00
|
|
|
}
|
2017-07-11 05:08:03 +02:00
|
|
|
}
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2017-07-11 05:08:03 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
2017-08-10 19:50:59 +02:00
|
|
|
// Fetch page
|
2017-07-11 05:08:03 +02:00
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2019-09-11 20:04:28 +02:00
|
|
|
private static final String FORMATS = "formats";
|
|
|
|
private static final String ADAPTIVE_FORMATS = "adaptiveFormats";
|
2017-07-11 05:08:03 +02:00
|
|
|
private static final String HTTPS = "https:";
|
2020-10-26 21:22:21 +01:00
|
|
|
private static final String DEOBFUSCATION_FUNC_NAME = "deobfuscate";
|
2017-07-11 05:08:03 +02:00
|
|
|
|
2020-07-28 00:44:38 +02:00
|
|
|
private final static 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*;",
|
|
|
|
"yt\\.akamaized\\.net/\\)\\s*\\|\\|\\s*.*?\\s*c\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\(",
|
|
|
|
"\\bc\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\("
|
|
|
|
};
|
2018-09-08 07:25:07 +02:00
|
|
|
|
2017-08-06 22:20:15 +02:00
|
|
|
@Override
|
2020-10-29 18:44:05 +01:00
|
|
|
public void onFetchPage(@Nonnull final Downloader downloader)
|
|
|
|
throws IOException, ExtractionException {
|
|
|
|
initialAjaxJson = getJsonResponse(getUrl() + "&pbj=1", getExtractorLocalization());
|
|
|
|
|
|
|
|
initialData = initialAjaxJson.getObject(3).getObject("response", null);
|
|
|
|
if (initialData == null) {
|
|
|
|
initialData = initialAjaxJson.getObject(2).getObject("response", null);
|
|
|
|
if (initialData == null) {
|
|
|
|
throw new ParsingException("Could not get initial data");
|
2020-10-04 15:30:17 +02:00
|
|
|
}
|
2017-07-11 05:08:03 +02:00
|
|
|
}
|
2020-02-28 15:17:47 +01:00
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
playerResponse = initialAjaxJson.getObject(2).getObject("playerResponse", null);
|
2021-02-24 12:06:19 +01:00
|
|
|
// Save the playerResponse from the youtube.com website,
|
|
|
|
// 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.
|
2021-02-24 14:13:12 +01:00
|
|
|
JsonObject youtubePlayerResponse = playerResponse;
|
2021-02-24 12:06:19 +01:00
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
if (playerResponse == null || !playerResponse.has("streamingData")) {
|
|
|
|
// try to get player response by fetching video info page
|
|
|
|
fetchVideoInfoPage();
|
|
|
|
}
|
2017-07-11 05:08:03 +02:00
|
|
|
|
2021-02-24 14:13:12 +01:00
|
|
|
if (playerResponse == null && youtubePlayerResponse == null) {
|
|
|
|
throw new ExtractionException("Could not get playerResponse");
|
|
|
|
} else if (youtubePlayerResponse == null) {
|
|
|
|
youtubePlayerResponse = playerResponse;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonObject playabilityStatus = (playerResponse == null ? youtubePlayerResponse : playerResponse)
|
|
|
|
.getObject("playabilityStatus");
|
2021-02-24 12:06:19 +01:00
|
|
|
String status = playabilityStatus.getString("status");
|
2021-02-14 15:22:45 +01:00
|
|
|
// 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.
|
2021-02-24 14:13:12 +01:00
|
|
|
if (status != null && !status.equalsIgnoreCase("ok")) {
|
2021-02-24 12:06:19 +01:00
|
|
|
playabilityStatus = youtubePlayerResponse.getObject("playabilityStatus");
|
|
|
|
status = playabilityStatus.getString("status");
|
|
|
|
|
2020-03-01 02:00:33 +01:00
|
|
|
final String reason = playabilityStatus.getString("reason");
|
2021-02-14 15:22:45 +01:00
|
|
|
|
2021-02-24 14:13:12 +01:00
|
|
|
if (status.equalsIgnoreCase("login_required")) {
|
2021-02-14 15:22:45 +01:00
|
|
|
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 thrown an AgeRestrictedContentException explicitly.
|
2021-02-23 19:03:34 +01:00
|
|
|
throw new AgeRestrictedContentException("This age-restricted video cannot be watched.");
|
2021-02-14 15:22:45 +01:00
|
|
|
}
|
|
|
|
}
|
2021-02-24 14:13:12 +01:00
|
|
|
if (status.equalsIgnoreCase("unplayable")) {
|
2021-02-14 15:22:45 +01:00
|
|
|
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");
|
|
|
|
}
|
2021-02-24 13:01:27 +01:00
|
|
|
if (reason.equals("Join this channel to get access to members-only content like this video, and other exclusive perks.") ||
|
2021-02-24 17:06:38 +01:00
|
|
|
reason.equals("Join this channel to get access to members-only content like this video and other exclusive perks.")) {
|
2021-02-14 15:22:45 +01:00
|
|
|
throw new PaidContentException("This video is only available for members of the channel of this video");
|
|
|
|
}
|
|
|
|
if (reason.equals("Video unavailable")) {
|
|
|
|
final String detailedErrorMessage = playabilityStatus.getObject("errorScreen")
|
|
|
|
.getObject("playerErrorMessageRenderer")
|
|
|
|
.getObject("subreason")
|
|
|
|
.getArray("runs")
|
|
|
|
.getObject(0)
|
|
|
|
.getString("text");
|
|
|
|
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 + "\"");
|
2020-03-01 02:00:33 +01:00
|
|
|
}
|
2017-07-11 05:08:03 +02:00
|
|
|
}
|
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
private void fetchVideoInfoPage() throws ParsingException, ReCaptchaException, IOException {
|
|
|
|
final String sts = getEmbeddedInfoStsAndStorePlayerJsUrl();
|
|
|
|
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 {
|
2020-10-29 18:44:05 +01:00
|
|
|
playerResponse = JsonParser.object().from(videoInfoPage.get("player_response"));
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final JsonParserException e) {
|
2020-10-29 18:44:05 +01:00
|
|
|
throw new ParsingException(
|
|
|
|
"Could not parse YouTube player response from video info page", e);
|
2019-09-11 19:05:41 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-18 23:05:58 +01:00
|
|
|
@Nonnull
|
2020-10-29 18:44:05 +01:00
|
|
|
private String getEmbeddedInfoStsAndStorePlayerJsUrl() {
|
2017-07-11 05:08:03 +02:00
|
|
|
try {
|
2017-12-18 23:05:58 +01:00
|
|
|
final String embedUrl = "https://www.youtube.com/embed/" + getId();
|
2020-10-29 18:44:05 +01:00
|
|
|
final String embedPageContent = NewPipe.getDownloader()
|
|
|
|
.get(embedUrl, getExtractorLocalization()).responseBody();
|
2017-12-18 23:05:58 +01:00
|
|
|
|
2020-10-27 13:48:58 +01:00
|
|
|
try {
|
|
|
|
final String assetsPattern = "\"assets\":.+?\"js\":\\s*(\"[^\"]+\")";
|
2020-10-29 18:44:05 +01:00
|
|
|
playerJsUrl = Parser.matchGroup1(assetsPattern, embedPageContent)
|
2020-10-27 13:48:58 +01:00
|
|
|
.replace("\\", "").replace("\"", "");
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Parser.RegexException ex) {
|
2020-10-29 18:44:05 +01:00
|
|
|
// 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()
|
2020-10-27 13:48:58 +01:00
|
|
|
final Document doc = Jsoup.parse(embedPageContent);
|
|
|
|
final Elements elems = doc.select("script").attr("name", "player_ias/base");
|
2021-02-24 17:06:38 +01:00
|
|
|
for (final Element elem : elems) {
|
2020-10-27 13:48:58 +01:00
|
|
|
if (elem.attr("src").contains("base.js")) {
|
2020-10-29 18:44:05 +01:00
|
|
|
playerJsUrl = elem.attr("src");
|
|
|
|
break;
|
2020-10-27 13:48:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
// Get embed sts
|
|
|
|
return Parser.matchGroup1("\"sts\"\\s*:\\s*(\\d+)", embedPageContent);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception i) {
|
2020-10-29 18:44:05 +01:00
|
|
|
// if it fails we simply reply with no sts as then it does not seem to be necessary
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private String getDeobfuscationFuncName(final String playerCode) throws DeobfuscateException {
|
|
|
|
Parser.RegexException exception = null;
|
|
|
|
for (final String regex : REGEXES) {
|
2019-07-30 20:53:23 +02:00
|
|
|
try {
|
2020-10-29 18:44:05 +01:00
|
|
|
return Parser.matchGroup1(regex, playerCode);
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Parser.RegexException re) {
|
2020-10-29 18:44:05 +01:00
|
|
|
if (exception == null) {
|
|
|
|
exception = re;
|
|
|
|
}
|
2019-07-30 20:53:23 +02:00
|
|
|
}
|
2017-07-11 05:08:03 +02:00
|
|
|
}
|
2020-10-29 18:44:05 +01:00
|
|
|
throw new DeobfuscateException("Could not find deobfuscate function with any of the given patterns.", exception);
|
2017-07-11 05:08:03 +02:00
|
|
|
}
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
private String loadDeobfuscationCode(@Nonnull final String playerJsUrl)
|
|
|
|
throws DeobfuscateException {
|
2017-03-01 18:47:52 +01:00
|
|
|
try {
|
2020-10-29 18:44:05 +01:00
|
|
|
final String playerCode = NewPipe.getDownloader()
|
|
|
|
.get(playerJsUrl, getExtractorLocalization()).responseBody();
|
2020-10-26 16:32:39 +01:00
|
|
|
final String deobfuscationFunctionName = getDeobfuscationFuncName(playerCode);
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2018-09-08 07:25:07 +02: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_]+\\)\\{.+?\\})";
|
2020-10-26 16:32:39 +01:00
|
|
|
final String deobfuscateFunction = "var " + Parser.matchGroup1(functionPattern, playerCode) + ";";
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2018-09-08 07:25:07 +02:00
|
|
|
final String helperObjectName =
|
2020-10-26 16:32:39 +01:00
|
|
|
Parser.matchGroup1(";([A-Za-z0-9_\\$]{2})\\...\\(", deobfuscateFunction);
|
2018-09-08 07:25:07 +02:00
|
|
|
final String helperPattern =
|
|
|
|
"(var " + helperObjectName.replace("$", "\\$") + "=\\{.+?\\}\\};)";
|
|
|
|
final String helperObject =
|
|
|
|
Parser.matchGroup1(helperPattern, playerCode.replace("\n", ""));
|
2017-03-01 18:47:52 +01:00
|
|
|
|
2018-09-08 07:25:07 +02:00
|
|
|
final String callerFunction =
|
2020-10-26 16:32:39 +01:00
|
|
|
"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;
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final IOException ioe) {
|
2020-10-26 16:32:39 +01:00
|
|
|
throw new DeobfuscateException("Could not load deobfuscate function", ioe);
|
2021-02-24 17:06:38 +01:00
|
|
|
} 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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
@Nonnull
|
|
|
|
private String getDeobfuscationCode() throws ParsingException {
|
|
|
|
if (cachedDeobfuscationCode == null) {
|
|
|
|
if (playerJsUrl == null) {
|
|
|
|
// the currentPlayerJsUrl was not found in any page fetched so far and there is
|
|
|
|
// nothing cached, so try fetching embedded info
|
|
|
|
getEmbeddedInfoStsAndStorePlayerJsUrl();
|
|
|
|
if (playerJsUrl == null) {
|
|
|
|
throw new ParsingException(
|
|
|
|
"Embedded info did not provide YouTube player js url");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (playerJsUrl.startsWith("//")) {
|
|
|
|
playerJsUrl = HTTPS + playerJsUrl;
|
|
|
|
} else if (playerJsUrl.startsWith("/")) {
|
2020-12-24 18:58:45 +01:00
|
|
|
// sometimes https://www.youtube.com part has to be added manually
|
|
|
|
playerJsUrl = HTTPS + "//www.youtube.com" + playerJsUrl;
|
2020-10-29 18:44:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
cachedDeobfuscationCode = loadDeobfuscationCode(playerJsUrl);
|
|
|
|
}
|
|
|
|
return cachedDeobfuscationCode;
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2020-10-26 21:22:21 +01:00
|
|
|
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});
|
2021-02-24 17:06:38 +01:00
|
|
|
} 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
|
|
|
}
|
|
|
|
|
2017-08-10 19:50:59 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Utils
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
2020-02-25 09:50:22 +01:00
|
|
|
private JsonObject getVideoPrimaryInfoRenderer() throws ParsingException {
|
2020-02-28 17:03:21 +01:00
|
|
|
if (this.videoPrimaryInfoRenderer != null) return this.videoPrimaryInfoRenderer;
|
|
|
|
|
2020-02-25 09:50:22 +01:00
|
|
|
JsonArray contents = initialData.getObject("contents").getObject("twoColumnWatchNextResults")
|
|
|
|
.getObject("results").getObject("results").getArray("contents");
|
|
|
|
JsonObject videoPrimaryInfoRenderer = null;
|
|
|
|
|
2021-02-24 17:06:38 +01:00
|
|
|
for (final Object content : contents) {
|
2020-04-16 16:08:14 +02:00
|
|
|
if (((JsonObject) content).has("videoPrimaryInfoRenderer")) {
|
2020-02-25 09:50:22 +01:00
|
|
|
videoPrimaryInfoRenderer = ((JsonObject) content).getObject("videoPrimaryInfoRenderer");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-18 09:50:22 +02:00
|
|
|
if (isNullOrEmpty(videoPrimaryInfoRenderer)) {
|
2020-02-25 09:50:22 +01:00
|
|
|
throw new ParsingException("Could not find videoPrimaryInfoRenderer");
|
|
|
|
}
|
|
|
|
|
2020-02-28 17:03:21 +01:00
|
|
|
this.videoPrimaryInfoRenderer = videoPrimaryInfoRenderer;
|
2020-02-25 09:50:22 +01:00
|
|
|
return videoPrimaryInfoRenderer;
|
|
|
|
}
|
|
|
|
|
2020-02-25 10:05:53 +01:00
|
|
|
private JsonObject getVideoSecondaryInfoRenderer() throws ParsingException {
|
2020-02-28 17:03:21 +01:00
|
|
|
if (this.videoSecondaryInfoRenderer != null) return this.videoSecondaryInfoRenderer;
|
|
|
|
|
2020-02-25 10:05:53 +01:00
|
|
|
JsonArray contents = initialData.getObject("contents").getObject("twoColumnWatchNextResults")
|
|
|
|
.getObject("results").getObject("results").getArray("contents");
|
|
|
|
JsonObject videoSecondaryInfoRenderer = null;
|
|
|
|
|
2021-02-24 17:06:38 +01:00
|
|
|
for (final Object content : contents) {
|
2020-04-16 16:08:14 +02:00
|
|
|
if (((JsonObject) content).has("videoSecondaryInfoRenderer")) {
|
2020-02-25 10:05:53 +01:00
|
|
|
videoSecondaryInfoRenderer = ((JsonObject) content).getObject("videoSecondaryInfoRenderer");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-18 09:50:22 +02:00
|
|
|
if (isNullOrEmpty(videoSecondaryInfoRenderer)) {
|
2020-02-25 10:05:53 +01:00
|
|
|
throw new ParsingException("Could not find videoSecondaryInfoRenderer");
|
|
|
|
}
|
|
|
|
|
2020-02-28 17:03:21 +01:00
|
|
|
this.videoSecondaryInfoRenderer = videoSecondaryInfoRenderer;
|
2020-02-25 10:05:53 +01:00
|
|
|
return videoSecondaryInfoRenderer;
|
|
|
|
}
|
|
|
|
|
2017-12-18 23:05:58 +01:00
|
|
|
@Nonnull
|
2018-02-01 22:27:14 +01:00
|
|
|
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
|
2017-12-18 23:05:58 +01:00
|
|
|
return "https://www.youtube.com/get_video_info?" + "video_id=" + id +
|
2021-05-21 15:22:24 +02:00
|
|
|
"&html5=1&eurl=https://youtube.googleapis.com/v/" + id +
|
2017-12-18 23:05:58 +01:00
|
|
|
"&sts=" + sts + "&ps=default&gl=US&hl=en";
|
|
|
|
}
|
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
private Map<String, ItagItem> getItags(final String streamingDataKey,
|
|
|
|
final ItagItem.ItagType itagTypeWanted)
|
|
|
|
throws ParsingException {
|
|
|
|
final Map<String, ItagItem> urlAndItags = new LinkedHashMap<>();
|
|
|
|
final JsonObject streamingData = playerResponse.getObject("streamingData");
|
2019-09-12 15:08:17 +02:00
|
|
|
if (!streamingData.has(streamingDataKey)) {
|
|
|
|
return urlAndItags;
|
2017-08-10 19:50:59 +02:00
|
|
|
}
|
|
|
|
|
2020-10-29 18:44:05 +01:00
|
|
|
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");
|
2017-08-10 19:50:59 +02:00
|
|
|
|
2019-09-11 19:05:41 +02:00
|
|
|
if (ItagItem.isSupported(itag)) {
|
2019-09-11 19:56:16 +02:00
|
|
|
try {
|
2017-08-10 19:50:59 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-09-11 19:56:16 +02:00
|
|
|
String streamUrl;
|
|
|
|
if (formatData.has("url")) {
|
|
|
|
streamUrl = formatData.getString("url");
|
|
|
|
} else {
|
2020-10-26 16:32:39 +01:00
|
|
|
// 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") + "="
|
2020-10-29 18:44:05 +01:00
|
|
|
+ deobfuscateSignature(cipher.get("s"));
|
2017-08-10 19:50:59 +02:00
|
|
|
}
|
2019-09-11 19:56:16 +02:00
|
|
|
|
2021-02-11 10:10:38 +01:00
|
|
|
JsonObject initRange = formatData.getObject("initRange");
|
|
|
|
JsonObject indexRange = formatData.getObject("indexRange");
|
2021-02-10 07:39:35 +01:00
|
|
|
String mimeType = formatData.getString("mimeType", EMPTY_STRING);
|
|
|
|
String codec = mimeType.contains("codecs") ? mimeType.split("\"")[1] : EMPTY_STRING;
|
|
|
|
|
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
|
|
|
|
2017-08-10 19:50:59 +02:00
|
|
|
urlAndItags.put(streamUrl, itagItem);
|
|
|
|
}
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final UnsupportedEncodingException ignored) {
|
2020-10-26 16:32:39 +01:00
|
|
|
}
|
2017-08-10 19:50:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return urlAndItags;
|
|
|
|
}
|
|
|
|
|
2020-02-08 23:58:46 +01:00
|
|
|
@Nonnull
|
|
|
|
@Override
|
|
|
|
public List<Frameset> getFrames() throws ExtractionException {
|
|
|
|
try {
|
2020-04-09 15:52:42 +02:00
|
|
|
final JsonObject storyboards = playerResponse.getObject("storyboards");
|
|
|
|
final JsonObject storyboardsRenderer;
|
|
|
|
if (storyboards.has("playerLiveStoryboardSpecRenderer")) {
|
|
|
|
storyboardsRenderer = storyboards.getObject("playerLiveStoryboardSpecRenderer");
|
|
|
|
} else {
|
|
|
|
storyboardsRenderer = storyboards.getObject("playerStoryboardSpecRenderer");
|
|
|
|
}
|
|
|
|
|
|
|
|
final String[] spec = storyboardsRenderer.getString("spec").split("\\|");
|
2020-02-08 23:58:46 +01:00
|
|
|
final String url = spec[0];
|
|
|
|
final ArrayList<Frameset> result = new ArrayList<>(spec.length - 1);
|
2020-04-09 15:52:42 +02:00
|
|
|
|
2020-02-08 23:58:46 +01:00
|
|
|
for (int i = 1; i < spec.length; ++i) {
|
|
|
|
final String[] parts = spec[i].split("#");
|
2021-01-14 20:01:06 +01:00
|
|
|
if (parts.length != 8 || Integer.parseInt(parts[5]) == 0) {
|
2020-02-08 23:58:46 +01:00
|
|
|
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,
|
2021-01-14 20:01:06 +01:00
|
|
|
Integer.parseInt(parts[5]),
|
2020-02-08 23:58:46 +01:00
|
|
|
framesPerPageX,
|
|
|
|
framesPerPageY
|
|
|
|
));
|
|
|
|
}
|
|
|
|
result.trimToSize();
|
|
|
|
return result;
|
2021-02-24 17:06:38 +01:00
|
|
|
} catch (final Exception e) {
|
2020-02-08 23:58:46 +01:00
|
|
|
throw new ExtractionException(e);
|
|
|
|
}
|
|
|
|
}
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
|
2020-02-25 09:07:22 +01:00
|
|
|
@Nonnull
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
@Override
|
2020-02-25 09:07:22 +01:00
|
|
|
public String getHost() {
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2020-02-25 09:07:22 +01:00
|
|
|
@Nonnull
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
@Override
|
2020-02-09 11:59:23 +01:00
|
|
|
public Privacy getPrivacy() {
|
2021-02-12 20:34:46 +01:00
|
|
|
final boolean isUnlisted = playerResponse
|
2020-02-09 11:59:23 +01:00
|
|
|
.getObject("microformat")
|
|
|
|
.getObject("playerMicroformatRenderer")
|
|
|
|
.getBoolean("isUnlisted");
|
|
|
|
return isUnlisted ? Privacy.UNLISTED : Privacy.PUBLIC;
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
}
|
|
|
|
|
2020-02-25 09:07:22 +01:00
|
|
|
@Nonnull
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
@Override
|
2020-02-25 09:07:22 +01:00
|
|
|
public String getCategory() {
|
2020-02-09 11:59:23 +01:00
|
|
|
return playerResponse.getObject("microformat")
|
|
|
|
.getObject("playerMicroformatRenderer")
|
|
|
|
.getString("category");
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
}
|
|
|
|
|
2020-02-25 09:07:22 +01:00
|
|
|
@Nonnull
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
@Override
|
2020-02-09 11:59:23 +01:00
|
|
|
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";
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2020-02-25 09:07:22 +01:00
|
|
|
public Locale getLanguageInfo() {
|
2020-01-25 13:16:42 +01:00
|
|
|
return null;
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Nonnull
|
|
|
|
@Override
|
2020-02-25 09:07:22 +01:00
|
|
|
public List<String> getTags() {
|
2020-02-09 11:59:23 +01:00
|
|
|
return JsonUtils.getStringListFromJsonArray(playerResponse.getObject("videoDetails").getArray("keywords"));
|
added metadata, fix descriptions, fix thumbnail, update tests
thumbnail: quality before: https://peertube.cpy.re/static/thumbnails/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
quality after: https://peertube.cpy.re/static/previews/d2a5ec78-5f85-4090-8ec5-dc1102e022ea.jpg
description: we were getting about the first 260 characters, we now get full description (with fallback to first 260 chars if the get request for full description fails)
test: updated tests to match description, also changed some test: it was assertEquals(extracted, expected), but the proper way to do it is assertEquals(expected, extracted)
metadata: got host, privacy (public, private, unlisted), licence, language, tags
2020-01-19 12:45:52 +01:00
|
|
|
}
|
2020-01-23 14:19:22 +01:00
|
|
|
|
|
|
|
@Nonnull
|
|
|
|
@Override
|
2020-02-25 09:07:22 +01:00
|
|
|
public String getSupportInfo() {
|
2020-01-23 14:19:22 +01:00
|
|
|
return "";
|
|
|
|
}
|
2020-12-12 10:24:29 +01:00
|
|
|
|
|
|
|
@Nonnull
|
|
|
|
@Override
|
|
|
|
public List<StreamSegment> getStreamSegments() throws ParsingException {
|
|
|
|
final ArrayList<StreamSegment> segments = new ArrayList<>();
|
|
|
|
if (initialData.has("engagementPanels")) {
|
|
|
|
final JsonArray panels = initialData.getArray("engagementPanels");
|
|
|
|
JsonArray segmentsArray = null;
|
|
|
|
|
|
|
|
// Search for correct panel containing the data
|
|
|
|
for (int i = 0; i < panels.size(); i++) {
|
2021-02-03 22:07:34 +01:00
|
|
|
final String panelIdentifier = panels.getObject(i).getObject("engagementPanelSectionListRenderer")
|
|
|
|
.getString("panelIdentifier");
|
|
|
|
if (panelIdentifier.equals("engagement-panel-macro-markers-description-chapters")
|
|
|
|
|| panelIdentifier.equals("engagement-panel-macro-markers")) {
|
2020-12-12 10:24:29 +01:00
|
|
|
segmentsArray = panels.getObject(i).getObject("engagementPanelSectionListRenderer")
|
|
|
|
.getObject("content").getObject("macroMarkersListRenderer").getArray("contents");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (segmentsArray != null) {
|
|
|
|
final long duration = getLength();
|
|
|
|
for (final Object object : segmentsArray) {
|
|
|
|
final JsonObject segmentJson = ((JsonObject) object).getObject("macroMarkersListItemRenderer");
|
|
|
|
|
|
|
|
final int startTimeSeconds = segmentJson.getObject("onTap").getObject("watchEndpoint")
|
|
|
|
.getInt("startTimeSeconds", -1);
|
|
|
|
|
|
|
|
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");
|
|
|
|
if (!previewsArray.isEmpty()) {
|
|
|
|
// Assume that the thumbnail with the highest resolution is at the last position
|
2020-12-12 15:00:45 +01:00
|
|
|
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;
|
|
|
|
}
|
2020-12-20 19:54:12 +01:00
|
|
|
|
|
|
|
@Nonnull
|
|
|
|
@Override
|
|
|
|
public List<MetaInfo> getMetaInfo() throws ParsingException {
|
|
|
|
return YoutubeParsingHelper.getMetaInfo(
|
|
|
|
initialData.getObject("contents").getObject("twoColumnWatchNextResults")
|
2021-02-07 22:42:21 +01:00
|
|
|
.getObject("results").getObject("results").getArray("contents"));
|
2020-12-20 19:54:12 +01:00
|
|
|
}
|
2017-03-01 18:47:52 +01:00
|
|
|
}
|