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
Expand Up @@ -759,6 +759,22 @@ public static String getUrlFromNavigationEndpoint(
.getString("playlistId");
}

if (navigationEndpoint.has("showDialogCommand")) {
try {
final JsonArray listItems = JsonUtils.getArray(navigationEndpoint,
"showDialogCommand.panelLoadingStrategy.inlineContent.dialogViewModel"
+ ".customContent.listViewModel.listItems");

// the first item seems to always be the channel that actually uploaded the video,
// i.e. it appears in their video feed
final JsonObject command = JsonUtils.getObject(listItems.getObject(0),
"listItemViewModel.rendererContext.commandContext.onTap.innertubeCommand");
return getUrlFromNavigationEndpoint(command);
} catch (final ParsingException p) {
}
}


if (navigationEndpoint.has("commandMetadata")) {
final JsonObject metadata = navigationEndpoint.getObject("commandMetadata")
.getObject("webCommandMetadata");
Expand Down Expand Up @@ -1572,4 +1588,24 @@ public static JsonBuilder<JsonObject> prepareJsonBuilder(

return builder;
}

/**
* Gets the first collaborator, which is the channel that owns the video,
* i.e. the video is displayed on their channel page.
*
* @param navigationEndpoint JSON object for the navigationEndpoint
* @return The first collaborator in the JSON object or {@code null}
*/
@Nullable
public static JsonObject getFirstCollaborator(final JsonObject navigationEndpoint)
throws ParsingException {
try {
// CHECKSTYLE:OFF
final JsonArray listItems = JsonUtils.getArray(navigationEndpoint, "showDialogCommand.panelLoadingStrategy.inlineContent.dialogViewModel.customContent.listViewModel.listItems");
// CHECKSTYLE:ON
return listItems.getObject(0).getObject("listItemViewModel");
} catch (final ParsingException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -541,23 +541,47 @@ public String getUploaderName() throws ParsingException {

@Override
public boolean isUploaderVerified() throws ParsingException {
return YoutubeParsingHelper.isVerified(
getVideoSecondaryInfoRenderer()
final JsonObject videoOwnerRenderer = getVideoSecondaryInfoRenderer()
.getObject("owner")
.getObject("videoOwnerRenderer")
.getArray("badges"));
.getObject("videoOwnerRenderer");

if (videoOwnerRenderer.has("badges")) {
return YoutubeParsingHelper.isVerified(videoOwnerRenderer
.getArray("badges"));
}


final JsonObject channel = YoutubeParsingHelper.getFirstCollaborator(
videoOwnerRenderer.getObject("navigationEndpoint"));
if (channel == null) {
return false;
}

return YoutubeParsingHelper.hasArtistOrVerifiedIconBadgeAttachment(
channel.getObject("title").getArray("attachmentRuns"));
}

@Nonnull
@Override
public List<Image> getUploaderAvatars() throws ParsingException {
assertPageFetched();

final List<Image> imageList = getImagesFromThumbnailsArray(
getVideoSecondaryInfoRenderer().getObject("owner")
.getObject("videoOwnerRenderer")
.getObject("thumbnail")
.getArray("thumbnails"));
final JsonObject owner = getVideoSecondaryInfoRenderer().getObject("owner")
.getObject("videoOwnerRenderer");

final List<Image> imageList;
if (owner.has("avatarStack")) {
imageList = getImagesFromThumbnailsArray(
owner.getObject("avatarStack").getObject("avatarStackViewModel")
.getArray("avatars")
// only consider the first collaborator, which is the video owner
.getObject(0)
.getObject("avatarViewModel")
.getObject("image")
.getArray("sources"));
} else {
imageList = getImagesFromThumbnailsArray(
owner.getObject("thumbnail").getArray("thumbnails"));
}

if (imageList.isEmpty() && ageLimit == NO_AGE_LIMIT) {
throw new ParsingException("Could not get uploader avatars");
Expand All @@ -570,12 +594,24 @@ public List<Image> getUploaderAvatars() throws ParsingException {
public long getUploaderSubscriberCount() throws ParsingException {
final JsonObject videoOwnerRenderer = JsonUtils.getObject(videoSecondaryInfoRenderer,
"owner.videoOwnerRenderer");
if (!videoOwnerRenderer.has("subscriberCountText")) {

String subscriberCountText = null;
if (videoOwnerRenderer.has("subscriberCountText")) {
subscriberCountText = getTextFromObject(videoOwnerRenderer
.getObject("subscriberCountText"));
} else {
final String content = YoutubeParsingHelper.getFirstCollaborator(
videoOwnerRenderer.getObject("navigationEndpoint")
).getObject("subtitle").getString("content");
subscriberCountText = content.split("•")[1];
}

if (isNullOrEmpty(subscriberCountText)) {
return UNKNOWN_SUBSCRIBER_COUNT;
}

try {
return Utils.mixedNumberWordToLong(getTextFromObject(videoOwnerRenderer
.getObject("subscriberCountText")));
return Utils.mixedNumberWordToLong(subscriberCountText);
} catch (final NumberFormatException e) {
throw new ParsingException("Could not get uploader subscriber count", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,56 @@ public void testSearchSuggestion() throws Exception {
}
}

public static class MultipleUploader extends DefaultSearchExtractorTest implements InitYoutubeTest {
private static final String QUERY = "Nxk6aRHi664";

@Override
protected SearchExtractor createExtractor() throws Exception {
return YouTube.getSearchExtractor(QUERY, singletonList(VIDEOS), "");
}

@Override public StreamingService expectedService() { return YouTube; }
@Override public String expectedName() { return QUERY; }
@Override public String expectedId() { return QUERY; }
@Override public String expectedUrlContains() { return "youtube.com/results?search_query=" + QUERY; }
@Override public String expectedOriginalUrlContains() { return "youtube.com/results?search_query=" + QUERY; }
@Override public String expectedSearchString() { return QUERY; }
@Nullable @Override public String expectedSearchSuggestion() { return null; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }

@Test
void testUploaderName() throws IOException, ExtractionException {
final List<InfoItem> items = extractor().getInitialPage().getItems();
assertEquals("Le Vortex - ARTE and Thomas Gauthier",
((StreamInfoItem) items.get(0)).getUploaderName());
}

@Test
void testUploaderUrl() throws IOException, ExtractionException {
final List<InfoItem> items = extractor().getInitialPage().getItems();
assertEquals("https://www.youtube.com/channel/UCZxLew-WXWm5dhRZBgEFl-Q",
((StreamInfoItem) items.get(0)).getUploaderUrl());
}
@Test
void testUploaderAvatars() throws IOException, ExtractionException {
final List<InfoItem> items = extractor().getInitialPage().getItems();
assertNotNull(((StreamInfoItem) items.get(0)).getUploaderAvatars());
}

@Disabled("Irrelevant - sometimes suggestions show up, sometimes not")
@Override
public void testSearchSuggestion() throws Exception {
super.testSearchSuggestion();
}

@Disabled("Irrelevant - sometimes suggestions show up, sometimes not")
@Override
public void testMoreRelatedItems() throws Exception {
super.testMoreRelatedItems();
}
}


public static class ShortFormContent extends DefaultSearchExtractorTest implements InitYoutubeTest {
private static final String QUERY = "#shorts";

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

import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import static org.schabi.newpipe.extractor.ExtractorAsserts.assertNotEmpty;
import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestImageCollection;

import org.junit.jupiter.api.Disabled;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest;
import org.schabi.newpipe.extractor.services.youtube.InitYoutubeTest;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;

import java.util.Collections;
import java.util.List;

import javax.annotation.Nullable;

public class YoutubeStreamExtractorCollaboratorsTest extends DefaultStreamExtractorTest
implements InitYoutubeTest {
private static final String ID = "3sbYbckT1VY";
private static final String URL = YoutubeStreamExtractorDefaultTest.BASE_URL + ID;

@Override
protected StreamExtractor createExtractor() throws Exception {
return YouTube.getStreamExtractor(URL);
}

@Override public StreamingService expectedService() { return YouTube; }
@Override public String expectedName() { return "Engineers vs Pumpkin Carving 2.0"; }
@Override public String expectedId() { return ID; }
@Override public String expectedUrlContains() { return YoutubeStreamExtractorDefaultTest.BASE_URL + ID; }
@Override public String expectedOriginalUrlContains() { return URL; }

@Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; }
@Override public String expectedUploaderName() { return "CrunchLabs"; }
@Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UC513PdAP2-jWkJunTh5kXRw"; }
@Override public long expectedUploaderSubscriberCountAtLeast() { return 227_0000; }
@Override public boolean expectedUploaderVerified() { return true; }
@Override public boolean expectedDescriptionIsEmpty() { return false; }
@Override public List<String> expectedDescriptionContains() { return Collections.emptyList(); }
@Override public long expectedLength() { return 696; }
@Override public long expectedViewCountAtLeast() { return 1_400_000; }
@Nullable @Override public String expectedUploadDate() { return "2025-10-25 15:33:05.000"; }
@Nullable @Override public String expectedTextualUploadDate() { return "2025-10-25T08:33:05-07:00"; }
@Override public long expectedLikeCountAtLeast() { return 20_000; }
@Override public long expectedDislikeCountAtLeast() { return -1; }
@Override public boolean expectedHasSubtitles() { return true; }
@Override public boolean expectedHasFrames() { return true; }

@Override public String expectedCategory() { return "Science & Technology"; }

@Override public String expectedLicence() { return "YouTube licence"; }
@Override
public List<String> expectedTags() {
return Collections.emptyList();
}

@Override
public void testUploaderAvatars() throws Exception {
List<Image> avatars = extractor().getUploaderAvatars();
assertNotEmpty(avatars);
defaultTestImageCollection(avatars);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"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 https://www.youtube-nocookie.com https://www.youtubeeducation.com https://www-onepick-opensocial.googleusercontent.com;report-uri /cspreport/allowlist"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy": [
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Fri, 31 Oct 2025 15:26:13 GMT"
],
"document-policy": [
"include-js-call-stacks-in-crash-reports"
],
"expires": [
"Fri, 31 Oct 2025 15:26:13 GMT"
],
"origin-trial": [
"AmhMBR6zCLzDDxpW+HfpP67BqwIknWnyMOXOQGfzYswFmJe+fgaI6XZgAzcxOrzNtP7hEDsOo1jdjFnVr2IdxQ4AAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTc1ODA2NzE5OSwiaXNTdWJkb21haW4iOnRydWV9",
"AiDEBptUfVeO93q48VdVMe/ubupazdAl8AaHP+NBzdnW8quUcHdzJUyGSfrmtpKJu7EOvwRp9ug2rEo3XU+WMAMAAAB2eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJEZXZpY2VCb3VuZFNlc3Npb25DcmVkZW50aWFsczIiLCJleHBpcnkiOjE3NzQzMTA0MDAsImlzU3ViZG9tYWluIjp0cnVlfQ\u003d\u003d"
],
"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\u003deJwNzmtsjGkYxnFv_3Z0Z6bzzvvcjxQlbLU2sabaJiWOH0irS4Zu4jw2bTEtQquzM0MtiU1JEMdYp9o6bHZV1CllJXRtgw_OuqF0lwqpD9IQrETVsft8-H24k_u6crmbvyjuWmKl5ZdZrm-XWidXVVhzvo5YuU0_WBvzolbHoah1rCFqPR8Qt0KpcWv33bi1-NFya59_ZsLyPjMTGma7KSlx82aPm_9a3VS-dBP6xsOdiR6eT_ZwptZDQ72Hh089xEJeXhV5yYl5Ke_w0l6QxPk9SQyZ6uPwWR8Fl31sM1588pGWZbNqhM2wMTbHJ9jEwzaeuE31epusXTaJB22Sr9msb7M5n-YnsciP27i910-PE35a3_oZ9d7P9-JQlOPwyzSHh4UOwVKHlSsd1hiT1jqM2-wwdqdD4xGHAXUOKccdpNmhb7tDW6eD70tF_lDFipGK-3mKromKYEjRK66wVivUTsWCWsUiY4VRZVxsVIy5pUhpUoy-rfjqriK9RVH_RJHcYbKdig2fFS0uodkjDO0pBPsYKUJdf0FShfx0Yb5RmyEEcoR_RwkV44XioBCbInQae2cL40PCwLDJlQrucmG78ahCWBsxd5VQsk6YtUPI2CX8US2sqRF8B4Sag8KWX4V-v4vZLVyoExKOCleNP08Jj88KReeEk5eE6BVh_1Xzc134-4bw3U3T3SxYLULeP0KlkfzAbDbuGaWtQrfHJtMmbHoqpD8Tfn4tHO0QkjqFd0buO-Evo91oei98_GA2fhQiXcL07hq8mp98mqijuag1h3trXAM1wUGaQ4M104do6odrto3VVOVqTk_WVM_QvC3U_JjSzaW8iTuObL_i8m9t3PSblRqoLI9FY3PDGcvCcwMlkfKyaCBcNj8wL7IwunBe8eLC7MzsnKzM7BEZWZmFSzP_B5923l0\""
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dEPX_t6VvDa8; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dSat, 04-Feb-2023 15:26:13 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"
}
}
Loading