kopia lustrzana https://github.com/TeamNewPipe/NewPipeExtractor
Merge branch 'dev' into Refactor-date-parsing
# Conflicts: # extractor/src/main/java/org/schabi/newpipe/extractor/localization/DateWrapper.javapull/1372/head
commit
f4084ed483
|
@ -20,7 +20,8 @@ If you're using Gradle, you could add NewPipe Extractor as a dependency with the
|
|||
-dontwarn org.mozilla.javascript.tools.**
|
||||
```
|
||||
|
||||
**Note:** To use NewPipe Extractor in Android projects with a `minSdk` below 33, [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) with the `desugar_jdk_libs_nio` artifact is required.
|
||||
> [!NOTE]
|
||||
> To use NewPipe Extractor in Android projects with a `minSdk` below 33, [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) with the `desugar_jdk_libs_nio` artifact is required.
|
||||
|
||||
### Testing changes
|
||||
|
||||
|
@ -41,6 +42,8 @@ Another approach would be to use the local Maven repository, here's a gist of ho
|
|||
3. Run gradle's `ìnstall` task to deploy this library to your local repository (using the wrapper, present in the root of this project: `./gradlew install`)
|
||||
4. Change the dependency version used in your project to match the one you chose in step 2 (`implementation 'com.github.teamnewpipe:NewPipeExtractor:LOCAL_SNAPSHOT'`)
|
||||
|
||||
|
||||
> [!TIP]
|
||||
> Tip for Android Studio users: After you make changes and run the `install` task, use the menu option `File → "Sync with File System"` to refresh the library in your project.
|
||||
|
||||
## Supported sites
|
||||
|
|
|
@ -29,8 +29,8 @@ allprojects {
|
|||
ext {
|
||||
nanojsonVersion = "e9d656ddb49a412a5a0a5d5ef20ca7ef09549996"
|
||||
jsr305Version = "3.0.2"
|
||||
junitVersion = "5.13.4"
|
||||
checkstyleVersion = "10.4"
|
||||
junitVersion = "5.14.0"
|
||||
checkstyleVersion = "10.26.1"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ checkstyleTest {
|
|||
|
||||
ext {
|
||||
rhinoVersion = '1.8.0'
|
||||
protobufVersion = '4.32.0'
|
||||
protobufVersion = '4.32.1'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -51,7 +51,7 @@ dependencies {
|
|||
testImplementation 'org.junit.jupiter:junit-jupiter-params'
|
||||
|
||||
testImplementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||
testImplementation 'com.google.code.gson:gson:2.13.1'
|
||||
testImplementation 'com.google.code.gson:gson:2.13.2'
|
||||
}
|
||||
|
||||
protobuf {
|
||||
|
|
|
@ -82,6 +82,14 @@ public class DateWrapper implements Serializable {
|
|||
return isApproximation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DateWrapper{"
|
||||
+ "instant=" + instant
|
||||
+ ", isApproximation=" + isApproximation
|
||||
+ '}';
|
||||
}
|
||||
|
||||
public static DateWrapper fromOffsetDateTime(final String date) throws ParsingException {
|
||||
if (date == null) {
|
||||
return null;
|
||||
|
|
|
@ -26,7 +26,6 @@ import org.schabi.newpipe.extractor.stream.Description;
|
|||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
import org.schabi.newpipe.extractor.utils.LocaleCompat;
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
|
|
@ -119,7 +119,8 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
|||
|
||||
@Override
|
||||
public long getTimeStamp() throws ParsingException {
|
||||
return getTimestampSeconds("(#t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)");
|
||||
final var timestamp = getTimestampSeconds("(#t=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)");
|
||||
return timestamp == -2 ? 0 : timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -168,7 +169,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
|||
|
||||
try {
|
||||
final JsonArray transcodings = track.getObject("media")
|
||||
.getArray("transcodings");
|
||||
.getArray("transcodings");
|
||||
if (!isNullOrEmpty(transcodings)) {
|
||||
// Get information about what stream formats are available
|
||||
extractAudioStreams(transcodings, audioStreams);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.schabi.newpipe.extractor.services.soundcloud.linkHandler;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
|
||||
|
@ -9,11 +11,18 @@ import org.schabi.newpipe.extractor.utils.Utils;
|
|||
public final class SoundcloudStreamLinkHandlerFactory extends LinkHandlerFactory {
|
||||
private static final SoundcloudStreamLinkHandlerFactory INSTANCE
|
||||
= new SoundcloudStreamLinkHandlerFactory();
|
||||
private static final String URL_PATTERN = "^https?://(www\\.|m\\.|on\\.)?"
|
||||
+ "soundcloud.com/[0-9a-z_-]+"
|
||||
+ "/(?!(tracks|albums|sets|reposts|followers|following)/?$)[0-9a-z_-]+/?([#?].*)?$";
|
||||
private static final String API_URL_PATTERN = "^https?://api-v2\\.soundcloud.com"
|
||||
+ "/(tracks|albums|sets|reposts|followers|following)/([0-9a-z_-]+)/";
|
||||
|
||||
private static final Pattern URL_PATTERN = Pattern.compile(
|
||||
"^https?://(?:www\\.|m\\.|on\\.)?"
|
||||
+ "soundcloud.com/[0-9a-z_-]+"
|
||||
+ "/(?!(?:tracks|albums|sets|reposts|followers|following)/?$)[0-9a-z_-]+/?(?:[#?].*)?$"
|
||||
);
|
||||
|
||||
private static final Pattern API_URL_PATTERN = Pattern.compile(
|
||||
"^https?://api-v2\\.soundcloud.com"
|
||||
+ "/(tracks|albums|sets|reposts|followers|following)/([0-9a-z_-]+)/"
|
||||
);
|
||||
|
||||
private SoundcloudStreamLinkHandlerFactory() {
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
|
|
@ -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;
|
||||
|
@ -472,4 +473,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,11 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
|||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -30,20 +35,24 @@ import javax.annotation.Nullable;
|
|||
* The following features are currently not implemented because they have never been observed:
|
||||
* <ul>
|
||||
* <li>Shorts</li>
|
||||
* <li>Premieres</li>
|
||||
* <li>Paid content (Premium, members first or only)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtractor {
|
||||
|
||||
private static final String NO_VIEWS_LOWERCASE = "no views";
|
||||
// This approach is language dependant (en-GB)
|
||||
// Leading end space is voluntary included
|
||||
private static final String PREMIERES_TEXT = "Premieres ";
|
||||
private static final DateTimeFormatter PREMIERES_DATE_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm");
|
||||
|
||||
private final JsonObject lockupViewModel;
|
||||
private final TimeAgoParser timeAgoParser;
|
||||
|
||||
private StreamType cachedStreamType;
|
||||
private String cachedName;
|
||||
private Optional<String> cachedTextualUploadDate;
|
||||
private Optional<String> cachedDateText;
|
||||
|
||||
private ChannelImageViewModel cachedChannelImageViewModel;
|
||||
private JsonArray cachedMetadataRows;
|
||||
|
@ -137,7 +146,9 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra
|
|||
@Override
|
||||
public long getDuration() throws ParsingException {
|
||||
// Duration cannot be extracted for live streams, but only for normal videos
|
||||
if (isLive()) {
|
||||
// Exact duration cannot be extracted for premieres, an approximation is only available in
|
||||
// accessibility context label
|
||||
if (isLive() || isPremiere()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -237,20 +248,27 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra
|
|||
@Nullable
|
||||
@Override
|
||||
public String getTextualUploadDate() throws ParsingException {
|
||||
if (cachedTextualUploadDate != null) {
|
||||
return cachedTextualUploadDate.orElse(null);
|
||||
}
|
||||
|
||||
// Live streams have no upload date
|
||||
if (isLive()) {
|
||||
cachedTextualUploadDate = Optional.empty();
|
||||
return null;
|
||||
}
|
||||
|
||||
// This might be null e.g. for live streams
|
||||
this.cachedTextualUploadDate = metadataPart(1, 1)
|
||||
.map(this::getTextContentFromMetadataPart);
|
||||
return cachedTextualUploadDate.orElse(null);
|
||||
// Date string might be null e.g. for live streams
|
||||
final Optional<String> dateText = getDateText();
|
||||
|
||||
if (isPremiere()) {
|
||||
return getDateFromPremiere(dateText);
|
||||
}
|
||||
|
||||
return dateText.orElse(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getDateFromPremiere(final Optional<String> dateText) {
|
||||
// This approach is language dependent
|
||||
// Remove the premieres text from the upload date metadata part
|
||||
return dateText.map(str -> str.replace(PREMIERES_TEXT, ""))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -265,11 +283,32 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra
|
|||
if (textualUploadDate == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isPremiere()) {
|
||||
final String premiereDate = getDateFromPremiere(getDateText());
|
||||
if (premiereDate == null) {
|
||||
throw new ParsingException("Could not get upload date from premiere");
|
||||
}
|
||||
|
||||
try {
|
||||
// As we request a UTC offset of 0 minutes, we get the UTC date
|
||||
return new DateWrapper(OffsetDateTime.of(LocalDateTime.parse(
|
||||
premiereDate, PREMIERES_DATE_FORMATTER), ZoneOffset.UTC));
|
||||
} catch (final DateTimeParseException e) {
|
||||
throw new ParsingException("Could not parse premiere upload date", e);
|
||||
}
|
||||
}
|
||||
|
||||
return timeAgoParser.parse(textualUploadDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewCount() throws ParsingException {
|
||||
if (isPremiere()) {
|
||||
// The number of people returned for premieres is the one currently waiting
|
||||
return -1;
|
||||
}
|
||||
|
||||
final Optional<String> optTextContent = metadataPart(1, 0)
|
||||
.map(this::getTextContentFromMetadataPart);
|
||||
// We could do this inline if the ParsingException would be a RuntimeException -.-
|
||||
|
@ -357,6 +396,20 @@ public class YoutubeStreamInfoItemLockupExtractor implements StreamInfoItemExtra
|
|||
return getStreamType() != StreamType.VIDEO_STREAM;
|
||||
}
|
||||
|
||||
private Optional<String> getDateText() throws ParsingException {
|
||||
if (cachedDateText == null) {
|
||||
cachedDateText = metadataPart(1, 1)
|
||||
.map(this::getTextContentFromMetadataPart);
|
||||
}
|
||||
return cachedDateText;
|
||||
}
|
||||
|
||||
private boolean isPremiere() throws ParsingException {
|
||||
return getDateText().map(dateText -> dateText.contains(PREMIERES_TEXT))
|
||||
// If we can't get date text, assume it is not a premiere, it should be a livestream
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
abstract static class ChannelImageViewModel {
|
||||
protected JsonObject viewModel;
|
||||
|
||||
|
|
|
@ -88,7 +88,8 @@ public final class AudioStream extends Stream {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the identifier of the {@link AudioStream}.
|
||||
* Set the identifier of the {@link AudioStream} which uniquely identifies the stream,
|
||||
* e.g. for YouTube this would be the itag
|
||||
*
|
||||
* <p>
|
||||
* It <b>must not be null</b> and should be non empty.
|
||||
|
@ -108,14 +109,14 @@ public final class AudioStream extends Stream {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the content of the {@link AudioStream}.
|
||||
*
|
||||
* Set the content or the URL of the {@link AudioStream}, depending on whether isUrl is
|
||||
* true
|
||||
* <p>
|
||||
* It must not be null, and should be non empty.
|
||||
* </p>
|
||||
*
|
||||
* @param content the content of the {@link AudioStream}
|
||||
* @param isUrl whether the content is a URL
|
||||
* @param isUrl whether content is the URL or the actual content of e.g. a DASH manifest
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setContent(@Nonnull final String content,
|
||||
|
@ -126,7 +127,7 @@ public final class AudioStream extends Stream {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the {@link MediaFormat} used by the {@link AudioStream}.
|
||||
* Set the {@link MediaFormat} used by the {@link AudioStream}, which can be null
|
||||
*
|
||||
* <p>
|
||||
* It should be one of the audio {@link MediaFormat}s ({@link MediaFormat#M4A M4A},
|
||||
|
@ -278,16 +279,22 @@ public final class AudioStream extends Stream {
|
|||
* Build an {@link AudioStream} using the builder's current values.
|
||||
*
|
||||
* <p>
|
||||
* The identifier and the content (and so the {@code isUrl} boolean) properties must have
|
||||
* The identifier and the content (and thus {@code isUrl}) properties must have
|
||||
* been set.
|
||||
* </p>
|
||||
*
|
||||
* @return a new {@link AudioStream} using the builder's current values
|
||||
* @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}) or
|
||||
* @throws IllegalStateException if {@code id}, {@code content} (and thus {@code isUrl}) or
|
||||
* {@code deliveryMethod} have been not set, or have been set as {@code null}
|
||||
*/
|
||||
@Nonnull
|
||||
public AudioStream build() {
|
||||
validateBuild();
|
||||
|
||||
return new AudioStream(this);
|
||||
}
|
||||
|
||||
void validateBuild() {
|
||||
if (id == null) {
|
||||
throw new IllegalStateException(
|
||||
"The identifier of the audio stream has been not set or is null. If you "
|
||||
|
@ -305,64 +312,39 @@ public final class AudioStream extends Stream {
|
|||
"The delivery method of the audio stream has been set as null, which is "
|
||||
+ "not allowed. Pass a valid one instead with setDeliveryMethod.");
|
||||
}
|
||||
|
||||
return new AudioStream(id, content, isUrl, mediaFormat, deliveryMethod, averageBitrate,
|
||||
manifestUrl, audioTrackId, audioTrackName, audioLocale, audioTrackType,
|
||||
itagItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new audio stream.
|
||||
* Create a new audio stream using the given {@link Builder}.
|
||||
*
|
||||
* @param id the identifier which uniquely identifies the stream, e.g. for YouTube
|
||||
* this would be the itag
|
||||
* @param content the content or the URL of the stream, depending on whether isUrl is
|
||||
* true
|
||||
* @param isUrl whether content is the URL or the actual content of e.g. a DASH
|
||||
* manifest
|
||||
* @param format the {@link MediaFormat} used by the stream, which can be null
|
||||
* @param deliveryMethod the {@link DeliveryMethod} of the stream
|
||||
* @param averageBitrate the average bitrate of the stream (which can be unknown, see
|
||||
* {@link #UNKNOWN_BITRATE})
|
||||
* @param audioTrackId the id of the audio track
|
||||
* @param audioTrackName the name of the audio track
|
||||
* @param audioLocale the {@link Locale} of the audio stream, representing its language
|
||||
* @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null
|
||||
* @param manifestUrl the URL of the manifest this stream comes from (if applicable,
|
||||
* otherwise null)
|
||||
* @param builder The {@link Builder} to use to create the audio stream
|
||||
*/
|
||||
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||
private AudioStream(@Nonnull final String id,
|
||||
@Nonnull final String content,
|
||||
final boolean isUrl,
|
||||
@Nullable final MediaFormat format,
|
||||
@Nonnull final DeliveryMethod deliveryMethod,
|
||||
final int averageBitrate,
|
||||
@Nullable final String manifestUrl,
|
||||
@Nullable final String audioTrackId,
|
||||
@Nullable final String audioTrackName,
|
||||
@Nullable final Locale audioLocale,
|
||||
@Nullable final AudioTrackType audioTrackType,
|
||||
@Nullable final ItagItem itagItem) {
|
||||
super(id, content, isUrl, format, deliveryMethod, manifestUrl);
|
||||
if (itagItem != null) {
|
||||
this.itagItem = itagItem;
|
||||
this.itag = itagItem.id;
|
||||
this.quality = itagItem.getQuality();
|
||||
this.bitrate = itagItem.getBitrate();
|
||||
this.initStart = itagItem.getInitStart();
|
||||
this.initEnd = itagItem.getInitEnd();
|
||||
this.indexStart = itagItem.getIndexStart();
|
||||
this.indexEnd = itagItem.getIndexEnd();
|
||||
this.codec = itagItem.getCodec();
|
||||
AudioStream(final Builder builder) {
|
||||
super(builder.id,
|
||||
builder.content,
|
||||
builder.isUrl,
|
||||
builder.mediaFormat,
|
||||
builder.deliveryMethod,
|
||||
builder.manifestUrl);
|
||||
if (builder.itagItem != null) {
|
||||
this.itagItem = builder.itagItem;
|
||||
this.itag = builder.itagItem.id;
|
||||
this.quality = builder.itagItem.getQuality();
|
||||
this.bitrate = builder.itagItem.getBitrate();
|
||||
this.initStart = builder.itagItem.getInitStart();
|
||||
this.initEnd = builder.itagItem.getInitEnd();
|
||||
this.indexStart = builder.itagItem.getIndexStart();
|
||||
this.indexEnd = builder.itagItem.getIndexEnd();
|
||||
this.codec = builder.itagItem.getCodec();
|
||||
}
|
||||
this.averageBitrate = averageBitrate;
|
||||
this.audioTrackId = audioTrackId;
|
||||
this.audioTrackName = audioTrackName;
|
||||
this.audioLocale = audioLocale;
|
||||
this.audioTrackType = audioTrackType;
|
||||
this.averageBitrate = builder.averageBitrate;
|
||||
this.audioTrackId = builder.audioTrackId;
|
||||
this.audioTrackName = builder.audioTrackName;
|
||||
this.audioLocale = builder.audioLocale;
|
||||
this.audioTrackType = builder.audioTrackType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -17,11 +17,7 @@ public class Description implements Serializable {
|
|||
|
||||
public Description(@Nullable final String content, final int type) {
|
||||
this.type = type;
|
||||
if (content == null) {
|
||||
this.content = "";
|
||||
} else {
|
||||
this.content = content;
|
||||
}
|
||||
this.content = Objects.requireNonNullElse(content, "");
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
|
|
|
@ -68,7 +68,7 @@ public abstract class Stream implements Serializable {
|
|||
* @param streamList the list of {@link Stream}s which will be compared
|
||||
* @return whether the list already contains one stream with equals stats
|
||||
*/
|
||||
public static boolean containSimilarStream(final Stream stream,
|
||||
public static boolean containSimilarStream(@Nonnull final Stream stream,
|
||||
final List<? extends Stream> streamList) {
|
||||
if (isNullOrEmpty(streamList)) {
|
||||
return false;
|
||||
|
@ -98,11 +98,9 @@ public abstract class Stream implements Serializable {
|
|||
* @return whether the stream have the same stats or not, based on the criteria above
|
||||
*/
|
||||
public boolean equalStats(@Nullable final Stream other) {
|
||||
if (other == null || mediaFormat == null || other.mediaFormat == null) {
|
||||
return false;
|
||||
}
|
||||
return mediaFormat.id == other.mediaFormat.id && deliveryMethod == other.deliveryMethod
|
||||
&& isUrl == other.isUrl;
|
||||
return other != null && mediaFormat != null && other.mediaFormat != null
|
||||
&& mediaFormat.id == other.mediaFormat.id && deliveryMethod == other.deliveryMethod
|
||||
&& isUrl == other.isUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,6 +135,7 @@ public abstract class Stream implements Serializable {
|
|||
*
|
||||
* @return the content or URL
|
||||
*/
|
||||
@Nonnull
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
|
|
@ -582,6 +582,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;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import java.util.Objects;
|
|||
* <p>
|
||||
* This class is used to construct {@link org.schabi.newpipe.extractor.Image Image}
|
||||
* instances from a single base URL/path, in order to get all or most image resolutions provided,
|
||||
* depending of the service and the resolutions provided.
|
||||
* depending on the service and the resolutions provided.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
|
|
@ -44,50 +44,113 @@ public final class Parser {
|
|||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Matcher matchOrThrow(@Nonnull final Pattern pattern,
|
||||
final String input) throws RegexException {
|
||||
final Matcher matcher = pattern.matcher(input);
|
||||
if (matcher.find()) {
|
||||
return matcher;
|
||||
} else {
|
||||
String errorMessage = "Failed to find pattern \"" + pattern.pattern() + "\"";
|
||||
if (input.length() <= 1024) {
|
||||
errorMessage += " inside of \"" + input + "\"";
|
||||
}
|
||||
throw new RegexException(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches group 1 of the given pattern against the input
|
||||
* and returns the matched group
|
||||
*
|
||||
* @param pattern The regex pattern to match.
|
||||
* @param input The input string to match against.
|
||||
* @return The matching group as a string.
|
||||
* @throws RegexException If the pattern does not match the input or if the group is not found.
|
||||
*/
|
||||
@Nonnull
|
||||
public static String matchGroup1(final String pattern, final String input)
|
||||
throws RegexException {
|
||||
return matchGroup(pattern, input, 1);
|
||||
}
|
||||
|
||||
public static String matchGroup1(final Pattern pattern,
|
||||
final String input) throws RegexException {
|
||||
/**
|
||||
* Matches group 1 of the given pattern against the input
|
||||
* and returns the matched group
|
||||
*
|
||||
* @param pattern The regex pattern to match.
|
||||
* @param input The input string to match against.
|
||||
* @return The matching group as a string.
|
||||
* @throws RegexException If the pattern does not match the input or if the group is not found.
|
||||
*/
|
||||
@Nonnull
|
||||
public static String matchGroup1(final Pattern pattern, final String input)
|
||||
throws RegexException {
|
||||
return matchGroup(pattern, input, 1);
|
||||
}
|
||||
|
||||
public static String matchGroup(final String pattern,
|
||||
final String input,
|
||||
final int group) throws RegexException {
|
||||
/**
|
||||
* Matches the specified group of the given pattern against the input,
|
||||
* and returns the matched group
|
||||
*
|
||||
* @param pattern The regex pattern to match.
|
||||
* @param input The input string to match against.
|
||||
* @param group The group number to retrieve (1-based index).
|
||||
* @return The matching group as a string.
|
||||
* @throws RegexException If the pattern does not match the input or if the group is not found.
|
||||
*/
|
||||
@Nonnull
|
||||
public static String matchGroup(final String pattern, final String input, final int group)
|
||||
throws RegexException {
|
||||
return matchGroup(Pattern.compile(pattern), input, group);
|
||||
}
|
||||
|
||||
public static String matchGroup(@Nonnull final Pattern pat,
|
||||
/**
|
||||
* Matches the specified group of the given pattern against the input,
|
||||
* and returns the matched group
|
||||
*
|
||||
* @param pattern The regex pattern to match.
|
||||
* @param input The input string to match against.
|
||||
* @param group The group number to retrieve (1-based index).
|
||||
* @return The matching group as a string.
|
||||
* @throws RegexException If the pattern does not match the input or if the group is not found.
|
||||
*/
|
||||
@Nonnull
|
||||
public static String matchGroup(@Nonnull final Pattern pattern,
|
||||
final String input,
|
||||
final int group) throws RegexException {
|
||||
final Matcher matcher = pat.matcher(input);
|
||||
final boolean foundMatch = matcher.find();
|
||||
if (foundMatch) {
|
||||
return matcher.group(group);
|
||||
} else {
|
||||
// only pass input to exception message when it is not too long
|
||||
if (input.length() > 1024) {
|
||||
throw new RegexException("Failed to find pattern \"" + pat.pattern() + "\"");
|
||||
} else {
|
||||
throw new RegexException("Failed to find pattern \"" + pat.pattern()
|
||||
+ "\" inside of \"" + input + "\"");
|
||||
}
|
||||
}
|
||||
final int group)
|
||||
throws RegexException {
|
||||
return matchOrThrow(pattern, input).group(group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches multiple patterns against the input string and
|
||||
* returns the first successful matcher
|
||||
*
|
||||
* @param patterns The array of regex patterns to match.
|
||||
* @param input The input string to match against.
|
||||
* @return A {@code Matcher} for the first successful match.
|
||||
* @throws RegexException If no patterns match the input or if {@code patterns} is empty.
|
||||
*/
|
||||
public static String matchGroup1MultiplePatterns(final Pattern[] patterns, final String input)
|
||||
throws RegexException {
|
||||
return matchMultiplePatterns(patterns, input).group(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches multiple patterns against the input string and
|
||||
* returns the first successful matcher
|
||||
*
|
||||
* @param patterns The array of regex patterns to match.
|
||||
* @param input The input string to match against.
|
||||
* @return A {@code Matcher} for the first successful match.
|
||||
* @throws RegexException If no patterns match the input or if {@code patterns} is empty.
|
||||
*/
|
||||
public static Matcher matchMultiplePatterns(final Pattern[] patterns, final String input)
|
||||
throws RegexException {
|
||||
Parser.RegexException exception = null;
|
||||
for (final Pattern pattern : patterns) {
|
||||
final Matcher matcher = pattern.matcher(input);
|
||||
RegexException exception = null;
|
||||
for (final var pattern : patterns) {
|
||||
final var matcher = pattern.matcher(input);
|
||||
if (matcher.find()) {
|
||||
return matcher;
|
||||
} else if (exception == null) {
|
||||
|
@ -107,14 +170,11 @@ public final class Parser {
|
|||
}
|
||||
|
||||
public static boolean isMatch(final String pattern, final String input) {
|
||||
final Pattern pat = Pattern.compile(pattern);
|
||||
final Matcher mat = pat.matcher(input);
|
||||
return mat.find();
|
||||
return isMatch(Pattern.compile(pattern), input);
|
||||
}
|
||||
|
||||
public static boolean isMatch(@Nonnull final Pattern pattern, final String input) {
|
||||
final Matcher mat = pattern.matcher(input);
|
||||
return mat.find();
|
||||
return pattern.matcher(input).find();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
|
|
@ -110,12 +110,22 @@ public final class Utils {
|
|||
* @param url the url to be tested
|
||||
*/
|
||||
public static void checkUrl(final String pattern, final String url) throws ParsingException {
|
||||
checkUrl(Pattern.compile(pattern), url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the url matches the pattern.
|
||||
*
|
||||
* @param pattern the pattern that will be used to check the url
|
||||
* @param url the url to be tested
|
||||
*/
|
||||
public static void checkUrl(final Pattern pattern, final String url) throws ParsingException {
|
||||
if (isNullOrEmpty(url)) {
|
||||
throw new IllegalArgumentException("Url can't be null or empty");
|
||||
}
|
||||
|
||||
if (!Parser.isMatch(pattern, url.toLowerCase())) {
|
||||
throw new ParsingException("Url don't match the pattern");
|
||||
throw new ParsingException("Url doesn't match the pattern");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,9 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
|||
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DownloaderFactory {
|
||||
|
||||
private static final DownloaderType DEFAULT_DOWNLOADER = DownloaderType.REAL;
|
||||
|
@ -36,20 +39,31 @@ public class DownloaderFactory {
|
|||
}
|
||||
|
||||
public static Downloader getDownloader(final Class<?> clazz) {
|
||||
return getDownloader(clazz, null);
|
||||
return getDownloader(getMockPath(clazz, null));
|
||||
}
|
||||
|
||||
public static Downloader getDownloader(final Class<?> clazz, final String specificUseCase) {
|
||||
public static Downloader getDownloader(final Class<?> clazz,
|
||||
@Nullable final String specificUseCase) {
|
||||
return getDownloader(getMockPath(clazz, specificUseCase));
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns a path without a trailing '/', so that it can be used both as a folder name
|
||||
* and as a filename. The {@link MockDownloader} will use it as a folder name, but other tests
|
||||
* can use it as a filename, if only one custom mock file is needed for that test.
|
||||
*/
|
||||
public static String getMockPath(final Class<?> clazz,
|
||||
@Nullable final String specificUseCase) {
|
||||
String baseName = clazz.getName();
|
||||
if (specificUseCase != null) {
|
||||
baseName += "." + specificUseCase;
|
||||
}
|
||||
return getDownloader("src/test/resources/mocks/v1/"
|
||||
+ baseName
|
||||
.toLowerCase(Locale.ENGLISH)
|
||||
.replace('$', '.')
|
||||
.replace("test", "")
|
||||
.replace('.', '/'));
|
||||
return "src/test/resources/mocks/v1/"
|
||||
+ baseName
|
||||
.toLowerCase(Locale.ENGLISH)
|
||||
.replace('$', '.')
|
||||
.replace("test", "")
|
||||
.replace('.', '/');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,6 @@ import org.schabi.newpipe.extractor.downloader.Response;
|
|||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -35,7 +34,7 @@ public final class DownloaderTestImpl extends Downloader {
|
|||
// Required for certain services
|
||||
// For example Bandcamp otherwise fails on Windows with Java 17+
|
||||
// as their Fastly-CDN returns 403
|
||||
.connectionSpecs(Arrays.asList(ConnectionSpec.RESTRICTED_TLS))
|
||||
.connectionSpecs(List.of(ConnectionSpec.RESTRICTED_TLS))
|
||||
.build());
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.InfoItemsCollector;
|
|||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.MetaInfo;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,7 +135,6 @@ public class SoundcloudStreamExtractorTest {
|
|||
@Override public long expectedDislikeCountAtLeast() { return -1; }
|
||||
@Override public boolean expectedHasAudioStreams() { return false; }
|
||||
@Override public boolean expectedHasVideoStreams() { return false; }
|
||||
@Override public boolean expectedHasRelatedItems() { return true; }
|
||||
@Override public boolean expectedHasSubtitles() { return false; }
|
||||
@Override public boolean expectedHasFrames() { return false; }
|
||||
@Override public int expectedStreamSegmentsCount() { return 0; }
|
||||
|
|
|
@ -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,83 @@
|
|||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.schabi.newpipe.downloader.DownloaderFactory.getMockPath;
|
||||
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager;
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamInfoItemLockupExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
public class YoutubeStreamInfoItemTest {
|
||||
@Test
|
||||
void videoRendererPremiere() throws FileNotFoundException, JsonParserException {
|
||||
final var json = JsonParser.object().from(new FileInputStream(getMockPath(
|
||||
YoutubeStreamInfoItemTest.class, "videoRendererPremiere") + ".json"));
|
||||
final var timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(Localization.DEFAULT);
|
||||
final var extractor = new YoutubeStreamInfoItemExtractor(json, timeAgoParser);
|
||||
assertAll(
|
||||
() -> assertEquals(StreamType.VIDEO_STREAM, extractor.getStreamType()),
|
||||
() -> assertFalse(extractor.isAd()),
|
||||
() -> assertEquals("https://www.youtube.com/watch?v=M_8QNw_JM4I", extractor.getUrl()),
|
||||
() -> assertEquals("This video will premiere in 6 months.", extractor.getName()),
|
||||
() -> assertEquals(33, extractor.getDuration()),
|
||||
() -> assertEquals("Blunt Brothers Productions", extractor.getUploaderName()),
|
||||
() -> assertEquals("https://www.youtube.com/channel/UCUPrbbdnot-aPgNM65svgOg", extractor.getUploaderUrl()),
|
||||
() -> assertFalse(extractor.getUploaderAvatars().isEmpty()),
|
||||
() -> assertTrue(extractor.isUploaderVerified()),
|
||||
() -> assertEquals("2026-03-15 13:12", extractor.getTextualUploadDate()),
|
||||
() -> {
|
||||
assertNotNull(extractor.getUploadDate());
|
||||
assertEquals(OffsetDateTime.of(2026, 3, 15, 13, 12, 0, 0, ZoneOffset.UTC), extractor.getUploadDate().offsetDateTime());
|
||||
},
|
||||
() -> assertEquals(-1, extractor.getViewCount()),
|
||||
() -> assertFalse(extractor.getThumbnails().isEmpty()),
|
||||
() -> assertEquals("Patience is key… MERCH SHOP : https://www.bluntbrosproductions.com Follow us on Instagram for early updates: ...", extractor.getShortDescription()),
|
||||
() -> assertFalse(extractor.isShortFormContent())
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockupViewModelPremiere()
|
||||
throws FileNotFoundException, JsonParserException {
|
||||
final var json = JsonParser.object().from(new FileInputStream(getMockPath(
|
||||
YoutubeStreamInfoItemTest.class, "lockupViewModelPremiere") + ".json"));
|
||||
final var timeAgoParser = TimeAgoPatternsManager.getTimeAgoParserFor(Localization.DEFAULT);
|
||||
final var extractor = new YoutubeStreamInfoItemLockupExtractor(json, timeAgoParser);
|
||||
assertAll(
|
||||
() -> assertEquals(StreamType.VIDEO_STREAM, extractor.getStreamType()),
|
||||
() -> assertFalse(extractor.isAd()),
|
||||
() -> assertEquals("https://www.youtube.com/watch?v=VIDEO_ID", extractor.getUrl()),
|
||||
() -> assertEquals("VIDEO_TITLE", extractor.getName()),
|
||||
() -> assertEquals(-1, extractor.getDuration()),
|
||||
() -> assertEquals("VIDEO_CHANNEL_NAME", extractor.getUploaderName()),
|
||||
() -> assertEquals("https://www.youtube.com/channel/UCD_on7-zu7Zuc3zissQvrgw", extractor.getUploaderUrl()),
|
||||
() -> assertFalse(extractor.getUploaderAvatars().isEmpty()),
|
||||
() -> assertFalse(extractor.isUploaderVerified()),
|
||||
() -> assertEquals("14/08/2025, 13:00", extractor.getTextualUploadDate()),
|
||||
() -> {
|
||||
assertNotNull(extractor.getUploadDate());
|
||||
assertEquals(OffsetDateTime.of(2025, 8, 14, 13, 0, 0, 0, ZoneOffset.UTC), extractor.getUploadDate().offsetDateTime());
|
||||
},
|
||||
() -> assertEquals(-1, extractor.getViewCount()),
|
||||
() -> assertFalse(extractor.getThumbnails().isEmpty()),
|
||||
() -> assertNull(extractor.getShortDescription()),
|
||||
() -> assertFalse(extractor.isShortFormContent())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ public class YoutubeSearchQHTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testWithContentfilter() throws Exception {
|
||||
public void testWithContentFilter() throws Exception {
|
||||
assertEquals("https://www.youtube.com/results?search_query=asdf&sp=EgIQAfABAQ%253D%253D", YouTube.getSearchQHFactory()
|
||||
.fromQuery("asdf", List.of(VIDEOS), "").getUrl());
|
||||
assertEquals("https://www.youtube.com/results?search_query=asdf&sp=EgIQAvABAQ%253D%253D", YouTube.getSearchQHFactory()
|
||||
|
|
|
@ -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
|
@ -0,0 +1,133 @@
|
|||
{
|
||||
"contentImage": {
|
||||
"thumbnailViewModel": {
|
||||
"image": {
|
||||
"sources": [
|
||||
{
|
||||
"url": "https://i.ytimg.com/vi/0_-Nh-nOhLQ/hqdefault.jpg?sqp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&rs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"width": 168,
|
||||
"height": 94
|
||||
},
|
||||
{
|
||||
"url": "https://i.ytimg.com/vi/0_-Nh-nOhLQ/hqdefault.jpg?sqp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&rs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"width": 336,
|
||||
"height": 188
|
||||
}
|
||||
]
|
||||
},
|
||||
"overlays": [
|
||||
{
|
||||
"thumbnailOverlayBadgeViewModel": {
|
||||
"thumbnailBadges": [
|
||||
{
|
||||
"thumbnailBadgeViewModel": {
|
||||
"text": "Upcoming",
|
||||
"badgeStyle": "THUMBNAIL_OVERLAY_BADGE_STYLE_DEFAULT",
|
||||
"animationActivationTargetId": "VIDEO_ID",
|
||||
"animationActivationEntityKey": "REDACTED",
|
||||
"lottieData": {
|
||||
"url": "https://www.gstatic.com/youtube/img/lottie/audio_indicator/audio_indicator_v2.json",
|
||||
"settings": {
|
||||
"loop": true,
|
||||
"autoplay": true
|
||||
}
|
||||
},
|
||||
"animatedText": "Now playing",
|
||||
"animationActivationEntitySelectorType": "THUMBNAIL_BADGE_ANIMATION_ENTITY_SELECTOR_TYPE_PLAYER_STATE"
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": "THUMBNAIL_OVERLAY_BADGE_POSITION_BOTTOM_END"
|
||||
}
|
||||
},
|
||||
{
|
||||
"thumbnailHoverOverlayToggleActionsViewModel": {
|
||||
"buttons": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"lockupMetadataViewModel": {
|
||||
"title": {
|
||||
"content": "VIDEO_TITLE"
|
||||
},
|
||||
"image": {
|
||||
"decoratedAvatarViewModel": {
|
||||
"avatar": {
|
||||
"avatarViewModel": {
|
||||
"image": {
|
||||
"sources": [
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"width": 68,
|
||||
"height": 68
|
||||
}
|
||||
]
|
||||
},
|
||||
"avatarImageSize": "AVATAR_SIZE_M"
|
||||
}
|
||||
},
|
||||
"a11yLabel": "Go to channel",
|
||||
"rendererContext": {
|
||||
"commandContext": {
|
||||
"onTap": {
|
||||
"innertubeCommand": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/@deep_flow_music_ua",
|
||||
"webPageType": "WEB_PAGE_TYPE_CHANNEL",
|
||||
"rootVe": 3611,
|
||||
"apiUrl": "/youtubei/v1/browse"
|
||||
}
|
||||
},
|
||||
"browseEndpoint": {
|
||||
"browseId": "UCD_on7-zu7Zuc3zissQvrgw",
|
||||
"canonicalBaseUrl": "/@deep_flow_music_ua"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"contentMetadataViewModel": {
|
||||
"metadataRows": [
|
||||
{
|
||||
"metadataParts": [
|
||||
{
|
||||
"text": {
|
||||
"content": "VIDEO_CHANNEL_NAME",
|
||||
"styleRuns": [],
|
||||
"attachmentRuns": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"metadataParts": [
|
||||
{
|
||||
"text": {
|
||||
"content": "56 waiting"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"content": "Premieres 14/08/2025, 13:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"delimiter": " • "
|
||||
}
|
||||
},
|
||||
"menuButton": {}
|
||||
}
|
||||
},
|
||||
"contentId": "VIDEO_ID",
|
||||
"contentType": "LOCKUP_CONTENT_TYPE_VIDEO"
|
||||
}
|
|
@ -0,0 +1,586 @@
|
|||
{
|
||||
"videoId": "M_8QNw_JM4I",
|
||||
"thumbnail": {
|
||||
"thumbnails": [
|
||||
{
|
||||
"url": "https://i.ytimg.com/vi/M_8QNw_JM4I/hq720.jpg?sqp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&rs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"width": 360,
|
||||
"height": 202
|
||||
},
|
||||
{
|
||||
"url": "https://i.ytimg.com/vi/M_8QNw_JM4I/hq720.jpg?sqp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&rs=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"width": 720,
|
||||
"height": 404
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "This video will premiere in 6 months."
|
||||
}
|
||||
],
|
||||
"accessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "This video will premiere in 6 months. 33 seconds"
|
||||
}
|
||||
}
|
||||
},
|
||||
"descriptionSnippet": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Patience is key… MERCH SHOP : https://www.bluntbrosproductions.com Follow us on Instagram for early updates: ..."
|
||||
}
|
||||
]
|
||||
},
|
||||
"longBylineText": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Blunt Brothers Productions",
|
||||
"navigationEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/@BluntBrothersProductions",
|
||||
"webPageType": "WEB_PAGE_TYPE_CHANNEL",
|
||||
"rootVe": 3611,
|
||||
"apiUrl": "/youtubei/v1/browse"
|
||||
}
|
||||
},
|
||||
"browseEndpoint": {
|
||||
"browseId": "UCUPrbbdnot-aPgNM65svgOg",
|
||||
"canonicalBaseUrl": "/@BluntBrothersProductions"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"lengthText": {
|
||||
"accessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "33 seconds"
|
||||
}
|
||||
},
|
||||
"simpleText": "0:33"
|
||||
},
|
||||
"navigationEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/watch?v=M_8QNw_JM4I&pp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"webPageType": "WEB_PAGE_TYPE_WATCH",
|
||||
"rootVe": 3832
|
||||
}
|
||||
},
|
||||
"watchEndpoint": {
|
||||
"videoId": "M_8QNw_JM4I",
|
||||
"params": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"playerParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"watchEndpointSupportedOnesieConfig": {
|
||||
"html5PlaybackOnesieConfig": {
|
||||
"commonConfig": {
|
||||
"url": "https://rr4---sn-n0ogpnx-1gie.googlevideo.com/initplayback?source=youtube&oeis=1&c=WEB&oad=3200&ovd=3200&oaad=11000&oavd=11000&ocs=700&oewis=1&oputc=1&ofpcc=1&msp=1&odepv=1&oreouc=1&id=33ff10370fc93382&ip=1.1.1.1&initcwndbps=3320000&mt=1759654189&oweuc="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ownerBadges": [
|
||||
{
|
||||
"metadataBadgeRenderer": {
|
||||
"icon": {
|
||||
"iconType": "CHECK_CIRCLE_THICK"
|
||||
},
|
||||
"style": "BADGE_STYLE_TYPE_VERIFIED",
|
||||
"tooltip": "Verified",
|
||||
"trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"accessibilityData": {
|
||||
"label": "Verified"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"ownerText": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Blunt Brothers Productions",
|
||||
"navigationEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/@BluntBrothersProductions",
|
||||
"webPageType": "WEB_PAGE_TYPE_CHANNEL",
|
||||
"rootVe": 3611,
|
||||
"apiUrl": "/youtubei/v1/browse"
|
||||
}
|
||||
},
|
||||
"browseEndpoint": {
|
||||
"browseId": "UCUPrbbdnot-aPgNM65svgOg",
|
||||
"canonicalBaseUrl": "/@BluntBrothersProductions"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"upcomingEventData": {
|
||||
"startTime": "1773580320",
|
||||
"isReminderSet": false,
|
||||
"upcomingEventText": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Premieres "
|
||||
},
|
||||
{
|
||||
"text": "DATE_PLACEHOLDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"shortBylineText": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Blunt Brothers Productions",
|
||||
"navigationEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/@BluntBrothersProductions",
|
||||
"webPageType": "WEB_PAGE_TYPE_CHANNEL",
|
||||
"rootVe": 3611,
|
||||
"apiUrl": "/youtubei/v1/browse"
|
||||
}
|
||||
},
|
||||
"browseEndpoint": {
|
||||
"browseId": "UCUPrbbdnot-aPgNM65svgOg",
|
||||
"canonicalBaseUrl": "/@BluntBrothersProductions"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"showActionMenu": false,
|
||||
"shortViewCountText": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "1"
|
||||
},
|
||||
{
|
||||
"text": " waiting"
|
||||
}
|
||||
],
|
||||
"accessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "1 waiting"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"menuRenderer": {
|
||||
"items": [
|
||||
{
|
||||
"menuServiceItemRenderer": {
|
||||
"text": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Add to queue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"iconType": "ADD_TO_QUEUE_TAIL"
|
||||
},
|
||||
"serviceEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"sendPost": true
|
||||
}
|
||||
},
|
||||
"signalServiceEndpoint": {
|
||||
"signal": "CLIENT_SIGNAL",
|
||||
"actions": [
|
||||
{
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"addToPlaylistCommand": {
|
||||
"openMiniplayer": true,
|
||||
"videoId": "M_8QNw_JM4I",
|
||||
"listType": "PLAYLIST_EDIT_LIST_TYPE_QUEUE",
|
||||
"onCreateListCommand": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"sendPost": true,
|
||||
"apiUrl": "/youtubei/v1/playlist/create"
|
||||
}
|
||||
},
|
||||
"createPlaylistServiceEndpoint": {
|
||||
"videoIds": [
|
||||
"M_8QNw_JM4I"
|
||||
],
|
||||
"params": "CAQ%3D"
|
||||
}
|
||||
},
|
||||
"videoIds": [
|
||||
"M_8QNw_JM4I"
|
||||
],
|
||||
"videoCommand": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/watch?v=M_8QNw_JM4I",
|
||||
"webPageType": "WEB_PAGE_TYPE_WATCH",
|
||||
"rootVe": 3832
|
||||
}
|
||||
},
|
||||
"watchEndpoint": {
|
||||
"videoId": "M_8QNw_JM4I",
|
||||
"watchEndpointSupportedOnesieConfig": {
|
||||
"html5PlaybackOnesieConfig": {
|
||||
"commonConfig": {
|
||||
"url": "https://rr4---sn-n0ogpnx-1gie.googlevideo.com/initplayback?source=youtube&oeis=1&c=WEB&oad=3200&ovd=3200&oaad=11000&oavd=11000&ocs=700&oewis=1&oputc=1&ofpcc=1&msp=1&odepv=1&oreouc=1&id=33ff10370fc93382&ip=1.1.1.1&initcwndbps=3320000&mt=1759654189&oweuc="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"menuServiceItemDownloadRenderer": {
|
||||
"serviceEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"offlineVideoEndpoint": {
|
||||
"videoId": "M_8QNw_JM4I",
|
||||
"onAddCommand": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"getDownloadActionCommand": {
|
||||
"videoId": "M_8QNw_JM4I",
|
||||
"params": "CAIQAA%3D%3D"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"menuServiceItemRenderer": {
|
||||
"text": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Share"
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"iconType": "SHARE"
|
||||
},
|
||||
"serviceEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"sendPost": true,
|
||||
"apiUrl": "/youtubei/v1/share/get_share_panel"
|
||||
}
|
||||
},
|
||||
"shareEntityServiceEndpoint": {
|
||||
"serializedShareEntity": "CgtNXzhRTndfSk00SQ%3D%3D",
|
||||
"commands": [
|
||||
{
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"openPopupAction": {
|
||||
"popup": {
|
||||
"unifiedSharePanelRenderer": {
|
||||
"trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"showLoadingSpinner": true
|
||||
}
|
||||
},
|
||||
"popupType": "DIALOG",
|
||||
"beReused": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"hasSeparator": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"accessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "Action menu"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"channelThumbnailSupportedRenderers": {
|
||||
"channelThumbnailWithLinkRenderer": {
|
||||
"thumbnail": {
|
||||
"thumbnails": [
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"width": 68,
|
||||
"height": 68
|
||||
}
|
||||
]
|
||||
},
|
||||
"navigationEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/@BluntBrothersProductions",
|
||||
"webPageType": "WEB_PAGE_TYPE_CHANNEL",
|
||||
"rootVe": 3611,
|
||||
"apiUrl": "/youtubei/v1/browse"
|
||||
}
|
||||
},
|
||||
"browseEndpoint": {
|
||||
"browseId": "UCUPrbbdnot-aPgNM65svgOg",
|
||||
"canonicalBaseUrl": "/@BluntBrothersProductions"
|
||||
}
|
||||
},
|
||||
"accessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "Go to channel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"thumbnailOverlays": [
|
||||
{
|
||||
"thumbnailOverlayTimeStatusRenderer": {
|
||||
"text": {
|
||||
"accessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "Upcoming"
|
||||
}
|
||||
},
|
||||
"simpleText": "UPCOMING"
|
||||
},
|
||||
"style": "UPCOMING"
|
||||
}
|
||||
},
|
||||
{
|
||||
"thumbnailOverlayToggleButtonRenderer": {
|
||||
"isToggled": false,
|
||||
"untoggledIcon": {
|
||||
"iconType": "WATCH_LATER"
|
||||
},
|
||||
"toggledIcon": {
|
||||
"iconType": "CHECK"
|
||||
},
|
||||
"untoggledTooltip": "Watch later",
|
||||
"toggledTooltip": "Added",
|
||||
"untoggledServiceEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"sendPost": true,
|
||||
"apiUrl": "/youtubei/v1/browse/edit_playlist"
|
||||
}
|
||||
},
|
||||
"playlistEditEndpoint": {
|
||||
"playlistId": "WL",
|
||||
"actions": [
|
||||
{
|
||||
"addedVideoId": "M_8QNw_JM4I",
|
||||
"action": "ACTION_ADD_VIDEO"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"toggledServiceEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"sendPost": true,
|
||||
"apiUrl": "/youtubei/v1/browse/edit_playlist"
|
||||
}
|
||||
},
|
||||
"playlistEditEndpoint": {
|
||||
"playlistId": "WL",
|
||||
"actions": [
|
||||
{
|
||||
"action": "ACTION_REMOVE_VIDEO_BY_VIDEO_ID",
|
||||
"removedVideoId": "M_8QNw_JM4I"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"untoggledAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "Watch later"
|
||||
}
|
||||
},
|
||||
"toggledAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "Added"
|
||||
}
|
||||
},
|
||||
"trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
|
||||
}
|
||||
},
|
||||
{
|
||||
"thumbnailOverlayToggleButtonRenderer": {
|
||||
"untoggledIcon": {
|
||||
"iconType": "ADD_TO_QUEUE_TAIL"
|
||||
},
|
||||
"toggledIcon": {
|
||||
"iconType": "PLAYLIST_ADD_CHECK"
|
||||
},
|
||||
"untoggledTooltip": "Add to queue",
|
||||
"toggledTooltip": "Added",
|
||||
"untoggledServiceEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"sendPost": true
|
||||
}
|
||||
},
|
||||
"signalServiceEndpoint": {
|
||||
"signal": "CLIENT_SIGNAL",
|
||||
"actions": [
|
||||
{
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"addToPlaylistCommand": {
|
||||
"openMiniplayer": true,
|
||||
"videoId": "M_8QNw_JM4I",
|
||||
"listType": "PLAYLIST_EDIT_LIST_TYPE_QUEUE",
|
||||
"onCreateListCommand": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"sendPost": true,
|
||||
"apiUrl": "/youtubei/v1/playlist/create"
|
||||
}
|
||||
},
|
||||
"createPlaylistServiceEndpoint": {
|
||||
"videoIds": [
|
||||
"M_8QNw_JM4I"
|
||||
],
|
||||
"params": "CAQ%3D"
|
||||
}
|
||||
},
|
||||
"videoIds": [
|
||||
"M_8QNw_JM4I"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"untoggledAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "Add to queue"
|
||||
}
|
||||
},
|
||||
"toggledAccessibility": {
|
||||
"accessibilityData": {
|
||||
"label": "Added"
|
||||
}
|
||||
},
|
||||
"trackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
|
||||
}
|
||||
},
|
||||
{
|
||||
"thumbnailOverlayNowPlayingRenderer": {
|
||||
"text": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Now playing"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"thumbnailOverlayLoadingPreviewRenderer": {
|
||||
"text": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Keep hovering to play"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"inlinePlaybackEndpoint": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/watch?v=M_8QNw_JM4I&pp=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"webPageType": "WEB_PAGE_TYPE_WATCH",
|
||||
"rootVe": 3832
|
||||
}
|
||||
},
|
||||
"watchEndpoint": {
|
||||
"videoId": "M_8QNw_JM4I",
|
||||
"params": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"playerParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"playerExtraUrlParams": [
|
||||
{
|
||||
"key": "inline",
|
||||
"value": "1"
|
||||
}
|
||||
],
|
||||
"watchEndpointSupportedOnesieConfig": {
|
||||
"html5PlaybackOnesieConfig": {
|
||||
"commonConfig": {
|
||||
"url": "https://rr4---sn-n0ogpnx-1gie.googlevideo.com/initplayback?source=youtube&oeis=1&c=WEB&oad=3200&ovd=3200&oaad=11000&oavd=11000&ocs=700&oewis=1&oputc=1&ofpcc=1&msp=1&odepv=1&oreouc=1&id=33ff10370fc93382&ip=1.1.1.1&initcwndbps=3320000&mt=1759654189&oweuc="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchVideoResultEntityKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"avatar": {
|
||||
"decoratedAvatarViewModel": {
|
||||
"avatar": {
|
||||
"avatarViewModel": {
|
||||
"image": {
|
||||
"sources": [
|
||||
{
|
||||
"url": "https://yt3.ggpht.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"width": 68,
|
||||
"height": 68
|
||||
}
|
||||
]
|
||||
},
|
||||
"avatarImageSize": "AVATAR_SIZE_M"
|
||||
}
|
||||
},
|
||||
"a11yLabel": "Go to channel",
|
||||
"rendererContext": {
|
||||
"commandContext": {
|
||||
"onTap": {
|
||||
"innertubeCommand": {
|
||||
"clickTrackingParams": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"commandMetadata": {
|
||||
"webCommandMetadata": {
|
||||
"url": "/@BluntBrothersProductions",
|
||||
"webPageType": "WEB_PAGE_TYPE_CHANNEL",
|
||||
"rootVe": 3611,
|
||||
"apiUrl": "/youtubei/v1/browse"
|
||||
}
|
||||
},
|
||||
"browseEndpoint": {
|
||||
"browseId": "UCUPrbbdnot-aPgNM65svgOg",
|
||||
"canonicalBaseUrl": "/@BluntBrothersProductions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue