1
0
mirror of https://github.com/TeamNewPipe/NewPipeExtractor synced 2025-10-05 16:12:47 +02:00

Merge pull request #1280 from FineFindus/feat/member-channeltab-info

This commit is contained in:
Stypox
2025-10-04 16:07:49 +02:00
committed by GitHub
16 changed files with 1258 additions and 0 deletions

View File

@@ -35,6 +35,7 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.ContentAvailability;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.Utils;
@@ -470,4 +471,33 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
throw new ParsingException("Could not determine if this is short-form content", e);
}
}
private boolean isMembersOnly() throws ParsingException {
return videoInfo.getArray("badges")
.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.map(badge -> badge.getObject("metadataBadgeRenderer").getString("style"))
.anyMatch("BADGE_STYLE_TYPE_MEMBERS_ONLY"::equals);
}
@Nonnull
@Override
public ContentAvailability getContentAvailability() throws ParsingException {
if (isPremiere()) {
return ContentAvailability.UPCOMING;
}
if (isMembersOnly()) {
return ContentAvailability.MEMBERSHIP;
}
if (isPremium()) {
return ContentAvailability.PAID;
}
return ContentAvailability.AVAILABLE;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Created by FineFindus on 10.07.25.
*
* Copyright (C) 2025 FineFindus <FineFindus@proton.me>
* ContentAvailability.java is part of NewPipe Extractor.
*
* NewPipe Extractor is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe Extractor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe Extractor. If not, see <https://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.extractor.stream;
/**
* Availability of the stream.
*
* <p>A stream may be available to all, restricted to a certain user group or time.</p>
*/
public enum ContentAvailability {
/**
* The availability of the stream is unknown (but clients may assume that it's available).
*/
UNKNOWN,
/**
* The stream is available to all users.
*/
AVAILABLE,
/**
* The stream is available to users with a membership.
*/
MEMBERSHIP,
/**
* The stream is behind a paywall.
*/
PAID,
/**
* The stream is only available in the future.
*/
UPCOMING,
}

View File

@@ -581,6 +581,17 @@ public abstract class StreamExtractor extends Extractor {
return false;
}
/**
* Get the availability of the stream.
*
* @return The stream's availability
* @throws ParsingException if there is an error in the extraction
*/
@Nonnull
public ContentAvailability getContentAvailability() throws ParsingException {
return ContentAvailability.UNKNOWN;
}
public enum Privacy {
PUBLIC,
UNLISTED,

View File

@@ -330,6 +330,11 @@ public class StreamInfo extends Info {
} catch (final Exception e) {
streamInfo.addError(e);
}
try {
streamInfo.setContentAvailability(extractor.getContentAvailability());
} catch (final Exception e) {
streamInfo.addError(e);
}
streamInfo.setRelatedItems(ExtractorHelper.getRelatedItemsOrLogError(streamInfo,
extractor));
@@ -381,6 +386,8 @@ public class StreamInfo extends Info {
private List<StreamSegment> streamSegments = List.of();
private List<MetaInfo> metaInfo = List.of();
private boolean shortFormContent = false;
@Nonnull
private ContentAvailability contentAvailability = ContentAvailability.AVAILABLE;
/**
* Preview frames, e.g. for the storyboard / seekbar thumbnail preview
@@ -727,4 +734,13 @@ public class StreamInfo extends Info {
public void setShortFormContent(final boolean isShortFormContent) {
this.shortFormContent = isShortFormContent;
}
@Nonnull
public ContentAvailability getContentAvailability() {
return contentAvailability;
}
public void setContentAvailability(@Nonnull final ContentAvailability availability) {
this.contentAvailability = availability;
}
}

View File

@@ -47,6 +47,8 @@ public class StreamInfoItem extends InfoItem {
private List<Image> uploaderAvatars = List.of();
private boolean uploaderVerified = false;
private boolean shortFormContent = false;
@Nonnull
private ContentAvailability contentAvailability = ContentAvailability.AVAILABLE;
public StreamInfoItem(final int serviceId,
final String url,
@@ -143,6 +145,23 @@ public class StreamInfoItem extends InfoItem {
this.shortFormContent = shortFormContent;
}
/**
* Gets the availability of the content.
*
* @return The availability of the stream.
*/
@Nonnull
public ContentAvailability getContentAvailability() {
return contentAvailability;
}
/**
* Sets the availability of the Stream.
*/
public void setContentAvailability(@Nonnull final ContentAvailability availability) {
this.contentAvailability = availability;
}
@Override
public String toString() {
return "StreamInfoItem{"

View File

@@ -147,4 +147,19 @@ public interface StreamInfoItemExtractor extends InfoItemExtractor {
default boolean isShortFormContent() throws ParsingException {
return false;
}
/**
* Get the availability of the stream.
*
* <p>
* The availability may not reflect the actual availability when requesting the stream.
* </p>
*
* @return The stream's availability
* @throws ParsingException if there is an error in the extraction
*/
@Nonnull
default ContentAvailability getContentAvailability() throws ParsingException {
return ContentAvailability.UNKNOWN;
}
}

View File

@@ -103,6 +103,11 @@ public class StreamInfoItemsCollector
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setContentAvailability(extractor.getContentAvailability());
} catch (final Exception e) {
addError(e);
}
return resultItem;
}

View File

@@ -9,4 +9,6 @@ public interface BaseSearchExtractorTest extends BaseListExtractorTest {
void testSearchSuggestion() throws Exception;
@Test
void testSearchCorrected() throws Exception;
@Test
void testMetaInfo() throws Exception;
}

View File

@@ -68,4 +68,10 @@ public interface BaseStreamExtractorTest extends BaseExtractorTest {
void testTags() throws Exception;
@Test
void testSupportInfo() throws Exception;
@Test
void testStreamSegmentsCount() throws Exception;
@Test
void testMetaInfo() throws Exception;
@Test
void testContentAvailability() throws Exception;
}

View File

@@ -50,6 +50,7 @@ public abstract class DefaultSearchExtractorTest extends DefaultListExtractorTes
}
@Test
@Override
public void testSearchCorrected() throws Exception {
assertEquals(isCorrectedSearch(), extractor().isCorrectedSearch());
}
@@ -58,6 +59,7 @@ public abstract class DefaultSearchExtractorTest extends DefaultListExtractorTes
* @see DefaultStreamExtractorTest#testMetaInfo()
*/
@Test
@Override
public void testMetaInfo() throws Exception {
final List<MetaInfo> metaInfoList = extractor().getMetaInfo();
final List<MetaInfo> expectedMetaInfoList = expectedMetaInfo();

View File

@@ -7,6 +7,7 @@ import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.ContentAvailability;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.Frameset;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
@@ -77,6 +78,7 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest<St
public String expectedSupportInfo() { return ""; } // default: no support info available
public int expectedStreamSegmentsCount() { return -1; } // return 0 or greater to test (default is -1 to ignore)
public List<MetaInfo> expectedMetaInfo() throws MalformedURLException { return Collections.emptyList(); } // default: no metadata info available
public ContentAvailability expectedContentAvailability() { return ContentAvailability.UNKNOWN; } // default: unknown content availability
@Test
@Override
@@ -429,6 +431,7 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest<St
}
@Test
@Override
public void testStreamSegmentsCount() throws Exception {
if (expectedStreamSegmentsCount() >= 0) {
assertEquals(expectedStreamSegmentsCount(), extractor().getStreamSegments().size());
@@ -439,6 +442,7 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest<St
* @see DefaultSearchExtractorTest#testMetaInfo()
*/
@Test
@Override
public void testMetaInfo() throws Exception {
final List<MetaInfo> metaInfoList = extractor().getMetaInfo();
final List<MetaInfo> expectedMetaInfoList = expectedMetaInfo();
@@ -463,6 +467,11 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest<St
assertTrue(urls.contains(expectedUrl));
}
}
}
@Test
@Override
public void testContentAvailability() throws Exception {
assertEquals(expectedContentAvailability(), extractor().getContentAvailability());
}
}

View File

@@ -25,8 +25,13 @@ import org.schabi.newpipe.extractor.services.DefaultSimpleExtractorTest;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubePlaylistExtractor;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.ContentAvailability;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
/**
* Test for {@link YoutubePlaylistExtractor}
*/
@@ -501,4 +506,28 @@ public class YoutubePlaylistExtractorTest {
assertFalse(page.hasNextPage(), "More items available when it shouldn't");
}
}
public static class MembersOnlyTests implements InitYoutubeTest {
@Test
void testOnlyMembersOnlyVideos() throws Exception {
final YoutubePlaylistExtractor extractor = (YoutubePlaylistExtractor) YouTube
.getPlaylistExtractor(
// auto-generated playlist with only membersOnly videos
"https://www.youtube.com/playlist?list=UUMOQuLXlFNAeDJMSmuzHU5axw");
extractor.fetchPage();
final List<StreamInfoItem> allItems = extractor.getInitialPage().getItems()
.stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toUnmodifiableList());
final List<StreamInfoItem> membershipVideos = allItems.stream()
.filter(item -> item.getContentAvailability() != ContentAvailability.MEMBERSHIP)
.collect(Collectors.toUnmodifiableList());
assertFalse(allItems.isEmpty());
assertTrue(membershipVideos.isEmpty());
}
}
}

View File

@@ -0,0 +1,96 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Origin": [
"https://www.youtube.com"
],
"Referer": [
"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": [
"Sat, 26 Jul 2025 08:56:33 GMT"
],
"document-policy": [
"include-js-call-stacks-in-crash-reports"
],
"expires": [
"Sat, 26 Jul 2025 08:56:33 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\u003deJwNzltIk3EYBnD_PmS6b-77_q9QlEKiQpEpOpuSZUGhmWgaFIlazsPmedrc5jwRlVqCSGczrfQmJVC0C8vMIgiKoALNC09FRYQkWheZtMTei9_d8zzvq3u8YSq4TNjWK0RYsk0MNZ4ROdvtwsttF4nvq0XaJodoS3KIlfsOMTDmEIvBLpEd4hK3plzi3qxLlH9yC89Xt7irZXpbt2Z6u5k5KNN7LEsHq1WH3506_JrToXZZh-xwBWqkgskUBYtHFYz0KRh7qGD-uwJnth4_zXqYnHpUruixkOGP8U5_TEz6Y9dxA_a1GdA_akDGKwOusKJ_BiytGdC4R0VsgorBwypcFhX95SoUl4rbrSqiO1QkMd9eFaF9Kja_UTEepsHXrEHHTDc0THRp6H6rYe6Phr1_NZwKkHDulHi6WyLPJNHOYuMlmg5IdJ-QOJIrMc_SiiSGSyQKSiV6ayUaGjjDos9yr5n7bRLv2MF2ieVrEsbrEvtvSjx_IDEzIBE4KJE-IrH2RGL9mYTfa4mADxLT0xJBCxJxyxJfViWKPXx_XeKyF0ERBIMfoWILYTSQcGsboYt9CyGkhxLGdxBiYggmttFEqI8nrLG-Q4S6JMIjNsPCkwnDbD2FkJVKyGGnmZnls0JmZcWslJUzG6tiduZgLuZmdayB3UkneB0jtLAfJwmebMLnHO7kEoqKeIN5mLua4OckJNYQZt38az2h7Bxh9TwhpIkzTDQTUlsISyzhImGSXbhE8GkloIOQzD72Ekjv-2Lo6ksfrWdiqQchEbWVTocz3xJZY8mPsNorbY4Ii60wosBe4igpyCvPNUYZTVFxxpjI6Kjcqqj_fHfPsg\""
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dz-zDeWiDX34; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dSun, 30-Oct-2022 08:56:33 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"
}
}