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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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{"
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -9,4 +9,6 @@ public interface BaseSearchExtractorTest extends BaseListExtractorTest {
|
||||
void testSearchSuggestion() throws Exception;
|
||||
@Test
|
||||
void testSearchCorrected() throws Exception;
|
||||
@Test
|
||||
void testMetaInfo() throws Exception;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user