This commit is contained in:
Audric V 2023-10-25 16:50:37 +03:00 committed by GitHub
commit 2de97c8718
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1752 additions and 194 deletions

View File

@ -8,6 +8,7 @@ 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.nio.charset.StandardCharsets;
import java.util.Optional;
@ -320,4 +321,46 @@ public final class YoutubeChannelHelper {
return Optional.empty();
}
}
/**
* Get a {@code channelAgeGateRenderer} object from a channel response if it exists.
*
* <p>
* A {@code channelAgeGateRenderer} is returned when a channel is age-restricted (creator seems
* to be able to set this setting), its pages are only accessible to logged-in and age-verified
* users. This renderer contains only the following channel metadata: name and avatar.
* </p>
*
* <p>
* This restriction doesn't seem to apply to all countries.
* </p>
*
* <p>
* At most one {@code channelAgeGateRenderer} should be returned per age-restricted channel
* response.
* </p>
*
* @param jsonResponse a channel JSON response
* @return the first {@code channelAgeGateRenderer} if there is one present or {@code null}
*/
@Nullable
public static JsonObject getChannelAgeGateRenderer(@Nonnull final JsonObject jsonResponse) {
return jsonResponse.getObject("contents")
.getObject("twoColumnBrowseResultsRenderer")
.getArray("tabs")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.flatMap(tab -> tab.getObject("tabRenderer")
.getObject("content")
.getObject("sectionListRenderer")
.getArray("contents")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast))
.filter(content -> content.has("channelAgeGateRenderer"))
.map(content -> content.getObject("channelAgeGateRenderer"))
.findFirst()
.orElse(null);
}
}

View File

@ -66,15 +66,6 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
private String channelId;
/**
* If a channel is age-restricted, its pages are only accessible to logged-in and
* age-verified users, we get an {@code channelAgeGateRenderer} in this case, containing only
* the following metadata: channel name and channel avatar.
*
* <p>
* This restriction doesn't seem to apply to all countries.
* </p>
*/
@Nullable
private JsonObject channelAgeGateRenderer;
@ -95,28 +86,7 @@ public class YoutubeChannelExtractor extends ChannelExtractor {
jsonResponse = data.jsonResponse;
channelHeader = YoutubeChannelHelper.getChannelHeader(jsonResponse);
channelId = data.channelId;
channelAgeGateRenderer = getChannelAgeGateRenderer();
}
@Nullable
private JsonObject getChannelAgeGateRenderer() {
return jsonResponse.getObject("contents")
.getObject("twoColumnBrowseResultsRenderer")
.getArray("tabs")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.flatMap(tab -> tab.getObject("tabRenderer")
.getObject("content")
.getObject("sectionListRenderer")
.getArray("contents")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast))
.filter(content -> content.has("channelAgeGateRenderer"))
.map(content -> content.getObject("channelAgeGateRenderer"))
.findFirst()
.orElse(null);
channelAgeGateRenderer = YoutubeChannelHelper.getChannelAgeGateRenderer(jsonResponse);
}
@Nonnull

View File

@ -61,11 +61,15 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
private String channelId;
@Nullable
private String visitorData;
@Nullable
private JsonObject channelAgeGateRenderer;
@Nullable
private YoutubeChannelTabPlaylistExtractor playlistExtractorInstance;
public YoutubeChannelTabExtractor(final StreamingService service,
final ListLinkHandler linkHandler) {
super(service, linkHandler);
useVisitorData = getName().equals(ChannelTabs.SHORTS);
useVisitorData = ChannelTabs.SHORTS.equals(getName());
}
@Nonnull
@ -101,6 +105,23 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
if (useVisitorData) {
visitorData = jsonResponse.getObject("responseContext").getString("visitorData");
}
channelAgeGateRenderer = YoutubeChannelHelper.getChannelAgeGateRenderer(jsonResponse);
if (channelAgeGateRenderer != null) {
final String channelTabName = getName();
if (ChannelTabs.VIDEOS.equals(channelTabName)
|| ChannelTabs.SHORTS.equals(channelTabName)
|| ChannelTabs.LIVESTREAMS.equals(channelTabName)) {
final ListLinkHandler originalLinkHandler = getLinkHandler();
playlistExtractorInstance =
new YoutubeChannelTabPlaylistExtractor(getService(),
new ListLinkHandler(originalLinkHandler.getOriginalUrl(),
originalLinkHandler.getUrl(),
getId(),
originalLinkHandler.getContentFilters(),
originalLinkHandler.getSortFilter()));
playlistExtractorInstance.fetchPage();
}
}
}
@Nonnull
@ -117,29 +138,31 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
@Nonnull
@Override
public String getId() throws ParsingException {
final String id = jsonResponse.getObject("header")
.getObject("c4TabbedHeaderRenderer")
.getString("channelId", "");
if (channelAgeGateRenderer == null) {
final String id = jsonResponse.getObject("header")
.getObject("c4TabbedHeaderRenderer")
.getString("channelId", "");
if (!id.isEmpty()) {
return id;
}
if (!id.isEmpty()) {
return id;
}
final Optional<String> carouselHeaderId = jsonResponse.getObject("header")
.getObject("carouselHeaderRenderer")
.getArray("contents")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.filter(item -> item.has("topicChannelDetailsRenderer"))
.findFirst()
.flatMap(item ->
Optional.ofNullable(item.getObject("topicChannelDetailsRenderer")
.getObject("navigationEndpoint")
.getObject("browseEndpoint")
.getString("browseId")));
if (carouselHeaderId.isPresent()) {
return carouselHeaderId.get();
final Optional<String> carouselHeaderId = jsonResponse.getObject("header")
.getObject("carouselHeaderRenderer")
.getArray("contents")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.filter(item -> item.has("topicChannelDetailsRenderer"))
.findFirst()
.flatMap(item ->
Optional.ofNullable(item.getObject("topicChannelDetailsRenderer")
.getObject("navigationEndpoint")
.getObject("browseEndpoint")
.getString("browseId")));
if (carouselHeaderId.isPresent()) {
return carouselHeaderId.get();
}
}
if (!isNullOrEmpty(channelId)) {
@ -150,6 +173,10 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
}
protected String getChannelName() {
if (channelAgeGateRenderer != null) {
return channelAgeGateRenderer.getString("channelTitle");
}
final String metadataName = jsonResponse.getObject("metadata")
.getObject("channelMetadataRenderer")
.getString("title");
@ -176,6 +203,13 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
@Nonnull
@Override
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
if (channelAgeGateRenderer != null) {
if (playlistExtractorInstance != null) {
return playlistExtractorInstance.getInitialPage();
}
return InfoItemsPage.emptyPage();
}
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
JsonArray items = new JsonArray();
@ -223,6 +257,13 @@ public class YoutubeChannelTabExtractor extends ChannelTabExtractor {
@Override
public InfoItemsPage<InfoItem> getPage(final Page page)
throws IOException, ExtractionException {
if (channelAgeGateRenderer != null) {
if (playlistExtractorInstance != null) {
return playlistExtractorInstance.getPage(page);
}
return InfoItemsPage.emptyPage();
}
if (page == null || isNullOrEmpty(page.getUrl())) {
throw new IllegalArgumentException("Page doesn't contain an URL");
}

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.extractor.services.youtube;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
@ -43,7 +42,6 @@ class YoutubeChannelTabExtractorTest {
@Override public String expectedUrlContains() throws Exception { return "https://www.youtube.com/channel/UCTwECeGqMZee77BjdoYtI2Q/videos"; }
@Override public String expectedOriginalUrlContains() throws Exception { return "https://www.youtube.com/user/creativecommons/videos"; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
@Override public boolean expectedHasMoreItems() { return true; }
}
static class Playlists extends DefaultListExtractorTest<ChannelTabExtractor> {
@ -65,7 +63,6 @@ class YoutubeChannelTabExtractorTest {
@Override public String expectedUrlContains() throws Exception { return "https://www.youtube.com/channel/UC2DjFE7Xf11URZqWBigcVOQ/playlists"; }
@Override public String expectedOriginalUrlContains() throws Exception { return "https://www.youtube.com/@EEVblog/playlists"; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.PLAYLIST; }
@Override public boolean expectedHasMoreItems() { return true; }
}
static class Channels extends DefaultListExtractorTest<ChannelTabExtractor> {
@ -87,7 +84,6 @@ class YoutubeChannelTabExtractorTest {
@Override public String expectedUrlContains() throws Exception { return "https://www.youtube.com/channel/UC2DjFE7Xf11URZqWBigcVOQ/channels"; }
@Override public String expectedOriginalUrlContains() throws Exception { return "https://www.youtube.com/channel/UC2DjFE7Xf11URZqWBigcVOQ/channels"; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.CHANNEL; }
@Override public boolean expectedHasMoreItems() { return true; }
}
static class Livestreams extends DefaultListExtractorTest<ChannelTabExtractor> {
@ -109,7 +105,6 @@ class YoutubeChannelTabExtractorTest {
@Override public String expectedUrlContains() throws Exception { return "https://www.youtube.com/channel/UCR-DXc1voovS8nhAvccRZhg/streams"; }
@Override public String expectedOriginalUrlContains() throws Exception { return "https://www.youtube.com/c/JeffGeerling/streams"; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
@Override public boolean expectedHasMoreItems() { return true; }
}
static class Shorts extends DefaultListExtractorTest<ChannelTabExtractor> {
@ -131,32 +126,12 @@ class YoutubeChannelTabExtractorTest {
@Override public String expectedUrlContains() throws Exception { return "https://www.youtube.com/channel/UCh8gHdtzO2tXd593_bjErWg/shorts"; }
@Override public String expectedOriginalUrlContains() throws Exception { return "https://www.youtube.com/channel/UCh8gHdtzO2tXd593_bjErWg/shorts"; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
@Override public boolean expectedHasMoreItems() { return true; }
}
private static abstract class AgeRestrictedTabsVideosBaseTest
extends DefaultListExtractorTest<ChannelTabExtractor> {
// TESTS FOR TABS OF AGE RESTRICTED CHANNELS
// Fetching the tabs individually would use the standard tabs without fallback to
// system playlists for stream tabs, we need to fetch the channel extractor to get the
// channel playlist tabs
// TODO: implement system playlists fallback in YoutubeChannelTabExtractor for stream
// tabs
static class AgeRestrictedTabsVideos extends DefaultListExtractorTest<ChannelTabExtractor> {
private static ChannelTabExtractor extractor;
@BeforeAll
static void setUp() throws IOException, ExtractionException {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "ageRestrictedTabsVideos"));
final ChannelExtractor channelExtractor = YouTube.getChannelExtractor(
"https://www.youtube.com/channel/UCbfnHqxXs_K3kvaH-WlNlig");
channelExtractor.fetchPage();
// the videos tab is the first one
extractor = YouTube.getChannelTabExtractor(channelExtractor.getTabs().get(0));
extractor.fetchPage();
}
protected static ChannelTabExtractor extractor;
@Override public ChannelTabExtractor extractor() throws Exception { return extractor; }
@Override public StreamingService expectedService() throws Exception { return YouTube; }
@ -165,7 +140,18 @@ class YoutubeChannelTabExtractorTest {
@Override public String expectedUrlContains() throws Exception { return "https://www.youtube.com/channel/UCbfnHqxXs_K3kvaH-WlNlig/videos"; }
@Override public String expectedOriginalUrlContains() throws Exception { return "https://www.youtube.com/channel/UCbfnHqxXs_K3kvaH-WlNlig/videos"; }
@Override public InfoItem.InfoType expectedInfoItemType() { return InfoItem.InfoType.STREAM; }
@Override public boolean expectedHasMoreItems() { return true; }
}
static class AgeRestrictedTabsVideos extends AgeRestrictedTabsVideosBaseTest {
@BeforeAll
static void setUp() throws IOException, ExtractionException {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(
RESOURCE_PATH + "ageRestrictedTabsVideos"));
extractor = YouTube.getChannelTabExtractorFromId(
"channel/UCbfnHqxXs_K3kvaH-WlNlig", ChannelTabs.VIDEOS);
extractor.fetchPage();
}
}
static class AgeRestrictedTabsShorts extends DefaultListExtractorTest<ChannelTabExtractor> {
@ -174,13 +160,10 @@ class YoutubeChannelTabExtractorTest {
@BeforeAll
static void setUp() throws IOException, ExtractionException {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "ageRestrictedTabsShorts"));
final ChannelExtractor channelExtractor = YouTube.getChannelExtractor(
"https://www.youtube.com/channel/UCbfnHqxXs_K3kvaH-WlNlig");
channelExtractor.fetchPage();
// the shorts tab is the second one
extractor = YouTube.getChannelTabExtractor(channelExtractor.getTabs().get(1));
NewPipe.init(DownloaderFactory.getDownloader(
RESOURCE_PATH + "ageRestrictedTabsShorts"));
extractor = YouTube.getChannelTabExtractorFromId(
"channel/UCbfnHqxXs_K3kvaH-WlNlig", ChannelTabs.SHORTS);
extractor.fetchPage();
}
@ -195,9 +178,26 @@ class YoutubeChannelTabExtractorTest {
@Test
@Override
public void testRelatedItems() throws Exception {
// this channel has no shorts, so an empty page is returned by the playlist extractor
// This channel has no shorts, so an empty page should be returned by the playlist
// extractor
assertTrue(extractor.getInitialPage().getItems().isEmpty());
assertTrue(extractor.getInitialPage().getErrors().isEmpty());
}
}
static class AgeRestrictedTabsVideosFromChannel extends AgeRestrictedTabsVideosBaseTest {
@BeforeAll
static void setUp() throws IOException, ExtractionException {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(
RESOURCE_PATH + "ageRestrictedTabsVideosFromChannel"));
final ChannelExtractor channelExtractor = YouTube.getChannelExtractor(
"https://www.youtube.com/channel/UCbfnHqxXs_K3kvaH-WlNlig");
channelExtractor.fetchPage();
// the videos tab is the first one
extractor = YouTube.getChannelTabExtractor(channelExtractor.getTabs().get(0));
extractor.fetchPage();
}
}
}

View File

@ -34,20 +34,17 @@
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy-report-only": [
"require-trusted-types-for \u0027script\u0027;report-uri /cspreport"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy": [
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Sun, 06 Aug 2023 11:54:54 GMT"
"Sun, 27 Aug 2023 13:58:38 GMT"
],
"expires": [
"Sun, 06 Aug 2023 11:54:54 GMT"
"Sun, 27 Aug 2023 13:58:38 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
@ -65,9 +62,10 @@
"ESF"
],
"set-cookie": [
"YSC\u003dIXSPROQ-CH8; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dMon, 09-Nov-2020 11:54:54 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"CONSENT\u003dPENDING+942; expires\u003dTue, 05-Aug-2025 11:54:54 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
"YSC\u003dfX4diOWsOao; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dMon, 30-Nov-2020 13:58:38 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJGUhIA; Domain\u003d.youtube.com; Expires\u003dFri, 23-Feb-2024 13:58:38 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dlax",
"CONSENT\u003dPENDING+200; expires\u003dTue, 26-Aug-2025 13:58:38 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -41,10 +41,10 @@
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Sun, 06 Aug 2023 11:55:07 GMT"
"Sun, 27 Aug 2023 13:58:22 GMT"
],
"expires": [
"Sun, 06 Aug 2023 11:55:07 GMT"
"Sun, 27 Aug 2023 13:58:22 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
@ -62,9 +62,10 @@
"ESF"
],
"set-cookie": [
"YSC\u003dqCdA3bmpLFQ; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dMon, 09-Nov-2020 11:55:07 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"CONSENT\u003dPENDING+717; expires\u003dTue, 05-Aug-2025 11:55:07 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
"YSC\u003dVBy9_nvvJ1s; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dMon, 30-Nov-2020 13:58:22 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJGUhIA; Domain\u003d.youtube.com; Expires\u003dFri, 23-Feb-2024 13:58:22 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dlax",
"CONSENT\u003dPENDING+755; expires\u003dTue, 26-Aug-2025 13:58:22 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
],
"strict-transport-security": [
"max-age\u003d31536000"

View File

@ -0,0 +1,86 @@
{
"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-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Sun, 27 Aug 2023 13:58:55 GMT"
],
"expires": [
"Sun, 27 Aug 2023 13:58:55 GMT"
],
"origin-trial": [
"AvC9UlR6RDk2crliDsFl66RWLnTbHrDbp+DiY6AYz/PNQ4G4tdUTjrHYr2sghbkhGQAVxb7jaPTHpEVBz0uzQwkAAAB4eyJvcmlnaW4iOiJodHRwczovL3lvdXR1YmUuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJWaWV3WFJlcXVlc3RlZFdpdGhEZXByZWNhdGlvbiIsImV4cGlyeSI6MTcxOTUzMjc5OSwiaXNTdWJkb21haW4iOnRydWV9"
],
"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-factor\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\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dzUzmQXpaKn8; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dMon, 30-Nov-2020 13:58:55 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_PRIVACY_METADATA\u003dCgJGUhIA; Domain\u003d.youtube.com; Expires\u003dFri, 23-Feb-2024 13:58:55 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dlax",
"CONSENT\u003dPENDING+311; expires\u003dTue, 26-Aug-2025 13:58:55 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
],
"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"
}
}