diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java index c1cf2e0e1..b5d673596 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java @@ -4,30 +4,36 @@ package org.schabi.newpipe.extractor.stream; * Created by Christian Schabesberger on 04.03.16. * * Copyright (C) Christian Schabesberger 2016 - * AudioStream.java is part of NewPipe. + * AudioStream.java is part of NewPipe Extractor. * - * NewPipe is free software: you can redistribute it and/or modify + * 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 is distributed in the hope that it will be useful, + * 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 + * 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. If not, see . + * along with NewPipe Extractor. If not, see . */ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.services.youtube.ItagItem; -public class AudioStream extends Stream { +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +public final class AudioStream extends Stream { + public static final int UNKNOWN_BITRATE = -1; + private final int averageBitrate; - // Fields for Dash - private int itag; + // Fields for DASH + private int itag = ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE; private int bitrate; private int initStart; private int initEnd; @@ -35,37 +41,249 @@ public class AudioStream extends Stream { private int indexEnd; private String quality; private String codec; + @Nullable + private ItagItem itagItem; /** - * Create a new audio stream - * @param url the url - * @param format the format - * @param averageBitrate the average bitrate + * Class to build {@link AudioStream} objects. */ - public AudioStream(final String url, - final MediaFormat format, - final int averageBitrate) { - super(url, format); + @SuppressWarnings("checkstyle:hiddenField") + public static final class Builder { + private String id; + private String content; + private boolean isUrl; + private DeliveryMethod deliveryMethod = DeliveryMethod.PROGRESSIVE_HTTP; + @Nullable + private MediaFormat mediaFormat; + @Nullable + private String baseUrl; + private int averageBitrate = UNKNOWN_BITRATE; + @Nullable + private ItagItem itagItem; + + /** + * Create a new {@link Builder} instance with its default values. + */ + public Builder() { + } + + /** + * Set the identifier of the {@link SubtitlesStream}. + * + *

+ * It must be not null and should be non empty. + *

+ * + *

+ * If you are not able to get an identifier, use the static constant {@link + * Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class. + *

+ * + * @param id the identifier of the {@link SubtitlesStream}, which must be not null + * @return this {@link Builder} instance + */ + public Builder setId(@Nonnull final String id) { + this.id = id; + return this; + } + + /** + * Set the content of the {@link AudioStream}. + * + *

+ * It must be non null and should be non empty. + *

+ * + * @param content the content of the {@link AudioStream} + * @param isUrl whether the content is a URL + * @return this {@link Builder} instance + */ + public Builder setContent(@Nonnull final String content, + final boolean isUrl) { + this.content = content; + this.isUrl = isUrl; + return this; + } + + /** + * Set the {@link MediaFormat} used by the {@link AudioStream}. + * + *

+ * It should be one of the audio {@link MediaFormat}s ({@link MediaFormat#M4A M4A}, + * {@link MediaFormat#WEBMA WEBMA}, {@link MediaFormat#MP3 MP3}, {@link MediaFormat#OPUS + * OPUS}, {@link MediaFormat#OGG OGG}, {@link MediaFormat#WEBMA_OPUS WEBMA_OPUS}) but can + * be {@code null} if the media format could not be determined. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param mediaFormat the {@link MediaFormat} of the {@link AudioStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setMediaFormat(@Nullable final MediaFormat mediaFormat) { + this.mediaFormat = mediaFormat; + return this; + } + + /** + * Set the {@link DeliveryMethod} of the {@link AudioStream}. + * + *

+ * It must be not null. + *

+ * + *

+ * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}. + *

+ * + * @param deliveryMethod the {@link DeliveryMethod} of the {@link AudioStream}, which must + * be not null + * @return this {@link Builder} instance + */ + public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + return this; + } + + /** + * Set the base URL of the {@link AudioStream}. + * + *

+ * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which + * they have been parsed. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param baseUrl the base URL of the {@link AudioStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setBaseUrl(@Nullable final String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Set the average bitrate of the {@link AudioStream}. + * + *

+ * The default value is {@link #UNKNOWN_BITRATE}. + *

+ * + * @param averageBitrate the average bitrate of the {@link AudioStream}, which should + * positive + * @return this {@link Builder} instance + */ + public Builder setAverageBitrate(final int averageBitrate) { + this.averageBitrate = averageBitrate; + return this; + } + + /** + * Set the {@link ItagItem} corresponding to the {@link AudioStream}. + * + *

+ * {@link ItagItem}s are YouTube specific objects, so they are only known for this service + * and can be null. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param itagItem the {@link ItagItem} of the {@link AudioStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setItagItem(@Nullable final ItagItem itagItem) { + this.itagItem = itagItem; + return this; + } + + /** + * Build an {@link AudioStream} using the builder's current values. + * + *

+ * The identifier and the content (and so the {@code isUrl} boolean) properties must have + * been set. + *

+ * + * @return a new {@link AudioStream} using the builder's current values + * @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}) or + * {@code deliveryMethod} have been not set or set as {@code null} + */ + @Nonnull + public AudioStream build() { + if (id == null) { + throw new IllegalStateException( + "The identifier of the audio stream has been not set or is null. If you " + + "are not able to get an identifier, use the static constant " + + "ID_UNKNOWN of the Stream class."); + } + + if (content == null) { + throw new IllegalStateException("The content of the audio stream has been not set " + + "or is null. Please specify a non-null one with setContent."); + } + + if (deliveryMethod == null) { + throw new IllegalStateException( + "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, + baseUrl, itagItem); + } + } + + + /** + * Create a new audio stream. + * + * @param id the ID 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 itagItem the {@link ItagItem} corresponding to the stream, which cannot be null + * @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more + * information) + */ + 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 baseUrl, + @Nullable final ItagItem itagItem) { + super(id, content, isUrl, format, deliveryMethod, baseUrl); + 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(); + } this.averageBitrate = averageBitrate; } /** - * Create a new audio stream - * @param url the url - * @param itag the ItagItem of the Stream + * {@inheritDoc} */ - public AudioStream(final String url, final ItagItem itag) { - this(url, itag.getMediaFormat(), itag.avgBitrate); - this.itag = itag.id; - this.quality = itag.getQuality(); - this.bitrate = itag.getBitrate(); - this.initStart = itag.getInitStart(); - this.initEnd = itag.getInitEnd(); - this.indexStart = itag.getIndexStart(); - this.indexEnd = itag.getIndexEnd(); - this.codec = itag.getCodec(); - } - @Override public boolean equalStats(final Stream cmp) { return super.equalStats(cmp) && cmp instanceof AudioStream @@ -73,42 +291,125 @@ public class AudioStream extends Stream { } /** - * Get the average bitrate - * @return the average bitrate or -1 + * Get the average bitrate of the stream. + * + * @return the average bitrate or {@link #UNKNOWN_BITRATE} if it is unknown */ public int getAverageBitrate() { return averageBitrate; } + /** + * Get the itag identifier of the stream. + * + *

+ * Always equals to {@link #ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE} for other streams than the + * ones of the YouTube service. + *

+ * + * @return the number of the {@link ItagItem} passed in the constructor of the audio stream. + */ public int getItag() { return itag; } + /** + * Get the bitrate of the stream. + * + * @return the bitrate set from the {@link ItagItem} passed in the constructor of the stream. + */ public int getBitrate() { return bitrate; } + /** + * Get the initialization start of the stream. + * + * @return the initialization start value set from the {@link ItagItem} passed in the + * constructor of the stream. + */ public int getInitStart() { return initStart; } + /** + * Get the initialization end of the stream. + * + * @return the initialization end value set from the {@link ItagItem} passed in the constructor + * of the stream. + */ public int getInitEnd() { return initEnd; } + /** + * Get the index start of the stream. + * + * @return the index start value set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getIndexStart() { return indexStart; } + /** + * Get the index end of the stream. + * + * @return the index end value set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getIndexEnd() { return indexEnd; } + /** + * Get the quality of the stream. + * + * @return the quality label set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public String getQuality() { return quality; } + /** + * Get the codec of the stream. + * + * @return the codec set from the {@link ItagItem} passed in the constructor of the stream. + */ public String getCodec() { return codec; } + + /** + * {@inheritDoc} + */ + @Override + @Nullable + public ItagItem getItagItem() { + return itagItem; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + if (!super.equals(obj)) { + return false; + } + + final AudioStream audioStream = (AudioStream) obj; + return averageBitrate == audioStream.averageBitrate; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), averageBitrate); + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java new file mode 100644 index 000000000..db74e91ab --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java @@ -0,0 +1,37 @@ +package org.schabi.newpipe.extractor.stream; + +/** + * An enum to represent the different delivery methods of {@link Stream streams} which are returned + * by the extractor. + */ +public enum DeliveryMethod { + + /** + * Enum constant which represents the use of the progressive HTTP streaming method to fetch a + * {@link Stream stream}. + */ + PROGRESSIVE_HTTP, + + /** + * Enum constant which represents the use of the DASH adaptive streaming method to fetch a + * {@link Stream stream}. + */ + DASH, + + /** + * Enum constant which represents the use of the HLS adaptive streaming method to fetch a + * {@link Stream stream}. + */ + HLS, + + /** + * Enum constant which represents the use of the SmoothStreaming adaptive streaming method to + * fetch a {@link Stream stream}. + */ + SS, + + /** + * Enum constant which represents the use of a torrent to fetch a {@link Stream stream}. + */ + TORRENT +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java index 5b827c159..b76594a6f 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java @@ -1,68 +1,73 @@ package org.schabi.newpipe.extractor.stream; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.services.youtube.ItagItem; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; -import org.schabi.newpipe.extractor.MediaFormat; - -import java.io.Serializable; -import java.util.List; - /** - * Creates a stream object from url, format and optional torrent url + * Abstract class which represents streams in the extractor. */ public abstract class Stream implements Serializable { - private final MediaFormat mediaFormat; - private final String url; - private final String torrentUrl; + public static final int FORMAT_ID_UNKNOWN = -1; + public static final String ID_UNKNOWN = " "; /** - * @deprecated Use {@link #getFormat()} or {@link #getFormatId()} - */ - @Deprecated - public final int format; - - /** - * Instantiates a new stream object. + * An integer to represent that the itag id returned is not available (only for YouTube, this + * should never happen) or not applicable (for other services than YouTube). * - * @param url the url - * @param format the format + *

+ * An itag should not have a negative value so {@code -1} is used for this constant. + *

*/ - public Stream(final String url, final MediaFormat format) { - this(url, null, format); - } + public static final int ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE = -1; + + private final String id; + @Nullable private final MediaFormat mediaFormat; + private final String content; + private final boolean isUrl; + private final DeliveryMethod deliveryMethod; + @Nullable private final String baseUrl; /** - * Instantiates a new stream object. + * Instantiates a new {@code Stream} object. * - * @param url the url - * @param torrentUrl the url to torrent file, example - * https://webtorrent.io/torrents/big-buck-bunny.torrent - * @param format the format + * @param id the ID which uniquely identifies the file, e.g. for YouTube this would + * be the itag + * @param content the content or URL, 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}, which can be null + * @param deliveryMethod the delivery method of the stream + * @param baseUrl the base URL of the content if the stream is a DASH or an HLS + * manifest, which can be null */ - public Stream(final String url, final String torrentUrl, final MediaFormat format) { - this.url = url; - this.torrentUrl = torrentUrl; - //noinspection deprecation - this.format = format.id; + public Stream(final String id, + final String content, + final boolean isUrl, + @Nullable final MediaFormat format, + final DeliveryMethod deliveryMethod, + @Nullable final String baseUrl) { + this.id = id; + this.content = content; + this.isUrl = isUrl; this.mediaFormat = format; + this.deliveryMethod = deliveryMethod; + this.baseUrl = baseUrl; } /** - * Reveals whether two streams have the same stats (format and bitrate, for example) - */ - public boolean equalStats(final Stream cmp) { - return cmp != null && getFormatId() == cmp.getFormatId(); - } - - /** - * Reveals whether two Streams are equal - */ - public boolean equals(final Stream cmp) { - return equalStats(cmp) && url.equals(cmp.url); - } - - /** - * Check if the list already contains one stream with equals stats + * Checks if the list already contains one stream with equals stats. + * + * @param stream the stream which will be compared to the streams in the stream list + * @param streamList the list of {@link Stream Streams} which will be compared + * @return whether the list already contains one stream with equals stats */ public static boolean containSimilarStream(final Stream stream, final List streamList) { @@ -78,38 +83,170 @@ public abstract class Stream implements Serializable { } /** - * Gets the url. + * Reveals whether two streams have the same stats ({@link MediaFormat media format} and + * {@link DeliveryMethod delivery method}). * - * @return the url + *

+ * If the {@link MediaFormat media format} of the stream is unknown, the streams are compared + * by only using the {@link DeliveryMethod delivery method} and their id. + *

+ * + *

+ * Note: This method always returns always false if the stream passed is null. + *

+ * + * @param cmp the stream object to be compared to this stream object + * @return whether the stream have the same stats or not, based on the criteria above */ + public boolean equalStats(@Nullable final Stream cmp) { + if (cmp == null) { + return false; + } + + Boolean haveSameMediaFormatId = null; + if (mediaFormat != null && cmp.mediaFormat != null) { + haveSameMediaFormatId = mediaFormat.id == cmp.mediaFormat.id; + } + final boolean areUsingSameDeliveryMethodAndAreUrlStreams = + deliveryMethod == cmp.deliveryMethod && isUrl == cmp.isUrl; + + return haveSameMediaFormatId != null + ? haveSameMediaFormatId && areUsingSameDeliveryMethodAndAreUrlStreams + : areUsingSameDeliveryMethodAndAreUrlStreams; + } + + /** + * Reveals whether two streams are equal. + * + * @param cmp the stream object to be compared to this stream object + * @return whether streams are equal + */ + public boolean equals(final Stream cmp) { + return equalStats(cmp) && content.equals(cmp.content); + } + + /** + * Gets the identifier of this stream, e.g. the itag for YouTube. + * + *

+ * It should be normally unique but {@link #ID_UNKNOWN} may be returned as the identifier if + * one used by the stream extractor cannot be extracted, if the extractor uses a value from a + * streaming service. + *

+ * + * @return the id (which may be {@link #ID_UNKNOWN}) + */ + public String getId() { + return id; + } + + /** + * Gets the URL of this stream if the content is a URL, or {@code null} if that's the not case. + * + * @return the URL if the content is a URL, {@code null} otherwise + * @deprecated Use {@link #getContent()} instead. + */ + @Deprecated + @Nullable public String getUrl() { - return url; + return isUrl ? content : null; } /** - * Gets the torrent url. + * Gets the content or URL. * - * @return the torrent url, example https://webtorrent.io/torrents/big-buck-bunny.torrent + * @return the content or URL */ - public String getTorrentUrl() { - return torrentUrl; + public String getContent() { + return content; } /** - * Gets the format. + * Returns if the content is a URL or not. + * + * @return {@code true} if the content of this stream content is a URL, {@code false} + * if it is the actual content + */ + public boolean isUrl() { + return isUrl; + } + + /** + * Gets the {@link MediaFormat}, which can be null. * * @return the format */ + @Nullable public MediaFormat getFormat() { return mediaFormat; } /** - * Gets the format id. + * Gets the format id, which can be unknown. * - * @return the format id + * @return the format id or {@link #FORMAT_ID_UNKNOWN} */ public int getFormatId() { - return mediaFormat.id; + if (mediaFormat != null) { + return mediaFormat.id; + } + return FORMAT_ID_UNKNOWN; } -} + + /** + * Gets the delivery method. + * + * @return the delivery method + */ + @Nonnull + public DeliveryMethod getDeliveryMethod() { + return deliveryMethod; + } + + /** + * Gets the base URL of a stream. + * + *

+ * If the stream is not a DASH stream or an HLS stream, this value will always be null. + * It may be also null for these streams too. + *

+ * + * @return the base URL of the stream or {@code null} + */ + @Nullable + public String getBaseUrl() { + return baseUrl; + } + + /** + * Gets the {@link ItagItem} of a stream. + * + *

+ * If the stream is not a YouTube stream, this value will always be null. + *

+ * + * @return the {@link ItagItem} of the stream or {@code null} + */ + @Nullable + public abstract ItagItem getItagItem(); + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + final Stream stream = (Stream) obj; + return id.equals(stream.id) && mediaFormat == stream.mediaFormat + && deliveryMethod == stream.deliveryMethod; + } + + @Override + public int hashCode() { + return Objects.hash(id, mediaFormat, deliveryMethod); + } +} \ No newline at end of file diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java index 0ac01a89c..732d822d7 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java @@ -1,53 +1,295 @@ package org.schabi.newpipe.extractor.stream; import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.services.youtube.ItagItem; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; -import java.io.Serializable; import java.util.Locale; +import java.util.Objects; -public class SubtitlesStream extends Stream implements Serializable { +import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; + +public final class SubtitlesStream extends Stream { private final MediaFormat format; private final Locale locale; private final boolean autoGenerated; private final String code; - public SubtitlesStream(final MediaFormat format, - final String languageCode, - final String url, - final boolean autoGenerated) { - super(url, format); + /** + * Class to build {@link SubtitlesStream} objects. + */ + @SuppressWarnings("checkstyle:HiddenField") + public static final class Builder { + private String id; + private String content; + private boolean isUrl; + private DeliveryMethod deliveryMethod = DeliveryMethod.PROGRESSIVE_HTTP; + @Nullable + private MediaFormat mediaFormat; + @Nullable + private String baseUrl; + private String languageCode; + // Use of the Boolean class instead of the primitive type needed for setter call check + private Boolean autoGenerated; + + /** + * Create a new {@link Builder} instance with its default values. + */ + public Builder() { + } + + /** + * Set the identifier of the {@link SubtitlesStream}. + * + * @param id the identifier of the {@link SubtitlesStream}, which should be not null + * (otherwise the fallback to create the identifier will be used when building + * the builder) + * @return this {@link Builder} instance + */ + public Builder setId(@Nonnull final String id) { + this.id = id; + return this; + } + + /** + * Set the content of the {@link SubtitlesStream}. + * + *

+ * It must be non null and should be non empty. + *

+ * + * @param content the content of the {@link SubtitlesStream} + * @param isUrl whether the content is a URL + * @return this {@link Builder} instance + */ + public Builder setContent(@Nonnull final String content, + final boolean isUrl) { + this.content = content; + this.isUrl = isUrl; + return this; + } + + /** + * Set the {@link MediaFormat} used by the {@link SubtitlesStream}. + * + *

+ * It should be one of the subtitles {@link MediaFormat}s ({@link MediaFormat#SRT SRT}, + * {@link MediaFormat#TRANSCRIPT1 TRANSCRIPT1}, {@link MediaFormat#TRANSCRIPT2 + * TRANSCRIPT2}, {@link MediaFormat#TRANSCRIPT3 TRANSCRIPT3}, {@link MediaFormat#TTML + * TTML}, {@link MediaFormat#VTT VTT}) but can be {@code null} if the media format could + * not be determined. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param mediaFormat the {@link MediaFormat} of the {@link SubtitlesStream}, which can be + * null + * @return this {@link Builder} instance + */ + public Builder setMediaFormat(@Nullable final MediaFormat mediaFormat) { + this.mediaFormat = mediaFormat; + return this; + } + + /** + * Set the {@link DeliveryMethod} of the {@link SubtitlesStream}. + * + *

+ * It must be not null. + *

+ * + *

+ * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}. + *

+ * + * @param deliveryMethod the {@link DeliveryMethod} of the {@link SubtitlesStream}, which + * must be not null + * @return this {@link Builder} instance + */ + public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + return this; + } + + /** + * Set the base URL of the {@link SubtitlesStream}. + * + *

+ * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which + * they have been parsed. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param baseUrl the base URL of the {@link SubtitlesStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setBaseUrl(@Nullable final String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Set the language code of the {@link SubtitlesStream}. + * + *

+ * It must be not null and should be not an empty string. + *

+ * + * @param languageCode the language code of the {@link SubtitlesStream} + * @return this {@link Builder} instance + */ + public Builder setLanguageCode(@Nonnull final String languageCode) { + this.languageCode = languageCode; + return this; + } + + /** + * Set whether the subtitles have been generated by the streaming service. + * + * @param autoGenerated whether the subtitles have been generated by the streaming + * service + * @return this {@link Builder} instance + */ + public Builder setAutoGenerated(final boolean autoGenerated) { + this.autoGenerated = autoGenerated; + return this; + } + + /** + * Build a {@link SubtitlesStream} using the builder's current values. + * + *

+ * The content (and so the {@code isUrl} boolean), the language code and the {@code + * isAutoGenerated} properties must have been set. + *

+ * + *

+ * If no identifier has been set, an identifier will be generated using the language code + * and the media format suffix if the media format is known + *

+ * + * @return a new {@link SubtitlesStream} using the builder's current values + * @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}), + * {@code deliveryMethod}, {@code languageCode} or the {@code isAutogenerated} have been + * not set or set as {@code null} + */ + @Nonnull + public SubtitlesStream build() { + if (content == null) { + throw new IllegalStateException("No valid content was specified. Please specify a " + + "valid one with setContent."); + } + + if (deliveryMethod == null) { + throw new IllegalStateException( + "The delivery method of the subtitles stream has been set as null, which " + + "is not allowed. Pass a valid one instead with" + + "setDeliveryMethod."); + } + + if (languageCode == null) { + throw new IllegalStateException("The language code of the subtitles stream has " + + "been not set or is null. Make sure you specified an non null language " + + "code with setLanguageCode."); + } + + if (autoGenerated == null) { + throw new IllegalStateException("The subtitles stream has been not set as an " + + "autogenerated subtitles stream or not. Please specify this information " + + "with setIsAutoGenerated."); + } + + if (id == null) { + id = languageCode + (mediaFormat != null ? "." + mediaFormat.suffix + : EMPTY_STRING); + } + + return new SubtitlesStream(id, content, isUrl, mediaFormat, deliveryMethod, + languageCode, autoGenerated, baseUrl); + } + } + + /** + * Create a new subtitles stream. + * + * @param id the ID 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 + * @param deliveryMethod the {@link DeliveryMethod} of the stream + * @param languageCode the language code of the stream + * @param autoGenerated whether the subtitles are auto-generated by the streaming service + * @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more + * information) + */ + private SubtitlesStream(@Nonnull final String id, + @Nonnull final String content, + final boolean isUrl, + @Nullable final MediaFormat format, + @Nonnull final DeliveryMethod deliveryMethod, + @Nonnull final String languageCode, + final boolean autoGenerated, + @Nullable final String baseUrl) { + super(id, content, isUrl, format, deliveryMethod, baseUrl); /* - * Locale.forLanguageTag only for API >= 21 - * Locale.Builder only for API >= 21 - * Country codes doesn't work well without - */ + * Locale.forLanguageTag only for Android API >= 21 + * Locale.Builder only for Android API >= 21 + * Country codes doesn't work well without + */ final String[] splits = languageCode.split("-"); switch (splits.length) { - default: - this.locale = new Locale(splits[0]); - break; - case 3: - // complex variants doesn't work! - this.locale = new Locale(splits[0], splits[1], splits[2]); - break; case 2: this.locale = new Locale(splits[0], splits[1]); break; + case 3: + // Complex variants don't work! + this.locale = new Locale(splits[0], splits[1], splits[2]); + break; + default: + this.locale = new Locale(splits[0]); + break; } + this.code = languageCode; this.format = format; this.autoGenerated = autoGenerated; } + /** + * Get the extension of the subtitles. + * + * @return the extension of the subtitles + */ public String getExtension() { return format.suffix; } + /** + * Return whether if the subtitles are auto-generated. + *

+ * Some streaming services can generate subtitles for their contents, like YouTube. + *

+ * + * @return {@code true} if the subtitles are auto-generated, {@code false} otherwise + */ public boolean isAutoGenerated() { return autoGenerated; } + /** + * {@inheritDoc} + */ @Override public boolean equalStats(final Stream cmp) { return super.equalStats(cmp) @@ -56,16 +298,67 @@ public class SubtitlesStream extends Stream implements Serializable { && autoGenerated == ((SubtitlesStream) cmp).autoGenerated; } + /** + * Get the display language name of the subtitles. + * + * @return the display language name of the subtitles + */ public String getDisplayLanguageName() { return locale.getDisplayName(locale); } + /** + * Get the language tag of the subtitles. + * + * @return the language tag of the subtitles + */ public String getLanguageTag() { return code; } + /** + * Get the {@link Locale locale} of the subtitles. + * + * @return the {@link Locale locale} of the subtitles + */ public Locale getLocale() { return locale; } + /** + * No subtitles which are currently extracted use an {@link ItagItem}, so {@code null} is + * returned by this method. + * + * @return {@code null} + */ + @Nullable + @Override + public ItagItem getItagItem() { + return null; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + if (!super.equals(obj)) { + return false; + } + + final SubtitlesStream subtitlesStream = (SubtitlesStream) obj; + return autoGenerated == subtitlesStream.autoGenerated + && locale.equals(subtitlesStream.locale) + && code.equals(subtitlesStream.code); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), locale, autoGenerated, code); + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java index 9e6b4eb2b..c8ab5cfc9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java @@ -4,31 +4,42 @@ package org.schabi.newpipe.extractor.stream; * Created by Christian Schabesberger on 04.03.16. * * Copyright (C) Christian Schabesberger 2016 - * VideoStream.java is part of NewPipe. + * VideoStream.java is part of NewPipe Extractor. * - * NewPipe is free software: you can redistribute it and/or modify + * 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 is distributed in the hope that it will be useful, + * 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 + * 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. If not, see . + * along with NewPipe Extractor. If not, see . */ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.services.youtube.ItagItem; -public class VideoStream extends Stream { +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +public final class VideoStream extends Stream { + public static final String RESOLUTION_UNKNOWN = ""; + + /** @deprecated Use {@link #getResolution()} instead. */ + @Deprecated public final String resolution; + + /** @deprecated Use {@link #isVideoOnly()} instead. */ + @Deprecated public final boolean isVideoOnly; - // Fields for Dash - private int itag; + // Fields for DASH + private int itag = ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE; private int bitrate; private int initStart; private int initEnd; @@ -39,118 +50,468 @@ public class VideoStream extends Stream { private int fps; private String quality; private String codec; + @Nullable private ItagItem itagItem; - public VideoStream(final String url, final MediaFormat format, final String resolution) { - this(url, format, resolution, false); + /** + * Class to build {@link VideoStream} objects. + */ + @SuppressWarnings("checkstyle:hiddenField") + public static final class Builder { + private String id; + private String content; + private boolean isUrl; + private DeliveryMethod deliveryMethod = DeliveryMethod.PROGRESSIVE_HTTP; + @Nullable + private MediaFormat mediaFormat; + @Nullable + private String baseUrl; + // Use of the Boolean class instead of the primitive type needed for setter call check + private Boolean isVideoOnly; + private String resolution; + @Nullable + private ItagItem itagItem; + + /** + * Create a new {@link Builder} instance with its default values. + */ + public Builder() { + } + + /** + * Set the identifier of the {@link VideoStream}. + * + *

+ * It must be not null and should be non empty. + *

+ * + *

+ * If you are not able to get an identifier, use the static constant {@link + * Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class. + *

+ * + * @param id the identifier of the {@link VideoStream}, which must be not null + * @return this {@link Builder} instance + */ + public Builder setId(@Nonnull final String id) { + this.id = id; + return this; + } + + /** + * Set the content of the {@link VideoStream}. + * + *

+ * It must be non null and should be non empty. + *

+ * + * @param content the content of the {@link VideoStream} + * @param isUrl whether the content is a URL + * @return this {@link Builder} instance + */ + public Builder setContent(@Nonnull final String content, + final boolean isUrl) { + this.content = content; + this.isUrl = isUrl; + return this; + } + + /** + * Set the {@link MediaFormat} used by the {@link VideoStream}. + * + *

+ * It should be one of the video {@link MediaFormat}s ({@link MediaFormat#MPEG_4 MPEG_4}, + * {@link MediaFormat#v3GPP v3GPP}, {@link MediaFormat#WEBM WEBM}) but can be {@code null} + * if the media format could not be determined. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param mediaFormat the {@link MediaFormat} of the {@link VideoStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setMediaFormat(@Nullable final MediaFormat mediaFormat) { + this.mediaFormat = mediaFormat; + return this; + } + + /** + * Set the {@link DeliveryMethod} of the {@link VideoStream}. + * + *

+ * It must be not null. + *

+ * + *

+ * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}. + *

+ * + * @param deliveryMethod the {@link DeliveryMethod} of the {@link VideoStream}, which must + * be not null + * @return this {@link Builder} instance + */ + public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + return this; + } + + /** + * Set the base URL of the {@link VideoStream}. + * + *

+ * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which + * they have been parsed. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param baseUrl the base URL of the {@link VideoStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setBaseUrl(@Nullable final String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Set whether the {@link VideoStream} is video-only. + * + *

+ * This property must be set before building the {@link VideoStream}. + *

+ * + * @param isVideoOnly whether the {@link VideoStream} is video-only + * @return this {@link Builder} instance + */ + public Builder setIsVideoOnly(final boolean isVideoOnly) { + this.isVideoOnly = isVideoOnly; + return this; + } + + /** + * Set the resolution of the {@link VideoStream}. + * + *

+ * This resolution can be used by clients to know the quality of the video stream. + *

+ * + *

+ * If you are not able to know the resolution, you should use {@link #RESOLUTION_UNKNOWN} + * as the resolution of the video stream. + *

+ * + *

+ * It must be set before building the builder and not null. + *

+ * + * @param resolution the resolution of the {@link VideoStream} + * @return this {@link Builder} instance + */ + public Builder setResolution(@Nonnull final String resolution) { + this.resolution = resolution; + return this; + } + + /** + * Set the {@link ItagItem} corresponding to the {@link VideoStream}. + * + *

+ * {@link ItagItem}s are YouTube specific objects, so they are only known for this service + * and can be null. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param itagItem the {@link ItagItem} of the {@link VideoStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setItagItem(@Nullable final ItagItem itagItem) { + this.itagItem = itagItem; + return this; + } + + /** + * Build a {@link VideoStream} using the builder's current values. + * + *

+ * The identifier, the content (and so the {@code isUrl} boolean), the {@code isVideoOnly} + * and the {@code resolution} properties must have been set. + *

+ * + * @return a new {@link VideoStream} using the builder's current values + * @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}), + * {@code deliveryMethod}, {@code isVideoOnly} or {@code resolution} have been not set or + * set as {@code null} + */ + @Nonnull + public VideoStream build() { + if (id == null) { + throw new IllegalStateException( + "The identifier of the video stream has been not set or is null. If you " + + "are not able to get an identifier, use the static constant " + + "ID_UNKNOWN of the Stream class."); + } + + if (content == null) { + throw new IllegalStateException("The content of the video stream has been not set " + + "or is null. Please specify a non-null one with setContent."); + } + + if (deliveryMethod == null) { + throw new IllegalStateException( + "The delivery method of the video stream has been set as null, which is " + + "not allowed. Pass a valid one instead with setDeliveryMethod."); + } + + if (isVideoOnly == null) { + throw new IllegalStateException("The video stream has been not set as a " + + "video-only stream or as a video stream with embedded audio. Please " + + "specify this information with setIsVideoOnly."); + } + + if (resolution == null) { + throw new IllegalStateException( + "The resolution of the video stream has been not set. Please specify it " + + "with setResolution (use an empty string if you are not able to " + + "get it)."); + } + + return new VideoStream(id, content, isUrl, mediaFormat, deliveryMethod, resolution, + isVideoOnly, baseUrl, itagItem); + } } - public VideoStream(final String url, - final MediaFormat format, - final String resolution, - final boolean isVideoOnly) { - this(url, null, format, resolution, isVideoOnly); - } - - public VideoStream(final String url, final boolean isVideoOnly, final ItagItem itag) { - this(url, itag.getMediaFormat(), itag.resolutionString, isVideoOnly); - this.itag = itag.id; - this.bitrate = itag.getBitrate(); - this.initStart = itag.getInitStart(); - this.initEnd = itag.getInitEnd(); - this.indexStart = itag.getIndexStart(); - this.indexEnd = itag.getIndexEnd(); - this.codec = itag.getCodec(); - this.height = itag.getHeight(); - this.width = itag.getWidth(); - this.quality = itag.getQuality(); - this.fps = itag.fps; - } - - public VideoStream(final String url, - final String torrentUrl, - final MediaFormat format, - final String resolution) { - this(url, torrentUrl, format, resolution, false); - } - - public VideoStream(final String url, - final String torrentUrl, - final MediaFormat format, - final String resolution, - final boolean isVideoOnly) { - super(url, torrentUrl, format); + /** + * Create a new video stream. + * + * @param id the ID 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 resolution the resolution of the stream + * @param isVideoOnly whether the stream is video-only + * @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null + * @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more + * information) + */ + private VideoStream(@Nonnull final String id, + @Nonnull final String content, + final boolean isUrl, + @Nullable final MediaFormat format, + @Nonnull final DeliveryMethod deliveryMethod, + @Nonnull final String resolution, + final boolean isVideoOnly, + @Nullable final String baseUrl, + @Nullable final ItagItem itagItem) { + super(id, content, isUrl, format, deliveryMethod, baseUrl); + if (itagItem != null) { + this.itagItem = itagItem; + this.itag = itagItem.id; + this.bitrate = itagItem.getBitrate(); + this.initStart = itagItem.getInitStart(); + this.initEnd = itagItem.getInitEnd(); + this.indexStart = itagItem.getIndexStart(); + this.indexEnd = itagItem.getIndexEnd(); + this.codec = itagItem.getCodec(); + this.height = itagItem.getHeight(); + this.width = itagItem.getWidth(); + this.quality = itagItem.getQuality(); + this.fps = itagItem.getFps(); + } this.resolution = resolution; this.isVideoOnly = isVideoOnly; } + /** + * {@inheritDoc} + */ @Override public boolean equalStats(final Stream cmp) { - return super.equalStats(cmp) && cmp instanceof VideoStream + return super.equalStats(cmp) + && cmp instanceof VideoStream && resolution.equals(((VideoStream) cmp).resolution) && isVideoOnly == ((VideoStream) cmp).isVideoOnly; } /** - * Get the video resolution + * Get the video resolution. * - * @return the video resolution + *

+ * It can be unknown for some streams, like for HLS master playlists. In this case, + * {@link #RESOLUTION_UNKNOWN} is returned by this method. + *

+ * + * @return the video resolution or {@link #RESOLUTION_UNKNOWN} */ + @Nonnull public String getResolution() { return resolution; } /** - * Check if the video is video only. - *

- * Video only streams have no audio + * Return whether the stream is video-only. * - * @return {@code true} if this stream is vid + *

+ * Video-only streams have no audio. + *

+ * + * @return {@code true} if this stream is video-only, {@code false} otherwise */ public boolean isVideoOnly() { return isVideoOnly; } + /** + * Get the itag identifier of the stream. + * + *

+ * Always equals to {@link #ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE} for other streams than the + * ones of the YouTube service. + *

+ * + * @return the number of the {@link ItagItem} passed in the constructor of the video stream. + */ public int getItag() { return itag; } + /** + * Get the bitrate of the stream. + * + * @return the bitrate set from the {@link ItagItem} passed in the constructor of the stream. + */ public int getBitrate() { return bitrate; } + /** + * Get the initialization start of the stream. + * + * @return the initialization start value set from the {@link ItagItem} passed in the + * constructor of the + * stream. + */ public int getInitStart() { return initStart; } + /** + * Get the initialization end of the stream. + * + * @return the initialization end value set from the {@link ItagItem} passed in the constructor + * of the stream. + */ public int getInitEnd() { return initEnd; } + /** + * Get the index start of the stream. + * + * @return the index start value set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getIndexStart() { return indexStart; } + /** + * Get the index end of the stream. + * + * @return the index end value set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getIndexEnd() { return indexEnd; } + /** + * Get the width of the video stream. + * + * @return the width set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getWidth() { return width; } + /** + * Get the height of the video stream. + * + * @return the height set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getHeight() { return height; } + /** + * Get the frames per second of the video stream. + * + * @return the frames per second set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getFps() { return fps; } + /** + * Get the quality of the stream. + * + * @return the quality label set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public String getQuality() { return quality; } + /** + * Get the codec of the stream. + * + * @return the codec set from the {@link ItagItem} passed in the constructor of the stream. + */ public String getCodec() { return codec; } + + /** + * {@inheritDoc} + */ + @Override + @Nullable + public ItagItem getItagItem() { + return itagItem; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + if (!super.equals(obj)) { + return false; + } + + final VideoStream videoStream = (VideoStream) obj; + return isVideoOnly == videoStream.isVideoOnly && resolution.equals(videoStream.resolution); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), resolution, isVideoOnly); + } }