Use our fork of nanojson

This commit is contained in:
wb9688 2020-04-16 16:08:14 +02:00
parent 1b522304c1
commit 979c5a7502
22 changed files with 219 additions and 323 deletions

View File

@ -10,6 +10,7 @@ allprojects {
repositories { repositories {
jcenter() jcenter()
maven { url "https://jitpack.io" }
} }
} }

View File

@ -1,11 +1,11 @@
dependencies { dependencies {
implementation project(':timeago-parser') implementation project(':timeago-parser')
implementation 'com.grack:nanojson:1.1' implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'org.jsoup:jsoup:1.9.2' implementation 'org.jsoup:jsoup:1.9.2'
implementation 'org.mozilla:rhino:1.7.7.1' implementation 'org.mozilla:rhino:1.7.7.1'
implementation 'com.github.spotbugs:spotbugs-annotations:3.1.0' implementation 'com.github.spotbugs:spotbugs-annotations:3.1.0'
implementation 'org.nibor.autolink:autolink:0.8.0' implementation 'org.nibor.autolink:autolink:0.8.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
} }

View File

@ -109,7 +109,7 @@ public class PeertubeAccountExtractor extends ChannelExtractor {
public InfoItemsPage<StreamInfoItem> getPage(String pageUrl) throws IOException, ExtractionException { public InfoItemsPage<StreamInfoItem> getPage(String pageUrl) throws IOException, ExtractionException {
Response response = getDownloader().get(pageUrl); Response response = getDownloader().get(pageUrl);
JsonObject json = null; JsonObject json = null;
if (null != response && !StringUtil.isBlank(response.responseBody())) { if (response != null && !StringUtil.isBlank(response.responseBody())) {
try { try {
json = JsonParser.object().from(response.responseBody()); json = JsonParser.object().from(response.responseBody());
} catch (Exception e) { } catch (Exception e) {
@ -169,7 +169,7 @@ public class PeertubeAccountExtractor extends ChannelExtractor {
try { try {
json = JsonParser.object().from(responseBody); json = JsonParser.object().from(responseBody);
} catch (JsonParserException e) { } catch (JsonParserException e) {
throw new ExtractionException("Unable to extract peertube channel data", e); throw new ExtractionException("Unable to extract PeerTube channel data", e);
} }
if (json == null) throw new ExtractionException("Unable to extract PeerTube channel data"); if (json == null) throw new ExtractionException("Unable to extract PeerTube channel data");
} }

View File

@ -167,7 +167,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
assertPageFetched(); assertPageFetched();
List<VideoStream> videoStreams = new ArrayList<>(); List<VideoStream> videoStreams = new ArrayList<>();
try { try {
JsonArray streams = json.getArray("files", new JsonArray()); JsonArray streams = json.getArray("files");
for (Object s : streams) { for (Object s : streams) {
if (!(s instanceof JsonObject)) continue; if (!(s instanceof JsonObject)) continue;
JsonObject stream = (JsonObject) s; JsonObject stream = (JsonObject) s;

View File

@ -16,6 +16,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.io.IOException; import java.io.IOException;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public class SoundcloudChannelExtractor extends ChannelExtractor { public class SoundcloudChannelExtractor extends ChannelExtractor {
private String userId; private String userId;
@ -62,10 +64,7 @@ public class SoundcloudChannelExtractor extends ChannelExtractor {
@Override @Override
public String getBannerUrl() { public String getBannerUrl() {
return user.getObject("visuals", new JsonObject()) return user.getObject("visuals").getArray("visuals").getObject(0).getString("visual_url");
.getArray("visuals", new JsonArray())
.getObject(0, new JsonObject())
.getString("visual_url");
} }
@Override @Override
@ -80,7 +79,7 @@ public class SoundcloudChannelExtractor extends ChannelExtractor {
@Override @Override
public String getDescription() { public String getDescription() {
return user.getString("description", ""); return user.getString("description", EMPTY_STRING);
} }
@Nonnull @Nonnull

View File

@ -3,6 +3,7 @@ package org.schabi.newpipe.extractor.services.soundcloud;
import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor; import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps; import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
public class SoundcloudChannelInfoItemExtractor implements ChannelInfoItemExtractor { public class SoundcloudChannelInfoItemExtractor implements ChannelInfoItemExtractor {
@ -24,7 +25,7 @@ public class SoundcloudChannelInfoItemExtractor implements ChannelInfoItemExtrac
@Override @Override
public String getThumbnailUrl() { public String getThumbnailUrl() {
String avatarUrl = itemObject.getString("avatar_url", ""); String avatarUrl = itemObject.getString("avatar_url", EMPTY_STRING);
String avatarUrlBetterResolution = avatarUrl.replace("large.jpg", "crop.jpg"); String avatarUrlBetterResolution = avatarUrl.replace("large.jpg", "crop.jpg");
return avatarUrlBetterResolution; return avatarUrlBetterResolution;
} }
@ -41,6 +42,6 @@ public class SoundcloudChannelInfoItemExtractor implements ChannelInfoItemExtrac
@Override @Override
public String getDescription() { public String getDescription() {
return itemObject.getString("description", ""); return itemObject.getString("description", EMPTY_STRING);
} }
} }

View File

@ -28,6 +28,7 @@ import java.util.*;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps; import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
public class SoundcloudParsingHelper { public class SoundcloudParsingHelper {
@ -256,17 +257,17 @@ public class SoundcloudParsingHelper {
@Nonnull @Nonnull
static String getUploaderUrl(JsonObject object) { static String getUploaderUrl(JsonObject object) {
String url = object.getObject("user").getString("permalink_url", ""); String url = object.getObject("user").getString("permalink_url", EMPTY_STRING);
return replaceHttpWithHttps(url); return replaceHttpWithHttps(url);
} }
@Nonnull @Nonnull
static String getAvatarUrl(JsonObject object) { static String getAvatarUrl(JsonObject object) {
String url = object.getObject("user", new JsonObject()).getString("avatar_url", ""); String url = object.getObject("user").getString("avatar_url", EMPTY_STRING);
return replaceHttpWithHttps(url); return replaceHttpWithHttps(url);
} }
public static String getUploaderName(JsonObject object) { public static String getUploaderName(JsonObject object) {
return object.getObject("user").getString("username", ""); return object.getObject("user").getString("username", EMPTY_STRING);
} }
} }

View File

@ -4,6 +4,7 @@ import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps; import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
public class SoundcloudPlaylistInfoItemExtractor implements PlaylistInfoItemExtractor { public class SoundcloudPlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
@ -31,7 +32,7 @@ public class SoundcloudPlaylistInfoItemExtractor implements PlaylistInfoItemExtr
public String getThumbnailUrl() throws ParsingException { public String getThumbnailUrl() throws ParsingException {
// Over-engineering at its finest // Over-engineering at its finest
if (itemObject.isString(ARTWORK_URL_KEY)) { if (itemObject.isString(ARTWORK_URL_KEY)) {
final String artworkUrl = itemObject.getString(ARTWORK_URL_KEY, ""); final String artworkUrl = itemObject.getString(ARTWORK_URL_KEY, EMPTY_STRING);
if (!artworkUrl.isEmpty()) { if (!artworkUrl.isEmpty()) {
String artworkUrlBetterResolution = artworkUrl.replace("large.jpg", "crop.jpg"); String artworkUrlBetterResolution = artworkUrl.replace("large.jpg", "crop.jpg");
return artworkUrlBetterResolution; return artworkUrlBetterResolution;
@ -45,7 +46,7 @@ public class SoundcloudPlaylistInfoItemExtractor implements PlaylistInfoItemExtr
// First look for track artwork url // First look for track artwork url
if (trackObject.isString(ARTWORK_URL_KEY)) { if (trackObject.isString(ARTWORK_URL_KEY)) {
String artworkUrl = trackObject.getString(ARTWORK_URL_KEY, ""); String artworkUrl = trackObject.getString(ARTWORK_URL_KEY, EMPTY_STRING);
if (!artworkUrl.isEmpty()) { if (!artworkUrl.isEmpty()) {
String artworkUrlBetterResolution = artworkUrl.replace("large.jpg", "crop.jpg"); String artworkUrlBetterResolution = artworkUrl.replace("large.jpg", "crop.jpg");
return artworkUrlBetterResolution; return artworkUrlBetterResolution;
@ -53,8 +54,8 @@ public class SoundcloudPlaylistInfoItemExtractor implements PlaylistInfoItemExtr
} }
// Then look for track creator avatar url // Then look for track creator avatar url
final JsonObject creator = trackObject.getObject(USER_KEY, new JsonObject()); final JsonObject creator = trackObject.getObject(USER_KEY);
final String creatorAvatar = creator.getString(AVATAR_URL_KEY, ""); final String creatorAvatar = creator.getString(AVATAR_URL_KEY, EMPTY_STRING);
if (!creatorAvatar.isEmpty()) return creatorAvatar; if (!creatorAvatar.isEmpty()) return creatorAvatar;
} }
} catch (Exception ignored) { } catch (Exception ignored) {
@ -63,7 +64,7 @@ public class SoundcloudPlaylistInfoItemExtractor implements PlaylistInfoItemExtr
try { try {
// Last resort, use user avatar url. If still not found, then throw exception. // Last resort, use user avatar url. If still not found, then throw exception.
return itemObject.getObject(USER_KEY).getString(AVATAR_URL_KEY, ""); return itemObject.getObject(USER_KEY).getString(AVATAR_URL_KEY, EMPTY_STRING);
} catch (Exception e) { } catch (Exception e) {
throw new ParsingException("Failed to extract playlist thumbnail url", e); throw new ParsingException("Failed to extract playlist thumbnail url", e);
} }

View File

@ -23,6 +23,7 @@ import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudSearchQueryHandlerFactory.ITEMS_PER_PAGE; import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudSearchQueryHandlerFactory.ITEMS_PER_PAGE;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
public class SoundcloudSearchExtractor extends SearchExtractor { public class SoundcloudSearchExtractor extends SearchExtractor {
@ -84,7 +85,7 @@ public class SoundcloudSearchExtractor extends SearchExtractor {
if (!(result instanceof JsonObject)) continue; if (!(result instanceof JsonObject)) continue;
//noinspection ConstantConditions //noinspection ConstantConditions
JsonObject searchResult = (JsonObject) result; JsonObject searchResult = (JsonObject) result;
String kind = searchResult.getString("kind", ""); String kind = searchResult.getString("kind", EMPTY_STRING);
switch (kind) { switch (kind) {
case "user": case "user":
collector.commit(new SoundcloudChannelInfoItemExtractor(searchResult)); collector.commit(new SoundcloudChannelInfoItemExtractor(searchResult));

View File

@ -34,6 +34,8 @@ import java.util.Locale;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
public class SoundcloudStreamExtractor extends StreamExtractor { public class SoundcloudStreamExtractor extends StreamExtractor {
private JsonObject track; private JsonObject track;
@ -45,7 +47,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException { public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException {
track = SoundcloudParsingHelper.resolveFor(downloader, getOriginalUrl()); track = SoundcloudParsingHelper.resolveFor(downloader, getOriginalUrl());
String policy = track.getString("policy", ""); String policy = track.getString("policy", EMPTY_STRING);
if (!policy.equals("ALLOW") && !policy.equals("MONETIZE")) { if (!policy.equals("ALLOW") && !policy.equals("MONETIZE")) {
throw new ContentNotAvailableException("Content not available: policy " + policy); throw new ContentNotAvailableException("Content not available: policy " + policy);
} }
@ -78,9 +80,9 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
@Nonnull @Nonnull
@Override @Override
public String getThumbnailUrl() { public String getThumbnailUrl() {
String artworkUrl = track.getString("artwork_url", ""); String artworkUrl = track.getString("artwork_url", EMPTY_STRING);
if (artworkUrl.isEmpty()) { if (artworkUrl.isEmpty()) {
artworkUrl = track.getObject("user").getString("avatar_url", ""); artworkUrl = track.getObject("user").getString("avatar_url", EMPTY_STRING);
} }
String artworkUrlBetterResolution = artworkUrl.replace("large.jpg", "crop.jpg"); String artworkUrlBetterResolution = artworkUrl.replace("large.jpg", "crop.jpg");
return artworkUrlBetterResolution; return artworkUrlBetterResolution;

View File

@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps; import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
public class SoundcloudStreamInfoItemExtractor implements StreamInfoItemExtractor { public class SoundcloudStreamInfoItemExtractor implements StreamInfoItemExtractor {
@ -62,7 +63,7 @@ public class SoundcloudStreamInfoItemExtractor implements StreamInfoItemExtracto
@Override @Override
public String getThumbnailUrl() { public String getThumbnailUrl() {
String artworkUrl = itemObject.getString("artwork_url", ""); String artworkUrl = itemObject.getString("artwork_url", EMPTY_STRING);
if (artworkUrl.isEmpty()) { if (artworkUrl.isEmpty()) {
artworkUrl = itemObject.getObject("user").getString("avatar_url"); artworkUrl = itemObject.getObject("user").getString("avatar_url");
} }

View File

@ -72,22 +72,16 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
while (level < 3) { while (level < 3) {
final JsonArray jsonResponse = getJsonResponse(url, getExtractorLocalization()); final JsonArray jsonResponse = getJsonResponse(url, getExtractorLocalization());
final JsonObject endpoint = jsonResponse.getObject(1, EMPTY_OBJECT) final JsonObject endpoint = jsonResponse.getObject(1).getObject("response")
.getObject("response", EMPTY_OBJECT).getArray("onResponseReceivedActions", EMPTY_ARRAY) .getArray("onResponseReceivedActions").getObject(0).getObject("navigateAction")
.getObject(0, EMPTY_OBJECT).getObject("navigateAction", EMPTY_OBJECT) .getObject("endpoint");
.getObject("endpoint", EMPTY_OBJECT);
final String webPageType = endpoint final String webPageType = endpoint.getObject("commandMetadata").getObject("webCommandMetadata")
.getObject("commandMetadata", EMPTY_OBJECT)
.getObject("webCommandMetadata", EMPTY_OBJECT)
.getString("webPageType", EMPTY_STRING); .getString("webPageType", EMPTY_STRING);
final String browseId = endpoint final String browseId = endpoint.getObject("browseEndpoint").getString("browseId", EMPTY_STRING);
.getObject("browseEndpoint", EMPTY_OBJECT)
.getString("browseId", EMPTY_STRING);
if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE") && !browseId.isEmpty()) { if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE") && !browseId.isEmpty()) {
if (!browseId.startsWith("UC")) { if (!browseId.startsWith("UC")) {
throw new ExtractionException("Redirected id is not pointing to a channel"); throw new ExtractionException("Redirected id is not pointing to a channel");
} }
@ -131,10 +125,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
@Nonnull @Nonnull
@Override @Override
public String getId() throws ParsingException { public String getId() throws ParsingException {
final String channelId = initialData final String channelId = initialData.getObject("header").getObject("c4TabbedHeaderRenderer")
.getObject("header", EMPTY_OBJECT) .getString("channelId", EMPTY_STRING);
.getObject("c4TabbedHeaderRenderer", EMPTY_OBJECT)
.getString("channelId", EMPTY_STRING);
if (!channelId.isEmpty()) { if (!channelId.isEmpty()) {
return channelId; return channelId;
@ -170,11 +162,9 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
@Override @Override
public String getBannerUrl() throws ParsingException { public String getBannerUrl() throws ParsingException {
try { try {
String url = null; String url = initialData.getObject("header").getObject("c4TabbedHeaderRenderer").getObject("banner")
try {
url = initialData.getObject("header").getObject("c4TabbedHeaderRenderer").getObject("banner")
.getArray("thumbnails").getObject(0).getString("url"); .getArray("thumbnails").getObject(0).getString("url");
} catch (Exception ignored) {}
if (url == null || url.contains("s.ytimg.com") || url.contains("default_banner")) { if (url == null || url.contains("s.ytimg.com") || url.contains("default_banner")) {
return null; return null;
} }
@ -196,19 +186,19 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
@Override @Override
public long getSubscriberCount() throws ParsingException { public long getSubscriberCount() throws ParsingException {
final JsonObject subscriberInfo = initialData.getObject("header").getObject("c4TabbedHeaderRenderer").getObject("subscriberCountText"); final JsonObject c4TabbedHeaderRenderer = initialData.getObject("header").getObject("c4TabbedHeaderRenderer");
if (subscriberInfo != null) { if (c4TabbedHeaderRenderer.has("subscriberCountText")) {
try { try {
return Utils.mixedNumberWordToLong(getTextFromObject(subscriberInfo)); return Utils.mixedNumberWordToLong(getTextFromObject(c4TabbedHeaderRenderer.getObject("subscriberCountText")));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new ParsingException("Could not get subscriber count", e); throw new ParsingException("Could not get subscriber count", e);
} }
} else { } else {
// If there's no subscribe button, the channel has the subscriber count disabled // If there's no subscribe button, the channel has the subscriber count disabled
if (initialData.getObject("header").getObject("c4TabbedHeaderRenderer").getObject("subscribeButton") == null) { if (c4TabbedHeaderRenderer.has("subscribeButton")) {
return -1;
} else {
return 0; return 0;
} else {
return -1;
} }
} }
} }
@ -260,7 +250,9 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
private String getNextPageUrlFrom(JsonArray continuations) { private String getNextPageUrlFrom(JsonArray continuations) {
if (continuations == null) return ""; if (continuations == null || continuations.isEmpty()) {
return "";
}
JsonObject nextContinuationData = continuations.getObject(0).getObject("nextContinuationData"); JsonObject nextContinuationData = continuations.getObject(0).getObject("nextContinuationData");
String continuation = nextContinuationData.getString("continuation"); String continuation = nextContinuationData.getString("continuation");
@ -277,7 +269,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
final TimeAgoParser timeAgoParser = getTimeAgoParser(); final TimeAgoParser timeAgoParser = getTimeAgoParser();
for (Object video : videos) { for (Object video : videos) {
if (((JsonObject) video).getObject("gridVideoRenderer") != null) { if (((JsonObject) video).has("gridVideoRenderer")) {
collector.commit(new YoutubeStreamInfoItemExtractor( collector.commit(new YoutubeStreamInfoItemExtractor(
((JsonObject) video).getObject("gridVideoRenderer"), timeAgoParser) { ((JsonObject) video).getObject("gridVideoRenderer"), timeAgoParser) {
@Override @Override
@ -302,8 +294,8 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
JsonObject videoTab = null; JsonObject videoTab = null;
for (Object tab : tabs) { for (Object tab : tabs) {
if (((JsonObject) tab).getObject("tabRenderer") != null) { if (((JsonObject) tab).has("tabRenderer")) {
if (((JsonObject) tab).getObject("tabRenderer").getString("title").equals("Videos")) { if (((JsonObject) tab).getObject("tabRenderer").getString("title", EMPTY_STRING).equals("Videos")) {
videoTab = ((JsonObject) tab).getObject("tabRenderer"); videoTab = ((JsonObject) tab).getObject("tabRenderer");
break; break;
} }
@ -314,13 +306,11 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
throw new ContentNotSupportedException("This channel has no Videos tab"); throw new ContentNotSupportedException("This channel has no Videos tab");
} }
try { if (getTextFromObject(videoTab.getObject("content").getObject("sectionListRenderer")
if (getTextFromObject(videoTab.getObject("content").getObject("sectionListRenderer") .getArray("contents").getObject(0).getObject("itemSectionRenderer")
.getArray("contents").getObject(0).getObject("itemSectionRenderer") .getArray("contents").getObject(0).getObject("messageRenderer")
.getArray("contents").getObject(0).getObject("messageRenderer") .getObject("text")).equals("This channel has no videos."))
.getObject("text")).equals("This channel has no videos.")) return null;
return null;
} catch (Exception ignored) {}
this.videoTab = videoTab; this.videoTab = videoTab;
return videoTab; return videoTab;

View File

@ -70,14 +70,12 @@ public class YoutubeChannelInfoItemExtractor implements ChannelInfoItemExtractor
@Override @Override
public long getSubscriberCount() throws ParsingException { public long getSubscriberCount() throws ParsingException {
try { try {
final JsonObject subscriberCountObject = channelInfoItem.getObject("subscriberCountText"); if (!channelInfoItem.has("subscriberCountText")) {
if (subscriberCountObject == null) {
// Subscription count is not available for this channel item. // Subscription count is not available for this channel item.
return -1; return -1;
} }
return Utils.mixedNumberWordToLong(getTextFromObject(subscriberCountObject)); return Utils.mixedNumberWordToLong(getTextFromObject(channelInfoItem.getObject("subscriberCountText")));
} catch (Exception e) { } catch (Exception e) {
throw new ParsingException("Could not get subscriber count", e); throw new ParsingException("Could not get subscriber count", e);
} }
@ -86,14 +84,13 @@ public class YoutubeChannelInfoItemExtractor implements ChannelInfoItemExtractor
@Override @Override
public long getStreamCount() throws ParsingException { public long getStreamCount() throws ParsingException {
try { try {
final JsonObject videoCountObject = channelInfoItem.getObject("videoCountText"); if (!channelInfoItem.has("videoCountText")) {
if (videoCountObject == null) {
// Video count is not available, channel probably has no public uploads. // Video count is not available, channel probably has no public uploads.
return -1; return -1;
} }
return Long.parseLong(Utils.removeNonDigitCharacters(getTextFromObject(videoCountObject))); return Long.parseLong(Utils.removeNonDigitCharacters(getTextFromObject(
channelInfoItem.getObject("videoCountText"))));
} catch (Exception e) { } catch (Exception e) {
throw new ParsingException("Could not get stream count", e); throw new ParsingException("Could not get stream count", e);
} }
@ -102,14 +99,12 @@ public class YoutubeChannelInfoItemExtractor implements ChannelInfoItemExtractor
@Override @Override
public String getDescription() throws ParsingException { public String getDescription() throws ParsingException {
try { try {
final JsonObject descriptionObject = channelInfoItem.getObject("descriptionSnippet"); if (!channelInfoItem.has("descriptionSnippet")) {
if (descriptionObject == null) {
// Channel have no description. // Channel have no description.
return null; return null;
} }
return getTextFromObject(descriptionObject); return getTextFromObject(channelInfoItem.getObject("descriptionSnippet"));
} catch (Exception e) { } catch (Exception e) {
throw new ParsingException("Could not get description", e); throw new ParsingException("Could not get description", e);
} }

View File

@ -37,6 +37,7 @@ import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeS
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_PLAYLISTS; import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_PLAYLISTS;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_SONGS; import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_SONGS;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_VIDEOS; import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.MUSIC_VIDEOS;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
public class YoutubeMusicSearchExtractor extends SearchExtractor { public class YoutubeMusicSearchExtractor extends SearchExtractor {
private JsonObject initialData; private JsonObject initialData;
@ -128,14 +129,10 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
@Override @Override
public String getSearchSuggestion() throws ParsingException { public String getSearchSuggestion() throws ParsingException {
final JsonObject itemSectionRenderer = initialData.getObject("contents").getObject("sectionListRenderer") final JsonObject didYouMeanRenderer = initialData.getObject("contents").getObject("sectionListRenderer")
.getArray("contents").getObject(0).getObject("itemSectionRenderer"); .getArray("contents").getObject(0).getObject("itemSectionRenderer")
if (itemSectionRenderer == null) { .getArray("contents").getObject(0).getObject("didYouMeanRenderer");
return ""; if (!didYouMeanRenderer.has("correctedQuery")) {
}
final JsonObject didYouMeanRenderer = itemSectionRenderer.getArray("contents")
.getObject(0).getObject("didYouMeanRenderer");
if (didYouMeanRenderer == null) {
return ""; return "";
} }
return getTextFromObject(didYouMeanRenderer.getObject("correctedQuery")); return getTextFromObject(didYouMeanRenderer.getObject("correctedQuery"));
@ -149,7 +146,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
final JsonArray contents = initialData.getObject("contents").getObject("sectionListRenderer").getArray("contents"); final JsonArray contents = initialData.getObject("contents").getObject("sectionListRenderer").getArray("contents");
for (Object content : contents) { for (Object content : contents) {
if (((JsonObject) content).getObject("musicShelfRenderer") != null) { if (((JsonObject) content).has("musicShelfRenderer")) {
collectMusicStreamsFrom(collector, ((JsonObject) content).getObject("musicShelfRenderer").getArray("contents")); collectMusicStreamsFrom(collector, ((JsonObject) content).getObject("musicShelfRenderer").getArray("contents"));
} }
} }
@ -162,7 +159,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
final JsonArray contents = initialData.getObject("contents").getObject("sectionListRenderer").getArray("contents"); final JsonArray contents = initialData.getObject("contents").getObject("sectionListRenderer").getArray("contents");
for (Object content : contents) { for (Object content : contents) {
if (((JsonObject) content).getObject("musicShelfRenderer") != null) { if (((JsonObject) content).has("musicShelfRenderer")) {
return getNextPageUrlFrom(((JsonObject) content).getObject("musicShelfRenderer").getArray("continuations")); return getNextPageUrlFrom(((JsonObject) content).getObject("musicShelfRenderer").getArray("continuations"));
} }
} }
@ -224,10 +221,6 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
throw new ParsingException("Could not parse JSON", e); throw new ParsingException("Could not parse JSON", e);
} }
if (ajaxJson.getObject("continuationContents") == null) {
return InfoItemsPage.emptyPage();
}
final JsonObject musicShelfContinuation = ajaxJson.getObject("continuationContents").getObject("musicShelfContinuation"); final JsonObject musicShelfContinuation = ajaxJson.getObject("continuationContents").getObject("musicShelfContinuation");
collectMusicStreamsFrom(collector, musicShelfContinuation.getArray("contents")); collectMusicStreamsFrom(collector, musicShelfContinuation.getArray("contents"));
@ -240,7 +233,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
final TimeAgoParser timeAgoParser = getTimeAgoParser(); final TimeAgoParser timeAgoParser = getTimeAgoParser();
for (Object item : videos) { for (Object item : videos) {
final JsonObject info = ((JsonObject) item).getObject("musicResponsiveListItemRenderer"); final JsonObject info = ((JsonObject) item).getObject("musicResponsiveListItemRenderer", null);
if (info != null) { if (info != null) {
final String searchType = getLinkHandler().getContentFilters().get(0); final String searchType = getLinkHandler().getContentFilters().get(0);
if (searchType.equals(MUSIC_SONGS) || searchType.equals(MUSIC_VIDEOS)) { if (searchType.equals(MUSIC_SONGS) || searchType.equals(MUSIC_VIDEOS)) {
@ -290,7 +283,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
JsonArray items = info.getObject("menu").getObject("menuRenderer").getArray("items"); JsonArray items = info.getObject("menu").getObject("menuRenderer").getArray("items");
for (Object item : items) { for (Object item : items) {
final JsonObject menuNavigationItemRenderer = ((JsonObject) item).getObject("menuNavigationItemRenderer"); final JsonObject menuNavigationItemRenderer = ((JsonObject) item).getObject("menuNavigationItemRenderer");
if (menuNavigationItemRenderer != null && menuNavigationItemRenderer.getObject("icon").getString("iconType").equals("ARTIST")) { if (menuNavigationItemRenderer.getObject("icon").getString("iconType", EMPTY_STRING).equals("ARTIST")) {
return getUrlFromNavigationEndpoint(menuNavigationItemRenderer.getObject("navigationEndpoint")); return getUrlFromNavigationEndpoint(menuNavigationItemRenderer.getObject("navigationEndpoint"));
} }
} }
@ -301,16 +294,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
.getObject(1).getObject("musicResponsiveListItemFlexColumnRenderer") .getObject(1).getObject("musicResponsiveListItemFlexColumnRenderer")
.getObject("text").getArray("runs").getObject(0).getObject("navigationEndpoint"); .getObject("text").getArray("runs").getObject(0).getObject("navigationEndpoint");
if (navigationEndpoint == null) { return getUrlFromNavigationEndpoint(navigationEndpoint);
return null;
}
final String url = getUrlFromNavigationEndpoint(navigationEndpoint);
if (url != null && !url.isEmpty()) {
return url;
}
throw new ParsingException("Could not get uploader url");
} }
} }
@ -480,7 +464,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
} }
private String getNextPageUrlFrom(final JsonArray continuations) throws ParsingException, IOException, ReCaptchaException { private String getNextPageUrlFrom(final JsonArray continuations) throws ParsingException, IOException, ReCaptchaException {
if (continuations == null) { if (continuations == null || continuations.isEmpty()) {
return ""; return "";
} }

View File

@ -47,23 +47,16 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
private JsonObject getUploaderInfo() throws ParsingException { private JsonObject getUploaderInfo() throws ParsingException {
JsonArray items = initialData.getObject("sidebar").getObject("playlistSidebarRenderer").getArray("items"); JsonArray items = initialData.getObject("sidebar").getObject("playlistSidebarRenderer").getArray("items");
try {
JsonObject uploaderInfo = items.getObject(1).getObject("playlistSidebarSecondaryInfoRenderer") JsonObject videoOwner = items.getObject(1).getObject("playlistSidebarSecondaryInfoRenderer").getObject("videoOwner");
.getObject("videoOwner").getObject("videoOwnerRenderer"); if (videoOwner.has("videoOwnerRenderer")) {
if (uploaderInfo != null) { return videoOwner.getObject("videoOwnerRenderer");
return uploaderInfo; }
}
} catch (Exception ignored) {}
// we might want to create a loop here instead of using duplicated code // we might want to create a loop here instead of using duplicated code
try { videoOwner = items.getObject(items.size()).getObject("playlistSidebarSecondaryInfoRenderer").getObject("videoOwner");
JsonObject uploaderInfo = items.getObject(items.size()).getObject("playlistSidebarSecondaryInfoRenderer") if (videoOwner.has("videoOwnerRenderer")) {
.getObject("videoOwner").getObject("videoOwnerRenderer"); return videoOwner.getObject("videoOwnerRenderer");
if (uploaderInfo != null) {
return uploaderInfo;
}
} catch (Exception e) {
throw new ParsingException("Could not get uploader info", e);
} }
throw new ParsingException("Could not get uploader info"); throw new ParsingException("Could not get uploader info");
} }
@ -89,33 +82,22 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
@Nonnull @Nonnull
@Override @Override
public String getName() throws ParsingException { public String getName() throws ParsingException {
try { String name = getTextFromObject(playlistInfo.getObject("title"));
String name = getTextFromObject(playlistInfo.getObject("title")); if (name == null || !name.isEmpty()) return name;
if (name != null) return name;
} catch (Exception ignored) {} return initialData.getObject("microformat").getObject("microformatDataRenderer").getString("title");
try {
return initialData.getObject("microformat").getObject("microformatDataRenderer").getString("title");
} catch (Exception e) {
throw new ParsingException("Could not get playlist name", e);
}
} }
@Override @Override
public String getThumbnailUrl() throws ParsingException { public String getThumbnailUrl() throws ParsingException {
String url = null; String url = playlistInfo.getObject("thumbnailRenderer").getObject("playlistVideoThumbnailRenderer")
.getObject("thumbnail").getArray("thumbnails").getObject(0).getString("url");
try { if (url == null || url.isEmpty()) {
url = playlistInfo.getObject("thumbnailRenderer").getObject("playlistVideoThumbnailRenderer") url = initialData.getObject("microformat").getObject("microformatDataRenderer").getObject("thumbnail")
.getObject("thumbnail").getArray("thumbnails").getObject(0).getString("url"); .getArray("thumbnails").getObject(0).getString("url");
} catch (Exception ignored) {}
if (url == null) { if (url == null || url.isEmpty()) throw new ParsingException("Could not get playlist thumbnail");
try {
url = initialData.getObject("microformat").getObject("microformatDataRenderer").getObject("thumbnail")
.getArray("thumbnails").getObject(0).getString("url");
} catch (Exception ignored) {}
if (url == null) throw new ParsingException("Could not get playlist thumbnail");
} }
return fixThumbnailUrl(url); return fixThumbnailUrl(url);
@ -123,8 +105,9 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
@Override @Override
public String getBannerUrl() { public String getBannerUrl() {
return ""; // Banner can't be handled by frontend right now. // Banner can't be handled by frontend right now.
// Whoever is willing to implement this should also implement it in the frontend. // Whoever is willing to implement this should also implement it in the frontend.
return "";
} }
@Override @Override
@ -199,7 +182,7 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
} }
private String getNextPageUrlFrom(JsonArray continuations) { private String getNextPageUrlFrom(JsonArray continuations) {
if (continuations == null) { if (continuations == null || continuations.isEmpty()) {
return ""; return "";
} }
@ -216,7 +199,7 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
final TimeAgoParser timeAgoParser = getTimeAgoParser(); final TimeAgoParser timeAgoParser = getTimeAgoParser();
for (Object video : videos) { for (Object video : videos) {
if (((JsonObject) video).getObject("playlistVideoRenderer") != null) { if (((JsonObject) video).has("playlistVideoRenderer")) {
collector.commit(new YoutubeStreamInfoItemExtractor(((JsonObject) video).getObject("playlistVideoRenderer"), timeAgoParser) { collector.commit(new YoutubeStreamInfoItemExtractor(((JsonObject) video).getObject("playlistVideoRenderer"), timeAgoParser) {
@Override @Override
public long getViewCount() { public long getViewCount() {

View File

@ -69,7 +69,7 @@ public class YoutubeSearchExtractor extends SearchExtractor {
.getObject("sectionListRenderer").getArray("contents").getObject(0) .getObject("sectionListRenderer").getArray("contents").getObject(0)
.getObject("itemSectionRenderer").getArray("contents").getObject(0) .getObject("itemSectionRenderer").getArray("contents").getObject(0)
.getObject("showingResultsForRenderer"); .getObject("showingResultsForRenderer");
if (showingResultsForRenderer == null) { if (!showingResultsForRenderer.has("correctedQuery")) {
return ""; return "";
} }
return getTextFromObject(showingResultsForRenderer.getObject("correctedQuery")); return getTextFromObject(showingResultsForRenderer.getObject("correctedQuery"));
@ -119,21 +119,21 @@ public class YoutubeSearchExtractor extends SearchExtractor {
final TimeAgoParser timeAgoParser = getTimeAgoParser(); final TimeAgoParser timeAgoParser = getTimeAgoParser();
for (Object item : videos) { for (Object item : videos) {
if (((JsonObject) item).getObject("backgroundPromoRenderer") != null) { if (((JsonObject) item).has("backgroundPromoRenderer")) {
throw new NothingFoundException(getTextFromObject(((JsonObject) item) throw new NothingFoundException(getTextFromObject(((JsonObject) item)
.getObject("backgroundPromoRenderer").getObject("bodyText"))); .getObject("backgroundPromoRenderer").getObject("bodyText")));
} else if (((JsonObject) item).getObject("videoRenderer") != null) { } else if (((JsonObject) item).has("videoRenderer")) {
collector.commit(new YoutubeStreamInfoItemExtractor(((JsonObject) item).getObject("videoRenderer"), timeAgoParser)); collector.commit(new YoutubeStreamInfoItemExtractor(((JsonObject) item).getObject("videoRenderer"), timeAgoParser));
} else if (((JsonObject) item).getObject("channelRenderer") != null) { } else if (((JsonObject) item).has("channelRenderer")) {
collector.commit(new YoutubeChannelInfoItemExtractor(((JsonObject) item).getObject("channelRenderer"))); collector.commit(new YoutubeChannelInfoItemExtractor(((JsonObject) item).getObject("channelRenderer")));
} else if (((JsonObject) item).getObject("playlistRenderer") != null) { } else if (((JsonObject) item).has("playlistRenderer")) {
collector.commit(new YoutubePlaylistInfoItemExtractor(((JsonObject) item).getObject("playlistRenderer"))); collector.commit(new YoutubePlaylistInfoItemExtractor(((JsonObject) item).getObject("playlistRenderer")));
} }
} }
} }
private String getNextPageUrlFrom(final JsonArray continuations) throws ParsingException { private String getNextPageUrlFrom(final JsonArray continuations) throws ParsingException {
if (continuations == null) { if (continuations == null || continuations.isEmpty()) {
return ""; return "";
} }

View File

@ -57,6 +57,7 @@ import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeP
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getJsonResponse; import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getJsonResponse;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getTextFromObject; import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getTextFromObject;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getUrlFromNavigationEndpoint; import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.getUrlFromNavigationEndpoint;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
/* /*
* Created by Christian Schabesberger on 06.08.15. * Created by Christian Schabesberger on 06.08.15.
@ -117,18 +118,12 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Override @Override
public String getName() throws ParsingException { public String getName() throws ParsingException {
assertPageFetched(); assertPageFetched();
String title = null; String title = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("title"));
try { if (title == null || title.isEmpty()) {
title = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("title")); title = playerResponse.getObject("videoDetails").getString("title");
} catch (Exception ignored) {}
if (title == null) { if (title == null || title.isEmpty()) throw new ParsingException("Could not get name");
try {
title = playerResponse.getObject("videoDetails").getString("title");
} catch (Exception ignored) {}
if (title == null) throw new ParsingException("Could not get name");
} }
return title; return title;
@ -140,35 +135,31 @@ public class YoutubeStreamExtractor extends StreamExtractor {
return null; return null;
} }
try { JsonObject micro = playerResponse.getObject("microformat").getObject("playerMicroformatRenderer");
JsonObject micro = playerResponse.getObject("microformat").getObject("playerMicroformatRenderer"); if (micro.isString("uploadDate") && !micro.getString("uploadDate").isEmpty()) {
if (micro.getString("uploadDate") != null && !micro.getString("uploadDate").isEmpty()) { return micro.getString("uploadDate");
return micro.getString("uploadDate"); }
} if (micro.isString("publishDate") && !micro.getString("publishDate").isEmpty()) {
if (micro.getString("publishDate") != null && !micro.getString("publishDate").isEmpty()) { return micro.getString("publishDate");
return micro.getString("publishDate"); }
}
} catch (Exception ignored) {} 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"));
Calendar parsedTime = timeAgoParser.parse(time).date();
return new SimpleDateFormat("yyyy-MM-dd").format(parsedTime.getTime());
} catch (Exception ignored) {}
try { // Premiered Feb 21, 2020
Date d = new SimpleDateFormat("MMM dd, YYYY", Locale.ENGLISH).parse(time);
return new SimpleDateFormat("yyyy-MM-dd").format(d.getTime());
} catch (Exception ignored) {}
}
try { try {
if (getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText")).startsWith("Premiered")) { // TODO: this parses English formatted dates only, we need a better approach to parse the textual date
String time = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText")).substring(10);
try { // Premiered 20 hours ago
TimeAgoParser timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(Localization.fromLocalizationCode("en"));
Calendar parsedTime = timeAgoParser.parse(time).date();
return new SimpleDateFormat("yyyy-MM-dd").format(parsedTime.getTime());
} catch (Exception ignored) {}
try { // Premiered Feb 21, 2020
Date d = new SimpleDateFormat("MMM dd, YYYY", Locale.ENGLISH).parse(time);
return new SimpleDateFormat("yyyy-MM-dd").format(d.getTime());
} catch (Exception ignored) {}
}
} catch (Exception ignored) {}
try {
// TODO this parses English formatted dates only, we need a better approach to parse the textual date
Date d = new SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH).parse( Date d = new SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH).parse(
getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText"))); getTextFromObject(getVideoPrimaryInfoRenderer().getObject("dateText")));
return new SimpleDateFormat("yyyy-MM-dd").format(d); return new SimpleDateFormat("yyyy-MM-dd").format(d);
@ -180,7 +171,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
public DateWrapper getUploadDate() throws ParsingException { public DateWrapper getUploadDate() throws ParsingException {
final String textualUploadDate = getTextualUploadDate(); final String textualUploadDate = getTextualUploadDate();
if (textualUploadDate == null) { if (textualUploadDate == null || textualUploadDate.isEmpty()) {
return null; return null;
} }
@ -208,17 +199,11 @@ public class YoutubeStreamExtractor extends StreamExtractor {
public Description getDescription() throws ParsingException { public Description getDescription() throws ParsingException {
assertPageFetched(); assertPageFetched();
// description with more info on links // description with more info on links
try { String description = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("description"), true);
String description = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("description"), true); if (description != null && !description.isEmpty()) return new Description(description, Description.HTML);
return new Description(description, Description.HTML);
} catch (Exception ignored) { }
// raw non-html description // raw non-html description
try { return new Description(playerResponse.getObject("videoDetails").getString("shortDescription"), Description.PLAIN_TEXT);
return new Description(playerResponse.getObject("videoDetails").getString("shortDescription"), Description.PLAIN_TEXT);
} catch (Exception ignored) {
throw new ParsingException("Could not get description");
}
} }
@Override @Override
@ -264,19 +249,13 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Override @Override
public long getViewCount() throws ParsingException { public long getViewCount() throws ParsingException {
assertPageFetched(); assertPageFetched();
String views = null; String views = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("viewCount")
try {
views = getTextFromObject(getVideoPrimaryInfoRenderer().getObject("viewCount")
.getObject("videoViewCountRenderer").getObject("viewCount")); .getObject("videoViewCountRenderer").getObject("viewCount"));
} catch (Exception ignored) {}
if (views == null) { if (views == null || views.isEmpty()) {
try { views = playerResponse.getObject("videoDetails").getString("viewCount");
views = playerResponse.getObject("videoDetails").getString("viewCount");
} catch (Exception ignored) {}
if (views == null) throw new ParsingException("Could not get view count"); if (views == null || views.isEmpty()) throw new ParsingException("Could not get view count");
} }
if (views.toLowerCase().contains("no views")) return 0; if (views.toLowerCase().contains("no views")) return 0;
@ -334,16 +313,16 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Override @Override
public String getUploaderUrl() throws ParsingException { public String getUploaderUrl() throws ParsingException {
assertPageFetched(); assertPageFetched();
try {
String uploaderUrl = getUrlFromNavigationEndpoint(getVideoSecondaryInfoRenderer() String uploaderUrl = getUrlFromNavigationEndpoint(getVideoSecondaryInfoRenderer()
.getObject("owner").getObject("videoOwnerRenderer").getObject("navigationEndpoint")); .getObject("owner").getObject("videoOwnerRenderer").getObject("navigationEndpoint"));
if (uploaderUrl != null) return uploaderUrl; if (uploaderUrl != null && !uploaderUrl.isEmpty()) return uploaderUrl;
} catch (Exception ignored) {}
try {
String uploaderId = playerResponse.getObject("videoDetails").getString("channelId"); String uploaderId = playerResponse.getObject("videoDetails").getString("channelId");
if (uploaderId != null) if (uploaderId != null && !uploaderId.isEmpty())
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("channel/" + uploaderId); return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("channel/" + uploaderId);
} catch (Exception ignored) {}
throw new ParsingException("Could not get uploader url"); throw new ParsingException("Could not get uploader url");
} }
@ -351,19 +330,13 @@ public class YoutubeStreamExtractor extends StreamExtractor {
@Override @Override
public String getUploaderName() throws ParsingException { public String getUploaderName() throws ParsingException {
assertPageFetched(); assertPageFetched();
String uploaderName = null; String uploaderName = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("owner")
try {
uploaderName = getTextFromObject(getVideoSecondaryInfoRenderer().getObject("owner")
.getObject("videoOwnerRenderer").getObject("title")); .getObject("videoOwnerRenderer").getObject("title"));
} catch (Exception ignored) {}
if (uploaderName == null) { if (uploaderName == null || uploaderName.isEmpty()) {
try { uploaderName = playerResponse.getObject("videoDetails").getString("author");
uploaderName = playerResponse.getObject("videoDetails").getString("author");
} catch (Exception ignored) {}
if (uploaderName == null) throw new ParsingException("Could not get uploader name"); if (uploaderName == null || uploaderName.isEmpty()) throw new ParsingException("Could not get uploader name");
} }
return uploaderName; return uploaderName;
@ -392,7 +365,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
if (videoInfoPage.containsKey("dashmpd")) { if (videoInfoPage.containsKey("dashmpd")) {
dashManifestUrl = videoInfoPage.get("dashmpd"); dashManifestUrl = videoInfoPage.get("dashmpd");
} else if (playerArgs != null && playerArgs.isString("dashmpd")) { } else if (playerArgs != null && playerArgs.isString("dashmpd")) {
dashManifestUrl = playerArgs.getString("dashmpd", ""); dashManifestUrl = playerArgs.getString("dashmpd", EMPTY_STRING);
} else { } else {
return ""; return "";
} }
@ -561,9 +534,9 @@ public class YoutubeStreamExtractor extends StreamExtractor {
final TimeAgoParser timeAgoParser = getTimeAgoParser(); final TimeAgoParser timeAgoParser = getTimeAgoParser();
for (Object ul : results) { for (Object ul : results) {
final JsonObject videoInfo = ((JsonObject) ul).getObject("compactVideoRenderer"); if (((JsonObject) ul).has("compactVideoRenderer")) {
collector.commit(new YoutubeStreamInfoItemExtractor(((JsonObject) ul).getObject("compactVideoRenderer"), timeAgoParser));
if (videoInfo != null) collector.commit(new YoutubeStreamInfoItemExtractor(videoInfo, timeAgoParser)); }
} }
return collector; return collector;
} catch (Exception e) { } catch (Exception e) {
@ -612,7 +585,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
final String playerUrl; final String playerUrl;
if (initialAjaxJson.getObject(2).getObject("response") != null) { // age-restricted videos if (initialAjaxJson.getObject(2).has("response")) { // age-restricted videos
initialData = initialAjaxJson.getObject(2).getObject("response"); initialData = initialAjaxJson.getObject(2).getObject("response");
ageLimit = 18; ageLimit = 18;
@ -631,7 +604,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
playerResponse = getPlayerResponse(); playerResponse = getPlayerResponse();
final JsonObject playabilityStatus = playerResponse.getObject("playabilityStatus", JsonUtils.EMPTY_OBJECT); final JsonObject playabilityStatus = playerResponse.getObject("playabilityStatus");
final String status = playabilityStatus.getString("status"); final String status = playabilityStatus.getString("status");
// If status exist, and is not "OK", throw a ContentNotAvailableException with the reason. // If status exist, and is not "OK", throw a ContentNotAvailableException with the reason.
if (status != null && !status.toLowerCase().equals("ok")) { if (status != null && !status.toLowerCase().equals("ok")) {
@ -808,10 +781,10 @@ public class YoutubeStreamExtractor extends StreamExtractor {
} }
captions = playerResponse.getObject("captions"); captions = playerResponse.getObject("captions");
final JsonObject renderer = captions.getObject("playerCaptionsTracklistRenderer", new JsonObject()); final JsonObject renderer = captions.getObject("playerCaptionsTracklistRenderer");
final JsonArray captionsArray = renderer.getArray("captionTracks", new JsonArray()); final JsonArray captionsArray = renderer.getArray("captionTracks");
// todo: use this to apply auto translation to different language from a source language // todo: use this to apply auto translation to different language from a source language
// final JsonArray autoCaptionsArray = renderer.getArray("translationLanguages", new JsonArray()); // final JsonArray autoCaptionsArray = renderer.getArray("translationLanguages");
// This check is necessary since there may be cases where subtitles metadata do not contain caption track info // This check is necessary since there may be cases where subtitles metadata do not contain caption track info
// e.g. https://www.youtube.com/watch?v=-Vpwatutnko // e.g. https://www.youtube.com/watch?v=-Vpwatutnko
@ -876,7 +849,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
JsonObject videoPrimaryInfoRenderer = null; JsonObject videoPrimaryInfoRenderer = null;
for (Object content : contents) { for (Object content : contents) {
if (((JsonObject) content).getObject("videoPrimaryInfoRenderer") != null) { if (((JsonObject) content).has("videoPrimaryInfoRenderer")) {
videoPrimaryInfoRenderer = ((JsonObject) content).getObject("videoPrimaryInfoRenderer"); videoPrimaryInfoRenderer = ((JsonObject) content).getObject("videoPrimaryInfoRenderer");
break; break;
} }
@ -898,7 +871,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
JsonObject videoSecondaryInfoRenderer = null; JsonObject videoSecondaryInfoRenderer = null;
for (Object content : contents) { for (Object content : contents) {
if (((JsonObject) content).getObject("videoSecondaryInfoRenderer") != null) { if (((JsonObject) content).has("videoSecondaryInfoRenderer")) {
videoSecondaryInfoRenderer = ((JsonObject) content).getObject("videoSecondaryInfoRenderer"); videoSecondaryInfoRenderer = ((JsonObject) content).getObject("videoSecondaryInfoRenderer");
break; break;
} }

View File

@ -17,6 +17,7 @@ import java.util.Calendar;
import java.util.Date; import java.util.Date;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.*; import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeParsingHelper.*;
import static org.schabi.newpipe.extractor.utils.JsonUtils.EMPTY_STRING;
/* /*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
@ -37,7 +38,6 @@ import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeP
*/ */
public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor { public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
private JsonObject videoInfo; private JsonObject videoInfo;
private final TimeAgoParser timeAgoParser; private final TimeAgoParser timeAgoParser;
private StreamType cachedStreamType; private StreamType cachedStreamType;
@ -59,23 +59,18 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
return cachedStreamType; return cachedStreamType;
} }
try { final JsonArray badges = videoInfo.getArray("badges");
JsonArray badges = videoInfo.getArray("badges"); for (Object badge : badges) {
for (Object badge : badges) { if (((JsonObject) badge).getObject("metadataBadgeRenderer").getString("label", EMPTY_STRING).equals("LIVE NOW")) {
if (((JsonObject) badge).getObject("metadataBadgeRenderer").getString("label").equals("LIVE NOW")) {
return cachedStreamType = StreamType.LIVE_STREAM;
}
}
} catch (Exception ignored) {}
try {
final String style = videoInfo.getArray("thumbnailOverlays").getObject(0)
.getObject("thumbnailOverlayTimeStatusRenderer").getString("style");
if (style.equalsIgnoreCase("LIVE")) {
return cachedStreamType = StreamType.LIVE_STREAM; return cachedStreamType = StreamType.LIVE_STREAM;
} }
} catch (Exception ignored) {} }
final String style = videoInfo.getArray("thumbnailOverlays").getObject(0)
.getObject("thumbnailOverlayTimeStatusRenderer").getString("style", EMPTY_STRING);
if (style.equalsIgnoreCase("LIVE")) {
return cachedStreamType = StreamType.LIVE_STREAM;
}
return cachedStreamType = StreamType.VIDEO_STREAM; return cachedStreamType = StreamType.VIDEO_STREAM;
} }
@ -98,7 +93,7 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
@Override @Override
public String getName() throws ParsingException { public String getName() throws ParsingException {
String name = getTextFromObject(videoInfo.getObject("title")); String name = getTextFromObject(videoInfo.getObject("title"));
if (name != null && !name.isEmpty()) return name; if (name == null || !name.isEmpty()) return name;
throw new ParsingException("Could not get name"); throw new ParsingException("Could not get name");
} }
@ -108,23 +103,17 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
return -1; return -1;
} }
String duration = null; String duration = getTextFromObject(videoInfo.getObject("lengthText"));
try { if (duration == null || duration.isEmpty()) {
duration = getTextFromObject(videoInfo.getObject("lengthText")); for (Object thumbnailOverlay : videoInfo.getArray("thumbnailOverlays")) {
} catch (Exception ignored) {} if (((JsonObject) thumbnailOverlay).has("thumbnailOverlayTimeStatusRenderer")) {
duration = getTextFromObject(((JsonObject) thumbnailOverlay)
if (duration == null) { .getObject("thumbnailOverlayTimeStatusRenderer").getObject("text"));
try {
for (Object thumbnailOverlay : videoInfo.getArray("thumbnailOverlays")) {
if (((JsonObject) thumbnailOverlay).getObject("thumbnailOverlayTimeStatusRenderer") != null) {
duration = getTextFromObject(((JsonObject) thumbnailOverlay)
.getObject("thumbnailOverlayTimeStatusRenderer").getObject("text"));
}
} }
} catch (Exception ignored) {} }
if (duration == null) throw new ParsingException("Could not get duration"); if (duration == null || duration.isEmpty()) throw new ParsingException("Could not get duration");
} }
return YoutubeParsingHelper.parseDurationString(duration); return YoutubeParsingHelper.parseDurationString(duration);
@ -132,23 +121,15 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
@Override @Override
public String getUploaderName() throws ParsingException { public String getUploaderName() throws ParsingException {
String name = null; String name = getTextFromObject(videoInfo.getObject("longBylineText"));
try { if (name == null || name.isEmpty()) {
name = getTextFromObject(videoInfo.getObject("longBylineText")); name = getTextFromObject(videoInfo.getObject("ownerText"));
} catch (Exception ignored) {}
if (name == null) { if (name == null || name.isEmpty()) {
try { name = getTextFromObject(videoInfo.getObject("shortBylineText"));
name = getTextFromObject(videoInfo.getObject("ownerText"));
} catch (Exception ignored) {}
if (name == null) { if (name == null || name.isEmpty()) throw new ParsingException("Could not get uploader name");
try {
name = getTextFromObject(videoInfo.getObject("shortBylineText"));
} catch (Exception ignored) {}
if (name == null) throw new ParsingException("Could not get uploader name");
} }
} }
@ -157,26 +138,18 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
@Override @Override
public String getUploaderUrl() throws ParsingException { public String getUploaderUrl() throws ParsingException {
String url = null; String url = getUrlFromNavigationEndpoint(videoInfo.getObject("longBylineText")
.getArray("runs").getObject(0).getObject("navigationEndpoint"));
try { if (url == null || url.isEmpty()) {
url = getUrlFromNavigationEndpoint(videoInfo.getObject("longBylineText") url = getUrlFromNavigationEndpoint(videoInfo.getObject("ownerText")
.getArray("runs").getObject(0).getObject("navigationEndpoint")); .getArray("runs").getObject(0).getObject("navigationEndpoint"));
} catch (Exception ignored) {}
if (url == null) { if (url == null || url.isEmpty()) {
try { url = getUrlFromNavigationEndpoint(videoInfo.getObject("shortBylineText")
url = getUrlFromNavigationEndpoint(videoInfo.getObject("ownerText")
.getArray("runs").getObject(0).getObject("navigationEndpoint")); .getArray("runs").getObject(0).getObject("navigationEndpoint"));
} catch (Exception ignored) {}
if (url == null) { if (url == null || url.isEmpty()) throw new ParsingException("Could not get uploader url");
try {
url = getUrlFromNavigationEndpoint(videoInfo.getObject("shortBylineText")
.getArray("runs").getObject(0).getObject("navigationEndpoint"));
} catch (Exception ignored) {}
if (url == null) throw new ParsingException("Could not get uploader url");
} }
} }
@ -195,12 +168,10 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
return new SimpleDateFormat("yyyy-MM-dd HH:mm").format(date); return new SimpleDateFormat("yyyy-MM-dd HH:mm").format(date);
} }
try { final String publishedTimeText = getTextFromObject(videoInfo.getObject("publishedTimeText"));
return getTextFromObject(videoInfo.getObject("publishedTimeText")); if (publishedTimeText != null && !publishedTimeText.isEmpty()) return publishedTimeText;
} catch (Exception e) {
// upload date is not always available, e.g. in playlists return null;
return null;
}
} }
@Nullable @Nullable
@ -228,17 +199,16 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
@Override @Override
public long getViewCount() throws ParsingException { public long getViewCount() throws ParsingException {
try { try {
if (videoInfo.getObject("topStandaloneBadge") != null || isPremium()) { if (videoInfo.has("topStandaloneBadge") || isPremium()) {
return -1; return -1;
} }
final JsonObject viewCountObject = videoInfo.getObject("viewCountText"); if (!videoInfo.has("viewCountText")) {
if (viewCountObject == null) {
// This object is null when a video has its views hidden. // This object is null when a video has its views hidden.
return -1; return -1;
} }
final String viewCount = getTextFromObject(viewCountObject); final String viewCount = getTextFromObject(videoInfo.getObject("viewCountText"));
if (viewCount.toLowerCase().contains("no views")) { if (viewCount.toLowerCase().contains("no views")) {
return 0; return 0;
@ -266,14 +236,11 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
} }
private boolean isPremium() { private boolean isPremium() {
try { JsonArray badges = videoInfo.getArray("badges");
JsonArray badges = videoInfo.getArray("badges"); for (Object badge : badges) {
for (Object badge : badges) { if (((JsonObject) badge).getObject("metadataBadgeRenderer").getString("label", EMPTY_STRING).equals("Premium")) {
if (((JsonObject) badge).getObject("metadataBadgeRenderer").getString("label").equals("Premium")) { return true;
return true;
}
} }
} catch (Exception ignored) {
} }
return false; return false;
} }

View File

@ -59,7 +59,7 @@ public class YoutubeSuggestionExtractor extends SuggestionExtractor {
// trim JSONP part "JP(...)" // trim JSONP part "JP(...)"
response = response.substring(3, response.length() - 1); response = response.substring(3, response.length() - 1);
try { try {
JsonArray collection = JsonParser.array().from(response).getArray(1, new JsonArray()); JsonArray collection = JsonParser.array().from(response).getArray(1);
for (Object suggestion : collection) { for (Object suggestion : collection) {
if (!(suggestion instanceof JsonArray)) continue; if (!(suggestion instanceof JsonArray)) continue;
String suggestionStr = ((JsonArray) suggestion).getString(0); String suggestionStr = ((JsonArray) suggestion).getString(0);

View File

@ -97,14 +97,11 @@ public class YoutubeTrendingExtractor extends KioskExtractor<StreamInfoItem> {
JsonObject expandedShelfContentsRenderer = ((JsonObject) itemSectionRenderer).getObject("itemSectionRenderer") JsonObject expandedShelfContentsRenderer = ((JsonObject) itemSectionRenderer).getObject("itemSectionRenderer")
.getArray("contents").getObject(0).getObject("shelfRenderer").getObject("content") .getArray("contents").getObject(0).getObject("shelfRenderer").getObject("content")
.getObject("expandedShelfContentsRenderer"); .getObject("expandedShelfContentsRenderer");
if (expandedShelfContentsRenderer != null) { for (Object ul : expandedShelfContentsRenderer.getArray("items")) {
for (Object ul : expandedShelfContentsRenderer.getArray("items")) { final JsonObject videoInfo = ((JsonObject) ul).getObject("videoRenderer");
final JsonObject videoInfo = ((JsonObject) ul).getObject("videoRenderer"); collector.commit(new YoutubeStreamInfoItemExtractor(videoInfo, timeAgoParser));
collector.commit(new YoutubeStreamInfoItemExtractor(videoInfo, timeAgoParser));
}
} }
} }
return new InfoItemsPage<>(collector, getNextPageUrl()); return new InfoItemsPage<>(collector, getNextPageUrl());
} }
} }

View File

@ -334,7 +334,7 @@ public class YoutubeParsingHelper {
} }
public static String getUrlFromNavigationEndpoint(JsonObject navigationEndpoint) throws ParsingException { public static String getUrlFromNavigationEndpoint(JsonObject navigationEndpoint) throws ParsingException {
if (navigationEndpoint.getObject("urlEndpoint") != null) { if (navigationEndpoint.has("urlEndpoint")) {
String internUrl = navigationEndpoint.getObject("urlEndpoint").getString("url"); String internUrl = navigationEndpoint.getObject("urlEndpoint").getString("url");
if (internUrl.startsWith("/redirect?")) { if (internUrl.startsWith("/redirect?")) {
// q parameter can be the first parameter // q parameter can be the first parameter
@ -354,7 +354,7 @@ public class YoutubeParsingHelper {
} else if (internUrl.startsWith("http")) { } else if (internUrl.startsWith("http")) {
return internUrl; return internUrl;
} }
} else if (navigationEndpoint.getObject("browseEndpoint") != null) { } else if (navigationEndpoint.has("browseEndpoint")) {
final JsonObject browseEndpoint = navigationEndpoint.getObject("browseEndpoint"); final JsonObject browseEndpoint = navigationEndpoint.getObject("browseEndpoint");
final String canonicalBaseUrl = browseEndpoint.getString("canonicalBaseUrl"); final String canonicalBaseUrl = browseEndpoint.getString("canonicalBaseUrl");
final String browseId = browseEndpoint.getString("browseId"); final String browseId = browseEndpoint.getString("browseId");
@ -369,7 +369,7 @@ public class YoutubeParsingHelper {
} }
throw new ParsingException("canonicalBaseUrl is null and browseId is not a channel (\"" + browseEndpoint + "\")"); throw new ParsingException("canonicalBaseUrl is null and browseId is not a channel (\"" + browseEndpoint + "\")");
} else if (navigationEndpoint.getObject("watchEndpoint") != null) { } else if (navigationEndpoint.has("watchEndpoint")) {
StringBuilder url = new StringBuilder(); StringBuilder url = new StringBuilder();
url.append("https://www.youtube.com/watch?v=").append(navigationEndpoint.getObject("watchEndpoint").getString("videoId")); url.append("https://www.youtube.com/watch?v=").append(navigationEndpoint.getObject("watchEndpoint").getString("videoId"));
if (navigationEndpoint.getObject("watchEndpoint").has("playlistId")) if (navigationEndpoint.getObject("watchEndpoint").has("playlistId"))
@ -377,7 +377,7 @@ public class YoutubeParsingHelper {
if (navigationEndpoint.getObject("watchEndpoint").has("startTimeSeconds")) if (navigationEndpoint.getObject("watchEndpoint").has("startTimeSeconds"))
url.append("&amp;t=").append(navigationEndpoint.getObject("watchEndpoint").getInt("startTimeSeconds")); url.append("&amp;t=").append(navigationEndpoint.getObject("watchEndpoint").getInt("startTimeSeconds"));
return url.toString(); return url.toString();
} else if (navigationEndpoint.getObject("watchPlaylistEndpoint") != null) { } else if (navigationEndpoint.has("watchPlaylistEndpoint")) {
return "https://www.youtube.com/playlist?list=" + return "https://www.youtube.com/playlist?list=" +
navigationEndpoint.getObject("watchPlaylistEndpoint").getString("playlistId"); navigationEndpoint.getObject("watchPlaylistEndpoint").getString("playlistId");
} }
@ -390,7 +390,7 @@ public class YoutubeParsingHelper {
StringBuilder textBuilder = new StringBuilder(); StringBuilder textBuilder = new StringBuilder();
for (Object textPart : textObject.getArray("runs")) { for (Object textPart : textObject.getArray("runs")) {
String text = ((JsonObject) textPart).getString("text"); String text = ((JsonObject) textPart).getString("text");
if (html && ((JsonObject) textPart).getObject("navigationEndpoint") != null) { if (html && ((JsonObject) textPart).has("navigationEndpoint")) {
String url = getUrlFromNavigationEndpoint(((JsonObject) textPart).getObject("navigationEndpoint")); String url = getUrlFromNavigationEndpoint(((JsonObject) textPart).getObject("navigationEndpoint"));
if (url != null && !url.isEmpty()) { if (url != null && !url.isEmpty()) {
textBuilder.append("<a href=\"").append(url).append("\">").append(text).append("</a>"); textBuilder.append("<a href=\"").append(url).append("\">").append(text).append("</a>");
@ -486,7 +486,7 @@ public class YoutubeParsingHelper {
*/ */
public static void defaultAlertsCheck(JsonObject initialData) throws ContentNotAvailableException { public static void defaultAlertsCheck(JsonObject initialData) throws ContentNotAvailableException {
final JsonArray alerts = initialData.getArray("alerts"); final JsonArray alerts = initialData.getArray("alerts");
if (alerts != null && !alerts.isEmpty()) { if (!alerts.isEmpty()) {
final JsonObject alertRenderer = alerts.getObject(0).getObject("alertRenderer"); final JsonObject alertRenderer = alerts.getObject(0).getObject("alertRenderer");
final String alertText = alertRenderer.getObject("text").getString("simpleText"); final String alertText = alertRenderer.getObject("text").getString("simpleText");
final String alertType = alertRenderer.getString("type"); final String alertType = alertRenderer.getString("type");

View File

@ -1,6 +1,6 @@
dependencies { dependencies {
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
implementation 'com.grack:nanojson:1.1' implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.spotbugs:spotbugs-annotations:3.1.0' implementation 'com.github.spotbugs:spotbugs-annotations:3.1.0'
} }