Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
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;
Expand Down Expand Up @@ -470,4 +471,33 @@ public boolean isShortFormContent() throws ParsingException {
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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Created by FineFindus on 10.07.25.
*
* Copyright (C) 2025 FineFindus <[email protected]>
* 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,
}
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,17 @@ public boolean isShortFormContent() throws ParsingException {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ private static void extractOptionalData(final StreamInfo streamInfo,
} 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));
Expand Down Expand Up @@ -381,6 +386,8 @@ private static void extractOptionalData(final StreamInfo streamInfo,
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
Expand Down Expand Up @@ -727,4 +734,13 @@ public boolean isShortFormContent() {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -143,6 +145,23 @@ public void setShortFormContent(final boolean shortFormContent) {
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{"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,19 @@ default String getShortDescription() throws ParsingException {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ public StreamInfoItem extract(final StreamInfoItemExtractor extractor) throws Pa
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setContentAvailability(extractor.getContentAvailability());
} catch (final Exception e) {
addError(e);
}

return resultItem;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface BaseSearchExtractorTest extends BaseListExtractorTest {
void testSearchSuggestion() throws Exception;
@Test
void testSearchCorrected() throws Exception;
@Test
void testMetaInfo() throws Exception;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public void testSearchSuggestion() throws Exception {
}

@Test
@Override
public void testSearchCorrected() throws Exception {
assertEquals(isCorrectedSearch(), extractor().isCorrectedSearch());
}
Expand All @@ -58,6 +59,7 @@ public void testSearchCorrected() throws Exception {
* @see DefaultStreamExtractorTest#testMetaInfo()
*/
@Test
@Override
public void testMetaInfo() throws Exception {
final List<MetaInfo> metaInfoList = extractor().getMetaInfo();
final List<MetaInfo> expectedMetaInfoList = expectedMetaInfo();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -429,6 +431,7 @@ public void testSupportInfo() throws Exception {
}

@Test
@Override
public void testStreamSegmentsCount() throws Exception {
if (expectedStreamSegmentsCount() >= 0) {
assertEquals(expectedStreamSegmentsCount(), extractor().getStreamSegments().size());
Expand All @@ -439,6 +442,7 @@ public void testStreamSegmentsCount() throws Exception {
* @see DefaultSearchExtractorTest#testMetaInfo()
*/
@Test
@Override
public void testMetaInfo() throws Exception {
final List<MetaInfo> metaInfoList = extractor().getMetaInfo();
final List<MetaInfo> expectedMetaInfoList = expectedMetaInfo();
Expand All @@ -463,6 +467,11 @@ public void testMetaInfo() throws Exception {
assertTrue(urls.contains(expectedUrl));
}
}
}

@Test
@Override
public void testContentAvailability() throws Exception {
assertEquals(expectedContentAvailability(), extractor().getContentAvailability());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@
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}
*/
Expand Down Expand Up @@ -501,4 +506,28 @@ void testOnlySingleContinuation() throws Exception {
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());
}
}
}
Loading