diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YouTubeChannelHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YouTubeChannelHelper.java new file mode 100644 index 000000000..2e43cbb13 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YouTubeChannelHelper.java @@ -0,0 +1,181 @@ +package org.schabi.newpipe.extractor.services.youtube; + +import com.grack.nanojson.JsonObject; +import com.grack.nanojson.JsonWriter; +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.localization.ContentCountry; +import org.schabi.newpipe.extractor.localization.Localization; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + +/** + * Shared functions for extracting YouTube channel pages and tabs. + */ +public final class YouTubeChannelHelper { + private YouTubeChannelHelper() { + } + + /** + * Take a YouTube channel ID or URL path, resolve it if necessary and return a channel ID. + * @param idOrPath YouTube channel ID or URL path + * @return YouTube Channel ID + */ + public static String resolveChannelId(final String idOrPath) + throws ExtractionException, IOException { + final String[] channelId = idOrPath.split("/"); + + if (channelId[0].startsWith("UC")) { + return channelId[0]; + } + + // If the url is an URL which is not a /channel URL, we need to use the + // navigation/resolve_url endpoint of the InnerTube API to get the channel id. Otherwise, + // we couldn't get information about the channel associated with this URL, if there is one. + if (!channelId[0].equals("channel")) { + final byte[] body = JsonWriter.string(YoutubeParsingHelper.prepareDesktopJsonBuilder( + Localization.DEFAULT, ContentCountry.DEFAULT) + .value("url", "https://www.youtube.com/" + idOrPath) + .done()) + .getBytes(StandardCharsets.UTF_8); + + final JsonObject jsonResponse = YoutubeParsingHelper.getJsonPostResponse( + "navigation/resolve_url", body, Localization.DEFAULT); + + checkIfChannelResponseIsValid(jsonResponse); + + final JsonObject endpoint = jsonResponse.getObject("endpoint"); + + final String webPageType = endpoint.getObject("commandMetadata") + .getObject("webCommandMetadata") + .getString("webPageType", ""); + + final JsonObject browseEndpoint = endpoint.getObject("browseEndpoint"); + final String browseId = browseEndpoint.getString("browseId", ""); + + if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE") + || webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL") + && !browseId.isEmpty()) { + if (!browseId.startsWith("UC")) { + throw new ExtractionException("Redirected id is not pointing to a channel"); + } + + return browseId; + } + } + return channelId[1]; + } + + /** + * Response data object for + * {@link #getChannelResponse(String, String, Localization, ContentCountry)} + */ + public static final class ChannelResponseData { + public final JsonObject responseJson; + public final String channelId; + + private ChannelResponseData(final JsonObject responseJson, final String channelId) { + this.responseJson = responseJson; + this.channelId = channelId; + } + } + + /** + * Fetch YouTube channel data. + *

Parameter list:

+ * + * + * @param channelId YouTube channel ID + * @param params Parameters to specify the YouTube channel tab + * @param loc YouTube localization + * @param country YouTube content country + * @return Channel response data + */ + public static ChannelResponseData getChannelResponse(final String channelId, + final String params, + final Localization loc, + final ContentCountry country) + throws ExtractionException, IOException { + String id = channelId; + JsonObject ajaxJson = null; + + int level = 0; + while (level < 3) { + final byte[] body = JsonWriter.string(YoutubeParsingHelper.prepareDesktopJsonBuilder( + loc, country) + .value("browseId", id) + .value("params", params) // Equal to videos + .done()) + .getBytes(StandardCharsets.UTF_8); + + final JsonObject jsonResponse = YoutubeParsingHelper.getJsonPostResponse( + "browse", body, loc); + + checkIfChannelResponseIsValid(jsonResponse); + + final JsonObject endpoint = jsonResponse.getArray("onResponseReceivedActions") + .getObject(0) + .getObject("navigateAction") + .getObject("endpoint"); + + final String webPageType = endpoint.getObject("commandMetadata") + .getObject("webCommandMetadata") + .getString("webPageType", ""); + + final String browseId = endpoint.getObject("browseEndpoint").getString("browseId", + ""); + + if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE") + || webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL") + && !browseId.isEmpty()) { + if (!browseId.startsWith("UC")) { + throw new ExtractionException("Redirected id is not pointing to a channel"); + } + + id = browseId; + level++; + } else { + ajaxJson = jsonResponse; + break; + } + } + + if (ajaxJson == null) { + throw new ExtractionException("Got no channel response"); + } + + YoutubeParsingHelper.defaultAlertsCheck(ajaxJson); + + return new ChannelResponseData(ajaxJson, id); + } + + /** + * Assert that a channel JSON response does not contain a 404 error. + * @param jsonResponse channel JSON response + * @throws ContentNotAvailableException if the channel was not found + */ + private static void checkIfChannelResponseIsValid(@Nonnull final JsonObject jsonResponse) + throws ContentNotAvailableException { + if (!isNullOrEmpty(jsonResponse.getObject("error"))) { + final JsonObject errorJsonObject = jsonResponse.getObject("error"); + final int errorCode = errorJsonObject.getInt("code"); + if (errorCode == 404) { + throw new ContentNotAvailableException("This channel doesn't exist."); + } else { + throw new ContentNotAvailableException("Got error:\"" + + errorJsonObject.getString("status") + "\": " + + errorJsonObject.getString("message")); + } + } + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index 1b8f8ade4..2b00515fa 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -1742,164 +1742,6 @@ public final class YoutubeParsingHelper { return false; } - /** - * Take a YouTube channel ID or URL path, resolve it if necessary and return a channel ID. - * @param idOrPath YouTube channel ID or URL path - * @return YouTube Channel ID - */ - public static String resolveChannelId(final String idOrPath) - throws ExtractionException, IOException { - final String[] channelId = idOrPath.split("/"); - - if (channelId[0].startsWith("UC")) { - return channelId[0]; - } - - // If the url is an URL which is not a /channel URL, we need to use the - // navigation/resolve_url endpoint of the InnerTube API to get the channel id. Otherwise, - // we couldn't get information about the channel associated with this URL, if there is one. - if (!channelId[0].equals("channel")) { - final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder( - Localization.DEFAULT, ContentCountry.DEFAULT) - .value("url", "https://www.youtube.com/" + idOrPath) - .done()) - .getBytes(StandardCharsets.UTF_8); - - final JsonObject jsonResponse = getJsonPostResponse("navigation/resolve_url", - body, Localization.DEFAULT); - - checkIfChannelResponseIsValid(jsonResponse); - - final JsonObject endpoint = jsonResponse.getObject("endpoint"); - - final String webPageType = endpoint.getObject("commandMetadata") - .getObject("webCommandMetadata") - .getString("webPageType", ""); - - final JsonObject browseEndpoint = endpoint.getObject("browseEndpoint"); - final String browseId = browseEndpoint.getString("browseId", ""); - - if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE") - || webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL") - && !browseId.isEmpty()) { - if (!browseId.startsWith("UC")) { - throw new ExtractionException("Redirected id is not pointing to a channel"); - } - - return browseId; - } - } - return channelId[1]; - } - - /** - * Response data object for - * {@link #getChannelResponse(String, String, Localization, ContentCountry)} - */ - public static final class ChannelResponseData { - public final JsonObject responseJson; - public final String channelId; - - private ChannelResponseData(final JsonObject responseJson, final String channelId) { - this.responseJson = responseJson; - this.channelId = channelId; - } - } - - /** - * Fetch YouTube channel data. - *

Parameter list:

- * - * - * @param channelId YouTube channel ID - * @param params Parameters to specify the YouTube channel tab - * @param loc YouTube localization - * @param country YouTube content country - * @return Channel response data - */ - public static ChannelResponseData getChannelResponse(final String channelId, - final String params, - final Localization loc, - final ContentCountry country) - throws ExtractionException, IOException { - String id = channelId; - JsonObject ajaxJson = null; - - int level = 0; - while (level < 3) { - final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder( - loc, country) - .value("browseId", id) - .value("params", params) // Equal to videos - .done()) - .getBytes(StandardCharsets.UTF_8); - - final JsonObject jsonResponse = getJsonPostResponse("browse", body, loc); - - checkIfChannelResponseIsValid(jsonResponse); - - final JsonObject endpoint = jsonResponse.getArray("onResponseReceivedActions") - .getObject(0) - .getObject("navigateAction") - .getObject("endpoint"); - - final String webPageType = endpoint.getObject("commandMetadata") - .getObject("webCommandMetadata") - .getString("webPageType", ""); - - final String browseId = endpoint.getObject("browseEndpoint").getString("browseId", - ""); - - if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE") - || webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL") - && !browseId.isEmpty()) { - if (!browseId.startsWith("UC")) { - throw new ExtractionException("Redirected id is not pointing to a channel"); - } - - id = browseId; - level++; - } else { - ajaxJson = jsonResponse; - break; - } - } - - if (ajaxJson == null) { - throw new ExtractionException("Got no channel response"); - } - - defaultAlertsCheck(ajaxJson); - - return new ChannelResponseData(ajaxJson, id); - } - - /** - * Assert that a channel JSON response does not contain a 404 error. - * @param jsonResponse channel JSON response - * @throws ContentNotAvailableException if the channel was not found - */ - private static void checkIfChannelResponseIsValid(@Nonnull final JsonObject jsonResponse) - throws ContentNotAvailableException { - if (!isNullOrEmpty(jsonResponse.getObject("error"))) { - final JsonObject errorJsonObject = jsonResponse.getObject("error"); - final int errorCode = errorJsonObject.getInt("code"); - if (errorCode == 404) { - throw new ContentNotAvailableException("This channel doesn't exist."); - } else { - throw new ContentNotAvailableException("Got error:\"" - + errorJsonObject.getString("status") + "\": " - + errorJsonObject.getString("message")); - } - } - } - /** * Generate a content playback nonce (also called {@code cpn}), sent by YouTube clients in * playback requests (and also for some clients, in the player request body). diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelExtractor.java index 5df0de62d..ae1794005 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelExtractor.java @@ -1,15 +1,15 @@ package org.schabi.newpipe.extractor.services.youtube.extractors; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.ChannelResponseData; +import static org.schabi.newpipe.extractor.services.youtube.YouTubeChannelHelper.ChannelResponseData; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.YOUTUBEI_V1_URL; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getChannelResponse; +import static org.schabi.newpipe.extractor.services.youtube.YouTubeChannelHelper.getChannelResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getKey; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.resolveChannelId; +import static org.schabi.newpipe.extractor.services.youtube.YouTubeChannelHelper.resolveChannelId; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import com.grack.nanojson.JsonArray; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java index 204fe9ddb..87d53f693 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeChannelTabExtractor.java @@ -24,14 +24,14 @@ import java.util.Collections; import java.util.List; import java.util.function.Consumer; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.ChannelResponseData; +import static org.schabi.newpipe.extractor.services.youtube.YouTubeChannelHelper.ChannelResponseData; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.YOUTUBEI_V1_URL; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getChannelResponse; +import static org.schabi.newpipe.extractor.services.youtube.YouTubeChannelHelper.getChannelResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getKey; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.resolveChannelId; +import static org.schabi.newpipe.extractor.services.youtube.YouTubeChannelHelper.resolveChannelId; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; public class YoutubeChannelTabExtractor extends ChannelTabExtractor {