Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
package org.schabi.newpipe.extractor.services.youtube;

import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.defaultAlertsCheck;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.hasArtistOrVerifiedIconBadgeAttachment;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;

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.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.defaultAlertsCheck;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.hasArtistOrVerifiedIconBadgeAttachment;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
* Shared functions for extracting YouTube channel pages and tabs.
Expand Down Expand Up @@ -65,35 +67,59 @@ public static String resolveChannelId(@Nonnull final String idOrPath)
// URL, then no information about the channel associated with this URL was found,
// so the unresolved url will be returned.
if (!channelId[0].equals("channel")) {
final byte[] body = JsonWriter.string(
prepareDesktopJsonBuilder(Localization.DEFAULT, ContentCountry.DEFAULT)
.value("url", "https://www.youtube.com/" + idOrPath)
String urlToResolve = "https://www.youtube.com/" + idOrPath;

JsonObject endpoint = new JsonObject();
String webPageType = "";
// Try to resolve YT channel redirects
// It works like that:
// @TheDailyShow
// -> resolves to thedailyshow
// -> resolves to the id: UCwWhs_6x42TyRM4Wstoq8HA
// Please note that this is not always the case, some handles
// e.g. @google or @Gronkh directly resolve the id
Comment on lines +75 to +80
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really important, but I wonder whether the contrary is more common instead. If so, this comment should updated.

for (int tries = 0;
urlToResolve != null && tries < 3;
tries++) {
final byte[] body = JsonWriter.string(
prepareDesktopJsonBuilder(Localization.DEFAULT, ContentCountry.DEFAULT)
.value("url", urlToResolve)
.done())
.getBytes(StandardCharsets.UTF_8);

final JsonObject jsonResponse = getJsonPostResponse(
final JsonObject jsonResponse = getJsonPostResponse(
"navigation/resolve_url", body, Localization.DEFAULT);

checkIfChannelResponseIsValid(jsonResponse);
checkIfChannelResponseIsValid(jsonResponse);

final JsonObject endpoint = jsonResponse.getObject("endpoint");
endpoint = jsonResponse.getObject("endpoint");

final String webPageType = endpoint.getObject("commandMetadata")
webPageType = endpoint.getObject("commandMetadata")
.getObject("webCommandMetadata")
.getString("webPageType", "");
.getString("webPageType");

urlToResolve = "WEB_PAGE_TYPE_UNKNOWN".equals(webPageType)
? endpoint.getObject("urlEndpoint").getString("url")
: null;
}

final JsonObject browseEndpoint = endpoint.getObject(BROWSE_ENDPOINT);
final String browseId = browseEndpoint.getString(BROWSE_ID, "");
final String browseId = endpoint.getObject(BROWSE_ENDPOINT)
.getString(BROWSE_ID, "");

if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE")
|| webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL")
if (("WEB_PAGE_TYPE_BROWSE".equalsIgnoreCase(webPageType)
|| "WEB_PAGE_TYPE_CHANNEL".equalsIgnoreCase(webPageType))
&& !browseId.isEmpty()) {
if (!browseId.startsWith("UC")) {
throw new ExtractionException("Redirected id is not pointing to a channel");
}

return browseId;
}

// Otherwise, the code after that will run into an IndexOutOfBoundsException
if (channelId.length < 2) {
throw new ExtractionException("Failed to resolve channelId for " + idOrPath);
}
}

// return the unresolved URL
Expand Down Expand Up @@ -175,13 +201,13 @@ public static ChannelResponseData getChannelResponse(@Nonnull final String chann

final String webPageType = endpoint.getObject("commandMetadata")
.getObject("webCommandMetadata")
.getString("webPageType", "");
.getString("webPageType");

final String browseId = endpoint.getObject(BROWSE_ENDPOINT)
.getString(BROWSE_ID, "");

if (webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_BROWSE")
|| webPageType.equalsIgnoreCase("WEB_PAGE_TYPE_CHANNEL")
if (("WEB_PAGE_TYPE_BROWSE".equalsIgnoreCase(webPageType)
|| "WEB_PAGE_TYPE_CHANNEL".equalsIgnoreCase(webPageType))
&& !browseId.isEmpty()) {
if (!browseId.startsWith("UC")) {
throw new ExtractionException("Redirected id is not pointing to a channel");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,10 @@ public String getUploaderUrl() throws ParsingException {
throw new ParsingException("Could not get uploader url");
}

private String resolveUploaderUrlFromRelativeUrl(final String url) throws ParsingException {
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl("c" + url);
private String resolveUploaderUrlFromRelativeUrl(final String relativeUrl)
throws ParsingException {
return YoutubeChannelLinkHandlerFactory.getInstance().getUrl(
relativeUrl.startsWith("/") ? relativeUrl.substring(1) : relativeUrl);
}

@Nonnull
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.schabi.newpipe.extractor.services.youtube;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;

class YouTubeChannelHelperTest implements InitYoutubeTest {

@ParameterizedTest
@ValueSource(strings = {
"@TheDailyShow",
"thedailyshow",
"channel/UCwWhs_6x42TyRM4Wstoq8HA",
"UCwWhs_6x42TyRM4Wstoq8HA"
})
void resolveSuccessfulTheDailyShow(final String idOrPath) {
final String id = assertDoesNotThrow(
() -> YoutubeChannelHelper.resolveChannelId(idOrPath));
assertEquals("UCwWhs_6x42TyRM4Wstoq8HA", id);
}

@ParameterizedTest
@ValueSource(strings = {
"@Gronkh",
"gronkh",
"channel/UCYJ61XIK64sp6ZFFS8sctxw",
"UCYJ61XIK64sp6ZFFS8sctxw"
})
void resolveSuccessfulGronkh(final String idOrPath) {
final String id = assertDoesNotThrow(
() -> YoutubeChannelHelper.resolveChannelId(idOrPath));
assertEquals("UCYJ61XIK64sp6ZFFS8sctxw", id);
}

@Test
void resolveFailNonExistingTag() {
assertThrows(ExtractionException.class, () -> YoutubeChannelHelper.resolveChannelId(
"@nonExistingHandleThatWillNeverExist15464"));
Comment thread
litetex marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Referer": [
"https://www.youtube.com"
],
"Origin": [
"https://www.youtube.com"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"access-control-allow-credentials": [
"true"
],
"access-control-allow-origin": [
"https://www.youtube.com"
],
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy": [
"require-trusted-types-for \u0027script\u0027"
],
"content-security-policy-report-only": [
"script-src \u0027unsafe-eval\u0027 \u0027self\u0027 \u0027unsafe-inline\u0027 https://www.google.com https://apis.google.com https://ssl.gstatic.com https://www.gstatic.com https://www.googletagmanager.com https://www.google-analytics.com https://*.youtube.com https://*.google.com https://*.gstatic.com https://youtube.com https://www.youtube.com https://google.com https://*.doubleclick.net https://*.googleapis.com https://www.googleadservices.com https://tpc.googlesyndication.com https://www.youtubekids.com;report-uri /cspreport/allowlist"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy": [
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Thu, 31 Jul 2025 09:08:16 GMT"
],
"document-policy": [
"include-js-call-stacks-in-crash-reports"
],
"expires": [
"Thu, 31 Jul 2025 09:08:16 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-form-factors\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"reporting-endpoints": [
"default\u003d\"/web-reports?context\u003deJwNz3tIU3EUB3Bv3zLdnbv3d4RCDRIRimyi0ymWJhQ-Ei2DInGWOt18NKfNuzXLIsIsSaKwUmsFFaRU2gMKZFr9ERiBFT7-yJQeIiFZ1h-ZZGXnjw-Hw5dzOEfnWzEWeUCqXqqRorOd0v2jB6WidS4p43W9lLdKk1ozNWn-lib1-DVpNtIjWaI8UseYR3K890qLU17pmlqwzB5esMzL_IU62O06_OzU4ceEDg1zOlhiZFCsjJEcGbM7ZDzukuF_KGPyswy3RY_vJXokuvWonddjJj8EA50hGB4JwcbdBqS2GtDdZ0D-oAHnWcUfA779NSApTUFvlgKPTUG3Q4HsUXC5RUF8u4IsFnRdweqXCgaiVQSVqNAx80UVw1dUBN9T4RtSMfFLxebfKvaFCmgbBPoTBUrNAkmbBJrSBXx7BLYXC0yyvAqBsmqBGw0CjY2cM-tJga1nBF61cj0rMNcmYLogsOWSwNPbApF3BN71CET0Ciw9EQh6wf2QQOiowPhbgTUzAslzAp8WBCoX2T-BcwEEQzChJozwLJzQF0HoWEvwsakowsB6QkICwcwCzYShFEJpKqE-ndC1jfAokzDOYrIJD9hSDqEwl1DE9rMSZmXlzM4qWTVzMCerYy6mMQ_zssOskV3dSQjYRWhmX_YSci2Ej0U8U0yoqOAdbJEFu_m-I5wfI4SdICywvGbCVyafIqSxptOElS2EgjbC8nb-8ybf1MW_dhM-9BAy_DzTT1AGCaQPOj5993mg-mZ6tAVRxoZat-a22mIP2axGu6vWqRltznJjmatKqyordRSb4kzmuGRTSmx8XHFd3H8KrcfX\""
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dYhEHSiU9_R8; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dFri, 04-Nov-2022 09:08:16 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "\n self.addEventListener(\u0027install\u0027, event \u003d\u003e {\n event.waitUntil(self.skipWaiting());\n });\n self.addEventListener(\u0027activate\u0027, event \u003d\u003e {\n event.waitUntil(\n self.clients.claim().then(() \u003d\u003e self.registration.unregister()));\n });\n ",
"latestUrl": "https://www.youtube.com/sw.js"
}
}

Large diffs are not rendered by default.

Loading