Cleanup ``YoutubeStreamExtractor`` and some related classes

* Fixed obvious sonar(lint) warnings
* Abstracted some code (get*Streams)
* Used some new lines to make code better readable
* Chopped down brace-jungle in some methods
* Use StandardCharset (Java 8 4tw)
This commit is contained in:
litetex 2022-05-01 16:38:05 +02:00
parent 52fa2d939a
commit fe30eb43a9
3 changed files with 394 additions and 353 deletions

View File

@ -16,7 +16,6 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareIosMobileJsonBuilder; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareIosMobileJsonBuilder;
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonArray;
@ -65,6 +64,7 @@ import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@ -77,6 +77,8 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -187,7 +189,8 @@ public class YoutubeStreamExtractor extends StreamExtractor {
return playerMicroFormatRenderer.getString("uploadDate"); return playerMicroFormatRenderer.getString("uploadDate");
} else if (!playerMicroFormatRenderer.getString("publishDate", EMPTY_STRING).isEmpty()) { } else if (!playerMicroFormatRenderer.getString("publishDate", EMPTY_STRING).isEmpty()) {
return playerMicroFormatRenderer.getString("publishDate"); return playerMicroFormatRenderer.getString("publishDate");
} else { }
final JsonObject liveDetails = playerMicroFormatRenderer.getObject( final JsonObject liveDetails = playerMicroFormatRenderer.getObject(
"liveBroadcastDetails"); "liveBroadcastDetails");
if (!liveDetails.getString("endTimestamp", EMPTY_STRING).isEmpty()) { if (!liveDetails.getString("endTimestamp", EMPTY_STRING).isEmpty()) {
@ -200,7 +203,6 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// this should never be reached, but a live stream without upload date is valid // this should never be reached, but a live stream without upload date is valid
return null; return null;
} }
}
if (getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText")) if (getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText"))
.startsWith("Premiered")) { .startsWith("Premiered")) {
@ -259,10 +261,14 @@ public class YoutubeStreamExtractor extends StreamExtractor {
public String getThumbnailUrl() throws ParsingException { public String getThumbnailUrl() throws ParsingException {
assertPageFetched(); assertPageFetched();
try { try {
final JsonArray thumbnails = playerResponse.getObject("videoDetails") final JsonArray thumbnails = playerResponse
.getObject("thumbnail").getArray("thumbnails"); .getObject("videoDetails")
.getObject("thumbnail")
.getArray("thumbnails");
// the last thumbnail is the one with the highest resolution // the last thumbnail is the one with the highest resolution
final String url = thumbnails.getObject(thumbnails.size() - 1).getString("url"); final String url = thumbnails
.getObject(thumbnails.size() - 1)
.getString("url");
return fixThumbnailUrl(url); return fixThumbnailUrl(url);
} catch (final Exception e) { } catch (final Exception e) {
@ -277,8 +283,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
assertPageFetched(); assertPageFetched();
// Description with more info on links // Description with more info on links
try { try {
final String description = getTextFromObject(getVideoSecondaryInfoRenderer() final String description = getTextFromObject(
.getObject("description"), true); getVideoSecondaryInfoRenderer().getObject("description"),
true);
if (!isNullOrEmpty(description)) { if (!isNullOrEmpty(description)) {
return new Description(description, Description.HTML); return new Description(description, Description.HTML);
} }
@ -299,27 +306,35 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Override @Override
public int getAgeLimit() throws ParsingException { public int getAgeLimit() throws ParsingException {
if (ageLimit == -1) { if (ageLimit != -1) {
ageLimit = NO_AGE_LIMIT;
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; return ageLimit;
} }
}
} final boolean ageRestricted = getVideoSecondaryInfoRenderer()
} .getObject("metadataRowContainer")
} .getObject("metadataRowContainerRenderer")
.getArray("rows")
.stream()
// Only JsonObjects allowed
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.flatMap(metadataRow -> metadataRow
.getObject("metadataRowRenderer")
.getArray("contents")
.stream()
// Only JsonObjects allowed
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast))
.flatMap(content -> content
.getArray("runs")
.stream()
// Only JsonObjects allowed
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast))
.map(run -> run.getString("text", EMPTY_STRING))
.anyMatch(rowText -> rowText.contains("Age-restricted"));
ageLimit = ageRestricted ? 18 : NO_AGE_LIMIT;
return ageLimit; return ageLimit;
} }
@ -370,9 +385,8 @@ public class YoutubeStreamExtractor extends StreamExtractor {
if (timestamp == -2) { if (timestamp == -2) {
// Regex for timestamp was not found // Regex for timestamp was not found
return 0; return 0;
} else {
return timestamp;
} }
return timestamp;
} }
@Override @Override
@ -476,10 +490,11 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Override @Override
public boolean isUploaderVerified() throws ParsingException { public boolean isUploaderVerified() throws ParsingException {
final JsonArray badges = getVideoSecondaryInfoRenderer().getObject("owner") return YoutubeParsingHelper.isVerified(
.getObject("videoOwnerRenderer").getArray("badges"); getVideoSecondaryInfoRenderer()
.getObject("owner")
return YoutubeParsingHelper.isVerified(badges); .getObject("videoOwnerRenderer")
.getArray("badges"));
} }
@Nonnull @Nonnull
@ -490,9 +505,13 @@ public class YoutubeStreamExtractor extends StreamExtractor {
String url = null; String url = null;
try { try {
url = getVideoSecondaryInfoRenderer().getObject("owner") url = getVideoSecondaryInfoRenderer()
.getObject("videoOwnerRenderer").getObject("thumbnail") .getObject("owner")
.getArray("thumbnails").getObject(0).getString("url"); .getObject("videoOwnerRenderer")
.getObject("thumbnail")
.getArray("thumbnails")
.getObject(0)
.getString("url");
} catch (final ParsingException ignored) { } catch (final ParsingException ignored) {
// Age-restricted videos cause a ParsingException here // Age-restricted videos cause a ParsingException here
} }
@ -530,8 +549,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// There is no DASH manifest available in the iOS clients and the DASH manifest of the // There is no DASH manifest available in the iOS clients and the DASH manifest of the
// Android client doesn't contain all available streams (mainly the WEBM ones) // Android client doesn't contain all available streams (mainly the WEBM ones)
return getManifestUrl("dash", Arrays.asList(html5StreamingData, return getManifestUrl(
androidStreamingData)); "dash",
Arrays.asList(html5StreamingData, androidStreamingData));
} }
@Nonnull @Nonnull
@ -542,93 +562,91 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// Return HLS manifest of the iOS client first because on livestreams, the HLS manifest // Return HLS manifest of the iOS client first because on livestreams, the HLS manifest
// returned has separated audio and video streams // returned has separated audio and video streams
// Also, on videos, non-iOS clients don't have an HLS manifest URL in their player response // Also, on videos, non-iOS clients don't have an HLS manifest URL in their player response
return getManifestUrl("hls", Arrays.asList(iosStreamingData, html5StreamingData, return getManifestUrl(
androidStreamingData)); "hls",
Arrays.asList(iosStreamingData, html5StreamingData, androidStreamingData));
} }
@Nonnull @Nonnull
private static String getManifestUrl(@Nonnull final String manifestType, private static String getManifestUrl(@Nonnull final String manifestType,
@Nonnull final List<JsonObject> streamingDataObjects) { @Nonnull final List<JsonObject> streamingDataObjects) {
final String manifestKey = manifestType + "ManifestUrl"; final String manifestKey = manifestType + "ManifestUrl";
for (final JsonObject streamingDataObject : streamingDataObjects) {
if (streamingDataObject != null) { return streamingDataObjects.stream()
final String manifestKeyValue = streamingDataObject.getString(manifestKey); .filter(Objects::nonNull)
if (manifestKeyValue != null) { .map(streamingDataObject -> streamingDataObject.getString(manifestKey))
return manifestKeyValue; .filter(Objects::nonNull)
} .findFirst()
} .orElse(EMPTY_STRING);
} }
return EMPTY_STRING; @FunctionalInterface
interface StreamTypeStreamBuilderHelper<T extends Stream> {
T buildStream(String url, ItagItem itagItem);
}
/**
* Abstract method for
* {@link #getAudioStreams()}, {@link #getVideoOnlyStreams()} and {@link #getVideoStreams()}.
*
* @param itags A map of Urls + ItagItems
* @param streamBuilder Builds the stream from the provided data
* @param exMsgStreamType Stream type inside the exception message e.g. "video streams"
* @param <T> Type of the stream
* @return
* @throws ExtractionException
*/
private <T extends Stream> List<T> getStreamsByType(
final Map<String, ItagItem> itags,
final StreamTypeStreamBuilderHelper<T> streamBuilder,
final String exMsgStreamType
) throws ExtractionException {
final List<T> streams = new ArrayList<>();
try {
for (final Map.Entry<String, ItagItem> entry : itags.entrySet()) {
final String url = tryDecryptUrl(entry.getKey(), getId());
final T stream = streamBuilder.buildStream(url, entry.getValue());
if (!Stream.containSimilarStream(stream, streams)) {
streams.add(stream);
}
}
} catch (final Exception e) {
throw new ParsingException("Could not get " + exMsgStreamType, e);
}
return streams;
} }
@Override @Override
public List<AudioStream> getAudioStreams() throws ExtractionException { public List<AudioStream> getAudioStreams() throws ExtractionException {
assertPageFetched(); assertPageFetched();
final List<AudioStream> audioStreams = new ArrayList<>(); return getStreamsByType(
getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.AUDIO),
try { AudioStream::new,
for (final Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS, "audio streams"
ItagItem.ItagType.AUDIO).entrySet()) { );
final ItagItem itag = entry.getValue();
final String url = tryDecryption(entry.getKey(), getId());
final AudioStream audioStream = new AudioStream(url, itag);
if (!Stream.containSimilarStream(audioStream, audioStreams)) {
audioStreams.add(audioStream);
}
}
} catch (final Exception e) {
throw new ParsingException("Could not get audio streams", e);
}
return audioStreams;
} }
@Override @Override
public List<VideoStream> getVideoStreams() throws ExtractionException { public List<VideoStream> getVideoStreams() throws ExtractionException {
assertPageFetched(); assertPageFetched();
final List<VideoStream> videoStreams = new ArrayList<>(); return getStreamsByType(
getItags(FORMATS, ItagItem.ItagType.VIDEO),
try { (url, itag) -> new VideoStream(url, false, itag),
for (final Map.Entry<String, ItagItem> entry : getItags(FORMATS, "video streams"
ItagItem.ItagType.VIDEO).entrySet()) { );
final ItagItem itag = entry.getValue();
final String url = tryDecryption(entry.getKey(), getId());
final VideoStream videoStream = new VideoStream(url, false, itag);
if (!Stream.containSimilarStream(videoStream, videoStreams)) {
videoStreams.add(videoStream);
}
}
} catch (final Exception e) {
throw new ParsingException("Could not get video streams", e);
}
return videoStreams;
} }
@Override @Override
public List<VideoStream> getVideoOnlyStreams() throws ExtractionException { public List<VideoStream> getVideoOnlyStreams() throws ExtractionException {
assertPageFetched(); assertPageFetched();
final List<VideoStream> videoOnlyStreams = new ArrayList<>(); return getStreamsByType(
getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.VIDEO_ONLY),
try { (url, itag) -> new VideoStream(url, true, itag),
for (final Map.Entry<String, ItagItem> entry : getItags(ADAPTIVE_FORMATS, "video only streams"
ItagItem.ItagType.VIDEO_ONLY).entrySet()) { );
final ItagItem itag = entry.getValue();
final String url = tryDecryption(entry.getKey(), getId());
final VideoStream videoStream = new VideoStream(url, true, itag);
if (!Stream.containSimilarStream(videoStream, videoOnlyStreams)) {
videoOnlyStreams.add(videoStream);
}
}
} catch (final Exception e) {
throw new ParsingException("Could not get video only streams", e);
}
return videoOnlyStreams;
} }
/** /**
@ -636,7 +654,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
* always needed. * always needed.
* This way a breaking change from YouTube does not result in a broken extractor. * This way a breaking change from YouTube does not result in a broken extractor.
*/ */
private String tryDecryption(final String url, final String videoId) { private String tryDecryptUrl(final String url, final String videoId) {
try { try {
return YoutubeThrottlingDecrypter.apply(url, videoId); return YoutubeThrottlingDecrypter.apply(url, videoId);
} catch (final ParsingException e) { } catch (final ParsingException e) {
@ -713,25 +731,33 @@ public class YoutubeStreamExtractor extends StreamExtractor {
try { try {
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId()); final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
final JsonArray results = nextResponse.getObject("contents") final JsonArray results = nextResponse
.getObject("twoColumnWatchNextResults").getObject("secondaryResults") .getObject("contents")
.getObject("secondaryResults").getArray("results"); .getObject("twoColumnWatchNextResults")
.getObject("secondaryResults")
.getObject("secondaryResults")
.getArray("results");
final TimeAgoParser timeAgoParser = getTimeAgoParser(); final TimeAgoParser timeAgoParser = getTimeAgoParser();
results.stream()
for (final Object resultObject : results) { .filter(JsonObject.class::isInstance)
final JsonObject result = (JsonObject) resultObject; .map(JsonObject.class::cast)
.map(result -> {
if (result.has("compactVideoRenderer")) { if (result.has("compactVideoRenderer")) {
collector.commit(new YoutubeStreamInfoItemExtractor( return new YoutubeStreamInfoItemExtractor(
result.getObject("compactVideoRenderer"), timeAgoParser)); result.getObject("compactVideoRenderer"), timeAgoParser);
} else if (result.has("compactRadioRenderer")) { } else if (result.has("compactRadioRenderer")) {
collector.commit(new YoutubeMixOrPlaylistInfoItemExtractor( return new YoutubeMixOrPlaylistInfoItemExtractor(
result.getObject("compactRadioRenderer"))); result.getObject("compactRadioRenderer"));
} else if (result.has("compactPlaylistRenderer")) { } else if (result.has("compactPlaylistRenderer")) {
collector.commit(new YoutubeMixOrPlaylistInfoItemExtractor( return new YoutubeMixOrPlaylistInfoItemExtractor(
result.getObject("compactPlaylistRenderer"))); result.getObject("compactPlaylistRenderer"));
}
} }
return null;
})
.filter(Objects::nonNull)
.forEach(collector::commit);
return collector; return collector;
} catch (final Exception e) { } catch (final Exception e) {
throw new ParsingException("Could not get related videos", e); throw new ParsingException("Could not get related videos", e);
@ -777,9 +803,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Override @Override
public void onFetchPage(@Nonnull final Downloader downloader) public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException { throws IOException, ExtractionException {
if (sts == null) { initStsFromPlayerJsIfNeeded();
getStsFromPlayerJs();
}
final String videoId = getId(); final String videoId = getId();
final Localization localization = getExtractorLocalization(); final Localization localization = getExtractorLocalization();
@ -833,13 +857,13 @@ public class YoutubeStreamExtractor extends StreamExtractor {
playerMicroFormatRenderer = youtubePlayerResponse.getObject("microformat") playerMicroFormatRenderer = youtubePlayerResponse.getObject("microformat")
.getObject("playerMicroformatRenderer"); .getObject("playerMicroformatRenderer");
final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder(localization, final byte[] body = JsonWriter.string(
contentCountry) prepareDesktopJsonBuilder(localization, contentCountry)
.value(VIDEO_ID, videoId) .value(VIDEO_ID, videoId)
.value(CONTENT_CHECK_OK, true) .value(CONTENT_CHECK_OK, true)
.value(RACY_CHECK_OK, true) .value(RACY_CHECK_OK, true)
.done()) .done())
.getBytes(UTF_8); .getBytes(StandardCharsets.UTF_8);
nextResponse = getJsonPostResponse(NEXT, body, localization); nextResponse = getJsonPostResponse(NEXT, body, localization);
if ((!ageRestricted && streamType == StreamType.VIDEO_STREAM) if ((!ageRestricted && streamType == StreamType.VIDEO_STREAM)
@ -863,11 +887,14 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nonnull final JsonObject playabilityStatus) @Nonnull final JsonObject playabilityStatus)
throws ParsingException { throws ParsingException {
String status = playabilityStatus.getString("status"); String status = playabilityStatus.getString("status");
if (status == null || status.equalsIgnoreCase("ok")) {
return;
}
// If status exist, and is not "OK", throw the specific exception based on error message // 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. // or a ContentNotAvailableException with the reason text if it's an unknown reason.
if (status != null && !status.equalsIgnoreCase("ok")) { final JsonObject newPlayabilityStatus =
final JsonObject newPlayabilityStatus youtubePlayerResponse.getObject("playabilityStatus");
= youtubePlayerResponse.getObject("playabilityStatus");
status = newPlayabilityStatus.getString("status"); status = newPlayabilityStatus.getString("status");
final String reason = newPlayabilityStatus.getString("reason"); final String reason = newPlayabilityStatus.getString("reason");
@ -899,7 +926,8 @@ public class YoutubeStreamExtractor extends StreamExtractor {
if (reason.contains("unavailable")) { if (reason.contains("unavailable")) {
final String detailedErrorMessage = getTextFromObject(newPlayabilityStatus final String detailedErrorMessage = getTextFromObject(newPlayabilityStatus
.getObject("errorScreen").getObject("playerErrorMessageRenderer") .getObject("errorScreen")
.getObject("playerErrorMessageRenderer")
.getObject("subreason")); .getObject("subreason"));
if (detailedErrorMessage != null && detailedErrorMessage.contains("country")) { if (detailedErrorMessage != null && detailedErrorMessage.contains("country")) {
throw new GeographicRestrictionException( throw new GeographicRestrictionException(
@ -910,7 +938,6 @@ public class YoutubeStreamExtractor extends StreamExtractor {
throw new ContentNotAvailableException("Got error: \"" + reason + "\""); throw new ContentNotAvailableException("Got error: \"" + reason + "\"");
} }
}
/** /**
* Fetch the Android Mobile API and assign the streaming data to the androidStreamingData JSON * Fetch the Android Mobile API and assign the streaming data to the androidStreamingData JSON
@ -921,14 +948,14 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nonnull final String videoId) @Nonnull final String videoId)
throws IOException, ExtractionException { throws IOException, ExtractionException {
androidCpn = generateContentPlaybackNonce(); androidCpn = generateContentPlaybackNonce();
final byte[] mobileBody = JsonWriter.string(prepareAndroidMobileJsonBuilder( final byte[] mobileBody = JsonWriter.string(
localization, contentCountry) prepareAndroidMobileJsonBuilder(localization, contentCountry)
.value(VIDEO_ID, videoId) .value(VIDEO_ID, videoId)
.value(CPN, androidCpn) .value(CPN, androidCpn)
.value(CONTENT_CHECK_OK, true) .value(CONTENT_CHECK_OK, true)
.value(RACY_CHECK_OK, true) .value(RACY_CHECK_OK, true)
.done()) .done())
.getBytes(UTF_8); .getBytes(StandardCharsets.UTF_8);
final JsonObject androidPlayerResponse = getJsonAndroidPostResponse(PLAYER, final JsonObject androidPlayerResponse = getJsonAndroidPostResponse(PLAYER,
mobileBody, localization, "&t=" + generateTParameter() mobileBody, localization, "&t=" + generateTParameter()
@ -952,14 +979,14 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nonnull final String videoId) @Nonnull final String videoId)
throws IOException, ExtractionException { throws IOException, ExtractionException {
iosCpn = generateContentPlaybackNonce(); iosCpn = generateContentPlaybackNonce();
final byte[] mobileBody = JsonWriter.string(prepareIosMobileJsonBuilder( final byte[] mobileBody = JsonWriter.string(
localization, contentCountry) prepareIosMobileJsonBuilder(localization, contentCountry)
.value(VIDEO_ID, videoId) .value(VIDEO_ID, videoId)
.value(CPN, iosCpn) .value(CPN, iosCpn)
.value(CONTENT_CHECK_OK, true) .value(CONTENT_CHECK_OK, true)
.value(RACY_CHECK_OK, true) .value(RACY_CHECK_OK, true)
.done()) .done())
.getBytes(UTF_8); .getBytes(StandardCharsets.UTF_8);
final JsonObject iosPlayerResponse = getJsonIosPostResponse(PLAYER, final JsonObject iosPlayerResponse = getJsonIosPostResponse(PLAYER,
mobileBody, localization, "&t=" + generateTParameter() mobileBody, localization, "&t=" + generateTParameter()
@ -987,9 +1014,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nonnull final Localization localization, @Nonnull final Localization localization,
@Nonnull final String videoId) @Nonnull final String videoId)
throws IOException, ExtractionException { throws IOException, ExtractionException {
if (sts == null) { initStsFromPlayerJsIfNeeded();
getStsFromPlayerJs();
}
// Because a cpn is unique to each request, we need to generate it again // Because a cpn is unique to each request, we need to generate it again
html5Cpn = generateContentPlaybackNonce(); html5Cpn = generateContentPlaybackNonce();
@ -1072,7 +1097,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
return cachedDeobfuscationCode; return cachedDeobfuscationCode;
} }
private static void getStsFromPlayerJs() throws ParsingException { private static void initStsFromPlayerJsIfNeeded() throws ParsingException {
if (!isNullOrEmpty(sts)) { if (!isNullOrEmpty(sts)) {
return; return;
} }
@ -1134,31 +1159,28 @@ public class YoutubeStreamExtractor extends StreamExtractor {
return theVideoPrimaryInfoRenderer; return theVideoPrimaryInfoRenderer;
} }
@Nonnull
private JsonObject getVideoSecondaryInfoRenderer() throws ParsingException { private JsonObject getVideoSecondaryInfoRenderer() throws ParsingException {
if (videoSecondaryInfoRenderer != null) { if (videoSecondaryInfoRenderer != null) {
return videoSecondaryInfoRenderer; return videoSecondaryInfoRenderer;
} }
final JsonArray contents = nextResponse.getObject("contents") videoSecondaryInfoRenderer = nextResponse
.getObject("twoColumnWatchNextResults").getObject("results").getObject("results") .getObject("contents")
.getArray("contents"); .getObject("twoColumnWatchNextResults")
.getObject("results")
.getObject("results")
.getArray("contents")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.filter(content -> content.has("videoSecondaryInfoRenderer"))
.map(content -> content.getObject("videoSecondaryInfoRenderer"))
.findFirst()
.orElseThrow(
() -> new ParsingException("Could not find videoSecondaryInfoRenderer"));
JsonObject theVideoSecondaryInfoRenderer = null; return videoSecondaryInfoRenderer;
for (final Object content : contents) {
if (((JsonObject) content).has("videoSecondaryInfoRenderer")) {
theVideoSecondaryInfoRenderer = ((JsonObject) content)
.getObject("videoSecondaryInfoRenderer");
break;
}
}
if (isNullOrEmpty(theVideoSecondaryInfoRenderer)) {
throw new ParsingException("Could not find videoSecondaryInfoRenderer");
}
videoSecondaryInfoRenderer = theVideoSecondaryInfoRenderer;
return theVideoSecondaryInfoRenderer;
} }
@Nonnull @Nonnull
@ -1194,29 +1216,35 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nonnull final String streamingDataKey, @Nonnull final String streamingDataKey,
@Nonnull final ItagItem.ItagType itagTypeWanted, @Nonnull final ItagItem.ItagType itagTypeWanted,
@Nonnull final String contentPlaybackNonce) { @Nonnull final String contentPlaybackNonce) {
if (streamingData == null || !streamingData.has(streamingDataKey)) {
return Collections.emptyMap();
}
final Map<String, ItagItem> urlAndItagsFromStreamingDataObject = new LinkedHashMap<>(); final Map<String, ItagItem> urlAndItagsFromStreamingDataObject = new LinkedHashMap<>();
if (streamingData != null && streamingData.has(streamingDataKey)) {
final JsonArray formats = streamingData.getArray(streamingDataKey); final JsonArray formats = streamingData.getArray(streamingDataKey);
for (int i = 0; i != formats.size(); ++i) { for (int i = 0; i < formats.size(); i++) {
final JsonObject formatData = formats.getObject(i); final JsonObject formatData = formats.getObject(i);
final int itag = formatData.getInt("itag"); final int itag = formatData.getInt("itag");
if (ItagItem.isSupported(itag)) { if (!ItagItem.isSupported(itag)) {
continue;
}
try { try {
final ItagItem itagItem = ItagItem.getItag(itag); final ItagItem itagItem = ItagItem.getItag(itag);
if (itagItem.itagType == itagTypeWanted) { if (itagItem.itagType != itagTypeWanted) {
continue;
}
// Ignore streams that are delivered using YouTube's OTF format, // Ignore streams that are delivered using YouTube's OTF format,
// as those only work with DASH and not with progressive HTTP. // as those only work with DASH and not with progressive HTTP.
if (formatData.getString("type", EMPTY_STRING) if ("FORMAT_STREAM_TYPE_OTF".equalsIgnoreCase(formatData.getString("type"))) {
.equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF")) {
continue; continue;
} }
final String streamUrl; final String streamUrl;
if (formatData.has("url")) { if (formatData.has("url")) {
streamUrl = formatData.getString("url") + "&cpn=" streamUrl = formatData.getString("url");
+ contentPlaybackNonce;
} else { } else {
// This url has an obfuscated signature // This url has an obfuscated signature
final String cipherString = formatData.has("cipher") final String cipherString = formatData.has("cipher")
@ -1232,45 +1260,38 @@ public class YoutubeStreamExtractor extends StreamExtractor {
final JsonObject indexRange = formatData.getObject("indexRange"); final JsonObject indexRange = formatData.getObject("indexRange");
final String mimeType = formatData.getString("mimeType", EMPTY_STRING); final String mimeType = formatData.getString("mimeType", EMPTY_STRING);
final String codec = mimeType.contains("codecs") final String codec = mimeType.contains("codecs")
? mimeType.split("\"")[1] : EMPTY_STRING; ? mimeType.split("\"")[1]
: EMPTY_STRING;
itagItem.setBitrate(formatData.getInt("bitrate")); itagItem.setBitrate(formatData.getInt("bitrate"));
itagItem.setWidth(formatData.getInt("width")); itagItem.setWidth(formatData.getInt("width"));
itagItem.setHeight(formatData.getInt("height")); itagItem.setHeight(formatData.getInt("height"));
itagItem.setInitStart(Integer.parseInt(initRange.getString("start", itagItem.setInitStart(Integer.parseInt(initRange.getString("start", "-1")));
"-1"))); itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", "-1")));
itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", "-1")));
"-1"))); itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", "-1")));
itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start",
"-1")));
itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end",
"-1")));
itagItem.fps = formatData.getInt("fps"); itagItem.fps = formatData.getInt("fps");
itagItem.setQuality(formatData.getString("quality")); itagItem.setQuality(formatData.getString("quality"));
itagItem.setCodec(codec); itagItem.setCodec(codec);
urlAndItagsFromStreamingDataObject.put(streamUrl, itagItem); urlAndItagsFromStreamingDataObject.put(streamUrl, itagItem);
}
} catch (final UnsupportedEncodingException | ParsingException ignored) { } catch (final UnsupportedEncodingException | ParsingException ignored) {
} }
} }
}
}
return urlAndItagsFromStreamingDataObject; return urlAndItagsFromStreamingDataObject;
} }
@Nonnull @Nonnull
@Override @Override
public List<Frameset> getFrames() throws ExtractionException { public List<Frameset> getFrames() throws ExtractionException {
try { try {
final JsonObject storyboards = playerResponse.getObject("storyboards"); final JsonObject storyboards = playerResponse.getObject("storyboards");
final JsonObject storyboardsRenderer; final JsonObject storyboardsRenderer = storyboards.getObject(
if (storyboards.has("playerLiveStoryboardSpecRenderer")) { storyboards.has("playerLiveStoryboardSpecRenderer")
storyboardsRenderer = storyboards.getObject("playerLiveStoryboardSpecRenderer"); ? "playerLiveStoryboardSpecRenderer"
} else { : "playerStoryboardSpecRenderer"
storyboardsRenderer = storyboards.getObject("playerStoryboardSpecRenderer"); );
}
if (storyboardsRenderer == null) { if (storyboardsRenderer == null) {
return Collections.emptyList(); return Collections.emptyList();
@ -1283,15 +1304,13 @@ public class YoutubeStreamExtractor extends StreamExtractor {
final String[] spec = storyboardsRendererSpec.split("\\|"); final String[] spec = storyboardsRendererSpec.split("\\|");
final String url = spec[0]; final String url = spec[0];
final ArrayList<Frameset> result = new ArrayList<>(spec.length - 1); final List<Frameset> result = new ArrayList<>(spec.length - 1);
for (int i = 1; i < spec.length; ++i) { for (int i = 1; i < spec.length; ++i) {
final String[] parts = spec[i].split("#"); final String[] parts = spec[i].split("#");
if (parts.length != 8 || Integer.parseInt(parts[5]) == 0) { if (parts.length != 8 || Integer.parseInt(parts[5]) == 0) {
continue; continue;
} }
final int frameWidth = Integer.parseInt(parts[0]);
final int frameHeight = Integer.parseInt(parts[1]);
final int totalCount = Integer.parseInt(parts[2]); final int totalCount = Integer.parseInt(parts[2]);
final int framesPerPageX = Integer.parseInt(parts[3]); final int framesPerPageX = Integer.parseInt(parts[3]);
final int framesPerPageY = Integer.parseInt(parts[4]); final int framesPerPageY = Integer.parseInt(parts[4]);
@ -1310,15 +1329,14 @@ public class YoutubeStreamExtractor extends StreamExtractor {
} }
result.add(new Frameset( result.add(new Frameset(
urls, urls,
frameWidth, /*frameWidth=*/Integer.parseInt(parts[0]),
frameHeight, /*frameHeight=*/Integer.parseInt(parts[1]),
totalCount, totalCount,
Integer.parseInt(parts[5]), /*durationPerFrame=*/Integer.parseInt(parts[5]),
framesPerPageX, framesPerPageX,
framesPerPageY framesPerPageY
)); ));
} }
result.trimToSize();
return result; return result;
} catch (final Exception e) { } catch (final Exception e) {
throw new ExtractionException("Could not get frames", e); throw new ExtractionException("Could not get frames", e);
@ -1328,8 +1346,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nonnull @Nonnull
@Override @Override
public Privacy getPrivacy() { public Privacy getPrivacy() {
final boolean isUnlisted = playerMicroFormatRenderer.getBoolean("isUnlisted"); return playerMicroFormatRenderer.getBoolean("isUnlisted")
return isUnlisted ? Privacy.UNLISTED : Privacy.PUBLIC; ? Privacy.UNLISTED
: Privacy.PUBLIC;
} }
@Nonnull @Nonnull
@ -1342,14 +1361,18 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Override @Override
public String getLicence() throws ParsingException { public String getLicence() throws ParsingException {
final JsonObject metadataRowRenderer = getVideoSecondaryInfoRenderer() final JsonObject metadataRowRenderer = getVideoSecondaryInfoRenderer()
.getObject("metadataRowContainer").getObject("metadataRowContainerRenderer") .getObject("metadataRowContainer")
.getObject("metadataRowContainerRenderer")
.getArray("rows") .getArray("rows")
.getObject(0).getObject("metadataRowRenderer"); .getObject(0)
.getObject("metadataRowRenderer");
final JsonArray contents = metadataRowRenderer.getArray("contents"); final JsonArray contents = metadataRowRenderer.getArray("contents");
final String license = getTextFromObject(contents.getObject(0)); final String license = getTextFromObject(contents.getObject(0));
return license != null && "Licence".equals(getTextFromObject(metadataRowRenderer return license != null
.getObject("title"))) ? license : "YouTube licence"; && "Licence".equals(getTextFromObject(metadataRowRenderer.getObject("title")))
? license
: "YouTube licence";
} }
@Override @Override
@ -1367,32 +1390,43 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Nonnull @Nonnull
@Override @Override
public List<StreamSegment> getStreamSegments() throws ParsingException { public List<StreamSegment> getStreamSegments() throws ParsingException {
final ArrayList<StreamSegment> segments = new ArrayList<>();
if (nextResponse.has("engagementPanels")) {
final JsonArray panels = nextResponse.getArray("engagementPanels");
JsonArray segmentsArray = null;
// Search for correct panel containing the data if (!nextResponse.has("engagementPanels")) {
for (int i = 0; i < panels.size(); i++) { return Collections.emptyList();
final String panelIdentifier = panels.getObject(i) }
final JsonArray segmentsArray = nextResponse.getArray("engagementPanels")
.stream()
// Check if object is a JsonObject
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
// Check if the panel is the correct one
.filter(panel -> "engagement-panel-macro-markers-description-chapters".equals(
panel
.getObject("engagementPanelSectionListRenderer") .getObject("engagementPanelSectionListRenderer")
.getString("panelIdentifier"); .getString("panelIdentifier")))
// panelIdentifier might be null if the panel has something to do with ads // Extract the data
// See https://github.com/TeamNewPipe/NewPipe/issues/7792#issuecomment-1030900188 .map(panel -> panel
if ("engagement-panel-macro-markers-description-chapters".equals(panelIdentifier)) { .getObject("engagementPanelSectionListRenderer")
segmentsArray = panels.getObject(i) .getObject("content")
.getObject("engagementPanelSectionListRenderer").getObject("content") .getObject("macroMarkersListRenderer")
.getObject("macroMarkersListRenderer").getArray("contents"); .getArray("contents"))
break; .findFirst()
} .orElse(null);
// If no data was found exit
if (segmentsArray == null) {
return Collections.emptyList();
} }
if (segmentsArray != null) {
final long duration = getLength(); final long duration = getLength();
for (final Object object : segmentsArray) { final List<StreamSegment> segments = new ArrayList<>();
final JsonObject segmentJson = ((JsonObject) object) for (final JsonObject segmentJson : segmentsArray.stream()
.getObject("macroMarkersListItemRenderer"); .filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.map(object -> object.getObject("macroMarkersListItemRenderer"))
.collect(Collectors.toList())
) {
final int startTimeSeconds = segmentJson.getObject("onTap") final int startTimeSeconds = segmentJson.getObject("onTap")
.getObject("watchEndpoint").getInt("startTimeSeconds", -1); .getObject("watchEndpoint").getInt("startTimeSeconds", -1);
@ -1411,29 +1445,31 @@ public class YoutubeStreamExtractor extends StreamExtractor {
final StreamSegment segment = new StreamSegment(title, startTimeSeconds); final StreamSegment segment = new StreamSegment(title, startTimeSeconds);
segment.setUrl(getUrl() + "?t=" + startTimeSeconds); segment.setUrl(getUrl() + "?t=" + startTimeSeconds);
if (segmentJson.has("thumbnail")) { if (segmentJson.has("thumbnail")) {
final JsonArray previewsArray = segmentJson.getObject("thumbnail") final JsonArray previewsArray = segmentJson
.getObject("thumbnail")
.getArray("thumbnails"); .getArray("thumbnails");
if (!previewsArray.isEmpty()) { if (!previewsArray.isEmpty()) {
// Assume that the thumbnail with the highest resolution is at the // Assume that the thumbnail with the highest resolution is at the last position
// last position
final String url = previewsArray final String url = previewsArray
.getObject(previewsArray.size() - 1).getString("url"); .getObject(previewsArray.size() - 1)
.getString("url");
segment.setPreviewUrl(fixThumbnailUrl(url)); segment.setPreviewUrl(fixThumbnailUrl(url));
} }
} }
segments.add(segment); segments.add(segment);
} }
}
}
return segments; return segments;
} }
@Nonnull @Nonnull
@Override @Override
public List<MetaInfo> getMetaInfo() throws ParsingException { public List<MetaInfo> getMetaInfo() throws ParsingException {
return YoutubeParsingHelper.getMetaInfo( return YoutubeParsingHelper.getMetaInfo(nextResponse
nextResponse.getObject("contents").getObject("twoColumnWatchNextResults") .getObject("contents")
.getObject("results").getObject("results").getArray("contents")); .getObject("twoColumnWatchNextResults")
.getObject("results")
.getObject("results")
.getArray("contents"));
} }
/** /**

View File

@ -1,19 +1,19 @@
package org.schabi.newpipe.extractor.stream; package org.schabi.newpipe.extractor.stream;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
/** /**
* Creates a stream object from url, format and optional torrent url * Creates a stream object from url, format and optional torrent url
*/ */
public abstract class Stream implements Serializable { public abstract class Stream implements Serializable {
private final MediaFormat mediaFormat; private final MediaFormat mediaFormat;
public final String url; private final String url;
public final String torrentUrl; private final String torrentUrl;
/** /**
* @deprecated Use {@link #getFormat()} or {@link #getFormatId()} * @deprecated Use {@link #getFormat()} or {@link #getFormatId()}

View File

@ -2,8 +2,6 @@ package org.schabi.newpipe.extractor.utils;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
@ -18,10 +16,17 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public final class Utils { public final class Utils {
public static final String HTTP = "http://"; public static final String HTTP = "http://";
public static final String HTTPS = "https://"; public static final String HTTPS = "https://";
/**
* @deprecated Use {@link java.nio.charset.StandardCharsets#UTF_8}
*/
@Deprecated
public static final String UTF_8 = "UTF-8"; public static final String UTF_8 = "UTF-8";
public static final String EMPTY_STRING = ""; public static final String EMPTY_STRING = "";
private static final Pattern M_PATTERN = Pattern.compile("(https?)?:\\/\\/m\\."); private static final Pattern M_PATTERN = Pattern.compile("(https?)?:\\/\\/m\\.");