From c2b9537b6645449cc81d1870ab0836dc7b4b7cbc Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 28 May 2022 23:09:02 +0200 Subject: [PATCH 01/43] Created new basic stream architecture --- .../streamdata/delivery/DASHDeliveryData.java | 5 ++ .../delivery/DASHManifestDeliveryData.java | 18 +++++ .../delivery/DASHUrlDeliveryData.java | 5 ++ .../streamdata/delivery/DeliveryData.java | 5 ++ .../streamdata/delivery/HLSDeliveryData.java | 5 ++ .../delivery/ProgressiveHTTPDeliveryData.java | 5 ++ .../delivery/TorrentDeliveryData.java | 5 ++ .../delivery/UrlBasedDeliveryData.java | 8 ++ .../simpleimpl/AbstractDeliveryDataImpl.java | 7 ++ .../AbstractUrlBasedDeliveryDataImpl.java | 25 ++++++ .../SimpleDASHManifestDeliveryDataImpl.java | 34 ++++++++ .../SimpleDASHUrlDeliveryDataImpl.java | 12 +++ .../simpleimpl/SimpleHLSDeliveryDataImpl.java | 12 +++ ...SimpleProgressiveHTTPDeliveryDataImpl.java | 12 +++ .../SimpleTorrentDeliveryDataImpl.java | 12 +++ .../format/AbstractMediaFormat.java | 51 ++++++++++++ .../streamdata/format/AudioMediaFormat.java | 14 ++++ .../format/SubtitleMediaFormat.java | 12 +++ .../format/VideoAudioMediaFormat.java | 12 +++ .../format/registry/AudioFormatRegistry.java | 23 ++++++ .../format/registry/MediaFormatRegistry.java | 79 +++++++++++++++++++ .../registry/SubtitleFormatRegistry.java | 25 ++++++ .../registry/VideoAudioFormatRegistry.java | 17 ++++ .../streamdata/stream/AudioStream.java | 40 ++++++++++ .../extractor/streamdata/stream/Stream.java | 15 ++++ .../streamdata/stream/SubtitleStream.java | 60 ++++++++++++++ .../streamdata/stream/VideoAudioStream.java | 17 ++++ .../streamdata/stream/VideoStream.java | 38 +++++++++ .../stream/simpleimpl/AbstractStreamImpl.java | 23 ++++++ .../simpleimpl/SimpleAudioStreamImpl.java | 53 +++++++++++++ .../simpleimpl/SimpleSubtitleStreamImpl.java | 71 +++++++++++++++++ .../SimpleVideoAudioStreamImpl.java | 74 +++++++++++++++++ .../simpleimpl/SimpleVideoStreamImpl.java | 50 ++++++++++++ 33 files changed, 844 insertions(+) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHUrlDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/HLSDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/TorrentDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/AbstractDeliveryDataImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/AbstractUrlBasedDeliveryDataImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHUrlDeliveryDataImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleHLSDeliveryDataImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleProgressiveHTTPDeliveryDataImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleTorrentDeliveryDataImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/SubtitleMediaFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/VideoAudioMediaFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/AudioFormatRegistry.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/SubtitleFormatRegistry.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/VideoAudioFormatRegistry.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/AbstractStreamImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleSubtitleStreamImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHDeliveryData.java new file mode 100644 index 0000000000..42d7e452d5 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHDeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +public interface DASHDeliveryData extends DeliveryData { + // Just a marker interface +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java new file mode 100644 index 0000000000..ce2452a732 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java @@ -0,0 +1,18 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +import javax.annotation.Nonnull; + +public interface DASHManifestDeliveryData extends DASHDeliveryData { + /** + * Returns the base url for the DashManifest. + * + * @return + */ + // TODO: Check removal + @Nonnull + default String getBaseUrl() { + return ""; + } + + String getManifestAsString(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHUrlDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHUrlDeliveryData.java new file mode 100644 index 0000000000..ff9ff1b2e7 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHUrlDeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +public interface DASHUrlDeliveryData extends UrlBasedDeliveryData, DASHDeliveryData { + // Nothing to implement additionally +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java new file mode 100644 index 0000000000..9149d7794c --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +public interface DeliveryData { + // Only a marker so far +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/HLSDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/HLSDeliveryData.java new file mode 100644 index 0000000000..4023a32e55 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/HLSDeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +public interface HLSDeliveryData extends UrlBasedDeliveryData { + // Nothing to implement additionally +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java new file mode 100644 index 0000000000..3817d47071 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +public interface ProgressiveHTTPDeliveryData extends DeliveryData { + // Nothing to implement additionally +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/TorrentDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/TorrentDeliveryData.java new file mode 100644 index 0000000000..b7112a37b9 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/TorrentDeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +public interface TorrentDeliveryData extends UrlBasedDeliveryData { + // Nothing to implement additionally +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java new file mode 100644 index 0000000000..a0df54f913 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java @@ -0,0 +1,8 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +import javax.annotation.Nonnull; + +public interface UrlBasedDeliveryData extends DeliveryData { + @Nonnull + String url(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/AbstractDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/AbstractDeliveryDataImpl.java new file mode 100644 index 0000000000..c6c70163b6 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/AbstractDeliveryDataImpl.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; + +public abstract class AbstractDeliveryDataImpl implements DeliveryData { + // Nothing to implement so far +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/AbstractUrlBasedDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/AbstractUrlBasedDeliveryDataImpl.java new file mode 100644 index 0000000000..81e4d6a4ff --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/AbstractUrlBasedDeliveryDataImpl.java @@ -0,0 +1,25 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.UrlBasedDeliveryData; + +import java.util.Objects; + +import javax.annotation.Nonnull; + +public abstract class AbstractUrlBasedDeliveryDataImpl extends AbstractDeliveryDataImpl + implements UrlBasedDeliveryData { + + @Nonnull + private final String url; + + protected AbstractUrlBasedDeliveryDataImpl(@Nonnull final String url) { + this.url = Objects.requireNonNull(url); + } + + @Nonnull + @Override + public String url() { + return url; + } + +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java new file mode 100644 index 0000000000..767ce3a044 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java @@ -0,0 +1,34 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.DASHManifestDeliveryData; + +import java.util.Objects; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +/** + * Note we build the manifests for YT ourself because the provided ones (according to TiA4f8R) + * + */ +public class SimpleDASHManifestDeliveryDataImpl extends AbstractDeliveryDataImpl + implements DASHManifestDeliveryData { + @Nonnull + private final Supplier dashManifestBuilder; + + public SimpleDASHManifestDeliveryDataImpl(@Nonnull final Supplier dashManifestBuilder) { + this.dashManifestBuilder = Objects.requireNonNull(dashManifestBuilder); + } + + @Override + public String getManifestAsString() { + return dashManifestBuilder.get(); + } +} \ No newline at end of file diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHUrlDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHUrlDeliveryDataImpl.java new file mode 100644 index 0000000000..263097915e --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHUrlDeliveryDataImpl.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.DASHUrlDeliveryData; + +import javax.annotation.Nonnull; + +public class SimpleDASHUrlDeliveryDataImpl extends AbstractUrlBasedDeliveryDataImpl + implements DASHUrlDeliveryData { + public SimpleDASHUrlDeliveryDataImpl(@Nonnull final String url) { + super(url); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleHLSDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleHLSDeliveryDataImpl.java new file mode 100644 index 0000000000..9496825bfc --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleHLSDeliveryDataImpl.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.HLSDeliveryData; + +import javax.annotation.Nonnull; + +public class SimpleHLSDeliveryDataImpl extends AbstractUrlBasedDeliveryDataImpl + implements HLSDeliveryData { + public SimpleHLSDeliveryDataImpl(@Nonnull final String url) { + super(url); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleProgressiveHTTPDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleProgressiveHTTPDeliveryDataImpl.java new file mode 100644 index 0000000000..94b09b4ab7 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleProgressiveHTTPDeliveryDataImpl.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.ProgressiveHTTPDeliveryData; + +import javax.annotation.Nonnull; + +public class SimpleProgressiveHTTPDeliveryDataImpl extends AbstractUrlBasedDeliveryDataImpl + implements ProgressiveHTTPDeliveryData { + public SimpleProgressiveHTTPDeliveryDataImpl(@Nonnull final String url) { + super(url); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleTorrentDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleTorrentDeliveryDataImpl.java new file mode 100644 index 0000000000..f3a3b36dbf --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleTorrentDeliveryDataImpl.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.TorrentDeliveryData; + +import javax.annotation.Nonnull; + +public class SimpleTorrentDeliveryDataImpl extends AbstractUrlBasedDeliveryDataImpl + implements TorrentDeliveryData { + public SimpleTorrentDeliveryDataImpl(@Nonnull final String url) { + super(url); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java new file mode 100644 index 0000000000..f765080077 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java @@ -0,0 +1,51 @@ +package org.schabi.newpipe.extractor.streamdata.format; + +import java.util.Objects; + +public abstract class AbstractMediaFormat { + private final int id; + private final String name; + private final String suffix; + private final String mimeType; + + protected AbstractMediaFormat( + final int id, + final String name, + final String suffix, + final String mimeType + ) { + this.id = id; + this.name = name; + this.suffix = suffix; + this.mimeType = mimeType; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getSuffix() { + return suffix; + } + + public String getMimeType() { + return mimeType; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof AbstractMediaFormat)) return false; + final AbstractMediaFormat that = (AbstractMediaFormat) o; + return getId() == that.getId() && Objects.equals(getName(), that.getName()) && Objects.equals(getSuffix(), that.getSuffix()) && Objects.equals(getMimeType(), that.getMimeType()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getName(), getSuffix(), getMimeType()); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java new file mode 100644 index 0000000000..a2fb322d24 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java @@ -0,0 +1,14 @@ +package org.schabi.newpipe.extractor.streamdata.format; + +public class AudioMediaFormat extends AbstractMediaFormat { + public AudioMediaFormat( + final int id, + final String name, + final String suffix, + final String mimeType + ) { + super(id, name, suffix, mimeType); + } + + +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/SubtitleMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/SubtitleMediaFormat.java new file mode 100644 index 0000000000..c860eb31dd --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/SubtitleMediaFormat.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.extractor.streamdata.format; + +public class SubtitleMediaFormat extends AbstractMediaFormat { + public SubtitleMediaFormat( + final int id, + final String name, + final String suffix, + final String mimeType + ) { + super(id, name, suffix, mimeType); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/VideoAudioMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/VideoAudioMediaFormat.java new file mode 100644 index 0000000000..4250fd39cb --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/VideoAudioMediaFormat.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.extractor.streamdata.format; + +public class VideoAudioMediaFormat extends AbstractMediaFormat { + public VideoAudioMediaFormat( + final int id, + final String name, + final String suffix, + final String mimeType + ) { + super(id, name, suffix, mimeType); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/AudioFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/AudioFormatRegistry.java new file mode 100644 index 0000000000..55e1d67c45 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/AudioFormatRegistry.java @@ -0,0 +1,23 @@ +package org.schabi.newpipe.extractor.streamdata.format.registry; + +import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; + +public class AudioFormatRegistry extends MediaFormatRegistry { + + public static final AudioMediaFormat M4A = + new AudioMediaFormat(0x100, "m4a", "m4a", "audio/mp4"); + public static final AudioMediaFormat WEBMA = + new AudioMediaFormat(0x200, "WebM", "webm", "audio/webm"); + public static final AudioMediaFormat MP3 = + new AudioMediaFormat(0x300, "MP3", "mp3", "audio/mpeg"); + public static final AudioMediaFormat OPUS = + new AudioMediaFormat(0x400, "opus", "opus", "audio/opus"); + public static final AudioMediaFormat OGG = + new AudioMediaFormat(0x500, "ogg", "ogg", "audio/ogg"); + public static final AudioMediaFormat WEBMA_OPUS = + new AudioMediaFormat(0x200, "WebM Opus", "webm", "audio/webm"); + + public AudioFormatRegistry() { + super(new AudioMediaFormat[]{M4A, WEBMA, MP3, OPUS, OGG, WEBMA_OPUS}); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java new file mode 100644 index 0000000000..2a676b39fe --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java @@ -0,0 +1,79 @@ +package org.schabi.newpipe.extractor.streamdata.format.registry; + +import org.schabi.newpipe.extractor.streamdata.format.AbstractMediaFormat; + +import java.util.Arrays; +import java.util.function.Function; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class MediaFormatRegistry { + + protected final F[] values; + + protected MediaFormatRegistry(final F[] values) { + this.values = values; + } + + public F[] values() { + return values; + } + + public T getById(final int id, + final Function field, + final T orElse) { + return Arrays.stream(values()) + .filter(mediaFormat -> mediaFormat.getId() == id) + .map(field) + .findFirst() + .orElse(orElse); + } + + /** + * Return the friendly name of the media format with the supplied id + * + * @param id the id of the media format. Currently an arbitrary, NewPipe-specific number. + * @return the friendly name of the MediaFormat associated with this ids, + * or an empty String if none match it. + */ + @Nonnull + public String getNameById(final int id) { + return getById(id, AbstractMediaFormat::getName, ""); + } + + /** + * Return the MIME type of the media format with the supplied id + * + * @param id the id of the media format. Currently an arbitrary, NewPipe-specific number. + * @return the MIME type of the MediaFormat associated with this ids, + * or an empty String if none match it. + */ + @Nullable + public String getMimeById(final int id) { + return getById(id, AbstractMediaFormat::getMimeType, null); + } + + /** + * Return the MediaFormat with the supplied mime type + * + * @return MediaFormat associated with this mime type, + * or null if none match it. + */ + @Nullable + public F getFromMimeType(final String mimeType) { + return Arrays.stream(values()) + .filter(mediaFormat -> mediaFormat.getMimeType().equals(mimeType)) + .findFirst() + .orElse(null); + } + + @Nullable + public F getFromSuffix(final String suffix) { + return Arrays.stream(values()) + .filter(mediaFormat -> mediaFormat.getSuffix().equals(suffix)) + .findFirst() + .orElse(null); + } + +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/SubtitleFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/SubtitleFormatRegistry.java new file mode 100644 index 0000000000..8de47e3b77 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/SubtitleFormatRegistry.java @@ -0,0 +1,25 @@ +package org.schabi.newpipe.extractor.streamdata.format.registry; + +import org.schabi.newpipe.extractor.streamdata.format.SubtitleMediaFormat; + +public class SubtitleFormatRegistry extends MediaFormatRegistry { + + public static final SubtitleMediaFormat VTT = + new SubtitleMediaFormat(0x1000, "WebVTT", "vtt", "text/vtt"); + public static final SubtitleMediaFormat TTML = + new SubtitleMediaFormat(0x2000, "Timed Text Markup Language", "ttml", + "application/ttml+xml"); + public static final SubtitleMediaFormat TRANSCRIPT1 = + new SubtitleMediaFormat(0x3000, "TranScript v1", "srv1", "text/xml"); + public static final SubtitleMediaFormat TRANSCRIPT2 = + new SubtitleMediaFormat(0x4000, "TranScript v2", "srv2", "text/xml"); + public static final SubtitleMediaFormat TRANSCRIPT3 = + new SubtitleMediaFormat(0x5000, "TranScript v3", "srv3", "text/xml"); + public static final SubtitleMediaFormat SRT = + new SubtitleMediaFormat(0x6000, "SubRip file format", "srt", "text/srt"); + + + public SubtitleFormatRegistry() { + super(new SubtitleMediaFormat[]{VTT, TTML, TRANSCRIPT1, TRANSCRIPT2, TRANSCRIPT3, SRT}); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/VideoAudioFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/VideoAudioFormatRegistry.java new file mode 100644 index 0000000000..aeccfd1696 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/VideoAudioFormatRegistry.java @@ -0,0 +1,17 @@ +package org.schabi.newpipe.extractor.streamdata.format.registry; + +import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; + +public class VideoAudioFormatRegistry extends MediaFormatRegistry { + + public static final VideoAudioMediaFormat MPEG_4 = + new VideoAudioMediaFormat(0x0, "MPEG-4", "mp4", "video/mp4"); + public static final VideoAudioMediaFormat V3GPP = + new VideoAudioMediaFormat(0x10, "3GPP", "3gp", "video/3gpp"); + public static final VideoAudioMediaFormat WEBM = + new VideoAudioMediaFormat(0x20, "WebM", "webm", "video/webm"); + + public VideoAudioFormatRegistry() { + super(new VideoAudioMediaFormat[]{MPEG_4, V3GPP, WEBM}); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java new file mode 100644 index 0000000000..092efda509 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java @@ -0,0 +1,40 @@ +package org.schabi.newpipe.extractor.streamdata.stream; + +import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; + +import java.util.Objects; + +import javax.annotation.Nullable; + +/** + * Represents a audio (only) stream. + */ +public interface AudioStream extends Stream { + int UNKNOWN_BITRATE = -1; + + // TODO: Check if this can be non-null + @Nullable + default AudioMediaFormat audioMediaFormat() { + return null; + } + + /** + * Get the average bitrate of the stream. + * + * @return the average bitrate or -1 if unknown + */ + default int averageBitrate() { + return UNKNOWN_BITRATE; + } + + @Override + default boolean equalsStream(@Nullable final Stream other) { + if (!(other instanceof AudioStream)) { + return false; + } + + final AudioStream otherAudioStream = (AudioStream) other; + return Objects.equals(audioMediaFormat(), otherAudioStream.audioMediaFormat()) + && averageBitrate() == otherAudioStream.averageBitrate(); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java new file mode 100644 index 0000000000..8af60b482a --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java @@ -0,0 +1,15 @@ +package org.schabi.newpipe.extractor.streamdata.stream; + +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public interface Stream { + @Nonnull + DeliveryData deliveryData(); + + + // TODO: May also have to check deliverydata + boolean equalsStream(@Nullable final Stream other); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java new file mode 100644 index 0000000000..f9ee57e631 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java @@ -0,0 +1,60 @@ +package org.schabi.newpipe.extractor.streamdata.stream; + +import org.schabi.newpipe.extractor.streamdata.format.SubtitleMediaFormat; + +import java.util.Locale; +import java.util.Objects; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents a subtitle (only) stream. + */ +public interface SubtitleStream extends Stream { + @Nonnull + SubtitleMediaFormat subtitleMediaFormat(); + + /** + * 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 + */ + default boolean autoGenerated() { + return false; + } + + /** + * Get the language code of the subtitles. + * + * @return the language code of the subtitles + */ + @Nonnull + String languageCode(); + + /** + * Get the {@link Locale locale} of the subtitles. + * + *

+ * Note: The locale is directly derived from the language-code. + *

+ * + * @return the {@link Locale locale} of the subtitles + */ + Locale locale(); + + @Override + default boolean equalsStream(@Nullable final Stream other) { + if (!(other instanceof SubtitleStream)) { + return false; + } + + final SubtitleStream otherSubtitleStream = (SubtitleStream) other; + return Objects.equals(subtitleMediaFormat(), otherSubtitleStream.subtitleMediaFormat()) + && autoGenerated() == otherSubtitleStream.autoGenerated() + && Objects.equals(languageCode(), otherSubtitleStream.languageCode()); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java new file mode 100644 index 0000000000..b6132ed464 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java @@ -0,0 +1,17 @@ +package org.schabi.newpipe.extractor.streamdata.stream; + +import javax.annotation.Nullable; + +/** + * Represents a combined video+audio stream. + */ +public interface VideoAudioStream extends VideoStream, AudioStream { + @Override + default boolean equalsStream(@Nullable final Stream other) { + if (!(other instanceof VideoAudioStream)) { + return false; + } + + return VideoStream.super.equalsStream(other) && AudioStream.super.equalsStream(other); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java new file mode 100644 index 0000000000..63b298bfcc --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.extractor.streamdata.stream; + +import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; + +import java.util.Objects; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents a video (only) stream. + */ +public interface VideoStream extends Stream { + String UNKNOWN_RESOLUTION = ""; + + // TODO: Check if this can be non-null + @Nullable + default VideoAudioMediaFormat videoMediaFormat() { + return null; + } + + // TODO: This should be a separate entity (containing e.g. height x width + fps) + @Nonnull + default String resolution() { + return UNKNOWN_RESOLUTION; + } + + @Override + default boolean equalsStream(@Nullable final Stream other) { + if (!(other instanceof VideoStream)) { + return false; + } + + final VideoStream otherVideoStream = (VideoStream) other; + return Objects.equals(videoMediaFormat(), otherVideoStream.videoMediaFormat()) + && Objects.equals(resolution(), otherVideoStream.resolution()); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/AbstractStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/AbstractStreamImpl.java new file mode 100644 index 0000000000..2a4abe0a52 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/AbstractStreamImpl.java @@ -0,0 +1,23 @@ +package org.schabi.newpipe.extractor.streamdata.stream.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.stream.Stream; + +import java.util.Objects; + +import javax.annotation.Nonnull; + +public abstract class AbstractStreamImpl implements Stream { + @Nonnull + private final DeliveryData deliveryData; + + protected AbstractStreamImpl(@Nonnull final DeliveryData deliveryData) { + this.deliveryData = Objects.requireNonNull(deliveryData); + } + + @Nonnull + @Override + public DeliveryData deliveryData() { + return deliveryData; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java new file mode 100644 index 0000000000..c360e3cb03 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java @@ -0,0 +1,53 @@ +package org.schabi.newpipe.extractor.streamdata.stream.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class SimpleAudioStreamImpl extends AbstractStreamImpl implements AudioStream { + @Nullable + private final AudioMediaFormat audioMediaFormat; + private final int averageBitrate; + + public SimpleAudioStreamImpl( + @Nonnull final DeliveryData deliveryData, + @Nullable final AudioMediaFormat audioMediaFormat, + final int averageBitrate + ) { + super(deliveryData); + this.audioMediaFormat = audioMediaFormat; + this.averageBitrate = averageBitrate; + } + + public SimpleAudioStreamImpl( + @Nonnull final DeliveryData deliveryData, + @Nullable final AudioMediaFormat audioMediaFormat + ) { + this(deliveryData, audioMediaFormat, UNKNOWN_BITRATE); + } + + public SimpleAudioStreamImpl( + @Nonnull final DeliveryData deliveryData, + final int averageBitrate + ) { + this(deliveryData, null, averageBitrate); + } + + public SimpleAudioStreamImpl(@Nonnull final DeliveryData deliveryData) { + this(deliveryData, null); + } + + @Nullable + @Override + public AudioMediaFormat audioMediaFormat() { + return audioMediaFormat; + } + + @Override + public int averageBitrate() { + return averageBitrate; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleSubtitleStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleSubtitleStreamImpl.java new file mode 100644 index 0000000000..2f41df6bf5 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleSubtitleStreamImpl.java @@ -0,0 +1,71 @@ +package org.schabi.newpipe.extractor.streamdata.stream.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.SubtitleMediaFormat; +import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream; + +import java.util.Locale; +import java.util.Objects; + +import javax.annotation.Nonnull; + +public class SimpleSubtitleStreamImpl extends AbstractStreamImpl implements SubtitleStream { + @Nonnull + private final SubtitleMediaFormat subtitleMediaFormat; + private final boolean autogenerated; + @Nonnull + private final String languageCode; + private final Locale locale; + + public SimpleSubtitleStreamImpl( + @Nonnull final DeliveryData deliveryData, + final SubtitleMediaFormat subtitleMediaFormat, + final boolean autogenerated, + @Nonnull final String languageCode + ) { + super(deliveryData); + this.subtitleMediaFormat = Objects.requireNonNull(subtitleMediaFormat); + this.autogenerated = autogenerated; + this.languageCode = Objects.requireNonNull(languageCode); + /* + * 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) { + 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; + } + } + + @Nonnull + @Override + public SubtitleMediaFormat subtitleMediaFormat() { + return subtitleMediaFormat; + } + + @Override + public boolean autoGenerated() { + return autogenerated; + } + + @Nonnull + @Override + public String languageCode() { + return languageCode; + } + + @Override + public Locale locale() { + return locale; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java new file mode 100644 index 0000000000..5986c1a0b1 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java @@ -0,0 +1,74 @@ +package org.schabi.newpipe.extractor.streamdata.stream.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; +import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; +import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; + +import java.util.Objects; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class SimpleVideoAudioStreamImpl extends AbstractStreamImpl implements VideoAudioStream { + @Nullable + private final AudioMediaFormat audioMediaFormat; + private final int averageBitrate; + + @Nullable + private final VideoAudioMediaFormat videoAudioMediaFormat; + @Nonnull + private final String resolution; + + public SimpleVideoAudioStreamImpl( + @Nonnull final DeliveryData deliveryData, + @Nullable final AudioMediaFormat audioMediaFormat, + final int averageBitrate, + @Nullable final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final String resolution + ) { + super(deliveryData); + this.audioMediaFormat = audioMediaFormat; + this.averageBitrate = averageBitrate; + this.videoAudioMediaFormat = videoAudioMediaFormat; + this.resolution = Objects.requireNonNull(resolution); + } + + public SimpleVideoAudioStreamImpl( + @Nonnull final DeliveryData deliveryData, + @Nullable final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final String resolution + ) { + this(deliveryData, null, UNKNOWN_BITRATE, videoAudioMediaFormat, resolution); + } + + public SimpleVideoAudioStreamImpl( + @Nonnull final DeliveryData deliveryData, + @Nullable final VideoAudioMediaFormat videoAudioMediaFormat + ) { + this(deliveryData, videoAudioMediaFormat, UNKNOWN_RESOLUTION); + } + + @Nullable + @Override + public AudioMediaFormat audioMediaFormat() { + return audioMediaFormat; + } + + @Override + public int averageBitrate() { + return averageBitrate; + } + + @Nullable + @Override + public VideoAudioMediaFormat videoMediaFormat() { + return videoAudioMediaFormat; + } + + @Nonnull + @Override + public String resolution() { + return resolution; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java new file mode 100644 index 0000000000..247099216f --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java @@ -0,0 +1,50 @@ +package org.schabi.newpipe.extractor.streamdata.stream.simpleimpl; + +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; +import org.schabi.newpipe.extractor.streamdata.stream.VideoStream; + +import java.util.Objects; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class SimpleVideoStreamImpl extends AbstractStreamImpl implements VideoStream { + @Nullable + private final VideoAudioMediaFormat videoAudioMediaFormat; + @Nonnull + private final String resolution; + + public SimpleVideoStreamImpl( + @Nonnull final DeliveryData deliveryData, + @Nullable final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final String resolution + ) { + super(deliveryData); + this.videoAudioMediaFormat = videoAudioMediaFormat; + this.resolution = Objects.requireNonNull(resolution); + } + + public SimpleVideoStreamImpl( + @Nonnull final DeliveryData deliveryData, + @Nonnull final String resolution + ) { + this(deliveryData, null, resolution); + } + + public SimpleVideoStreamImpl(@Nonnull final DeliveryData deliveryData) { + this(deliveryData, null, UNKNOWN_RESOLUTION); + } + + @Nullable + @Override + public VideoAudioMediaFormat videoMediaFormat() { + return videoAudioMediaFormat; + } + + @Nonnull + @Override + public String resolution() { + return resolution; + } +} From c129d7ba56754c060bf57681a04c86d9a0953485 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 4 Jun 2022 23:31:06 +0200 Subject: [PATCH 02/43] Removed old streaming API --- .../schabi/newpipe/extractor/MediaFormat.java | 168 ------ .../newpipe/extractor/stream/AudioStream.java | 383 -------------- .../extractor/stream/DeliveryMethod.java | 53 -- .../newpipe/extractor/stream/Stream.java | 207 -------- .../extractor/stream/StreamExtractor.java | 79 +-- .../newpipe/extractor/stream/StreamInfo.java | 85 +-- .../newpipe/extractor/stream/StreamType.java | 8 + .../extractor/stream/SubtitlesStream.java | 328 ------------ .../newpipe/extractor/stream/VideoStream.java | 485 ------------------ 9 files changed, 79 insertions(+), 1717 deletions(-) delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java deleted file mode 100644 index 4423068488..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.schabi.newpipe.extractor; - -/* - * Created by Adam Howard on 08/11/15. - * - * Copyright (c) Christian Schabesberger - * and Adam Howard 2015 - * - * MediaFormat.java is part of NewPipe. - * - * NewPipe 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, - * 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. If not, see . - */ - -import java.util.Arrays; -import java.util.function.Function; - -/** - * Static data about various media formats support by NewPipe, eg mime type, extension - */ - -@SuppressWarnings("MethodParamPad") // we want the media format table below to be aligned -public enum MediaFormat { - // @formatter:off - //video and audio combined formats - // id name suffix mimeType - MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"), - v3GPP (0x10, "3GPP", "3gp", "video/3gpp"), - WEBM (0x20, "WebM", "webm", "video/webm"), - // audio formats - M4A (0x100, "m4a", "m4a", "audio/mp4"), - WEBMA (0x200, "WebM", "webm", "audio/webm"), - MP3 (0x300, "MP3", "mp3", "audio/mpeg"), - OPUS (0x400, "opus", "opus", "audio/opus"), - OGG (0x500, "ogg", "ogg", "audio/ogg"), - WEBMA_OPUS(0x200, "WebM Opus", "webm", "audio/webm"), - // subtitles formats - VTT (0x1000, "WebVTT", "vtt", "text/vtt"), - TTML (0x2000, "Timed Text Markup Language", "ttml", "application/ttml+xml"), - TRANSCRIPT1(0x3000, "TranScript v1", "srv1", "text/xml"), - TRANSCRIPT2(0x4000, "TranScript v2", "srv2", "text/xml"), - TRANSCRIPT3(0x5000, "TranScript v3", "srv3", "text/xml"), - SRT (0x6000, "SubRip file format", "srt", "text/srt"); - // @formatter:on - - public final int id; - public final String name; - public final String suffix; - public final String mimeType; - - MediaFormat(final int id, final String name, final String suffix, final String mimeType) { - this.id = id; - this.name = name; - this.suffix = suffix; - this.mimeType = mimeType; - } - - private static T getById(final int id, - final Function field, - final T orElse) { - return Arrays.stream(MediaFormat.values()) - .filter(mediaFormat -> mediaFormat.id == id) - .map(field) - .findFirst() - .orElse(orElse); - } - - /** - * Return the friendly name of the media format with the supplied id - * - * @param id the id of the media format. Currently an arbitrary, NewPipe-specific number. - * @return the friendly name of the MediaFormat associated with this ids, - * or an empty String if none match it. - */ - public static String getNameById(final int id) { - return getById(id, MediaFormat::getName, ""); - } - - /** - * Return the file extension of the media format with the supplied id - * - * @param id the id of the media format. Currently an arbitrary, NewPipe-specific number. - * @return the file extension of the MediaFormat associated with this ids, - * or an empty String if none match it. - */ - public static String getSuffixById(final int id) { - return getById(id, MediaFormat::getSuffix, ""); - } - - /** - * Return the MIME type of the media format with the supplied id - * - * @param id the id of the media format. Currently an arbitrary, NewPipe-specific number. - * @return the MIME type of the MediaFormat associated with this ids, - * or an empty String if none match it. - */ - public static String getMimeById(final int id) { - return getById(id, MediaFormat::getMimeType, null); - } - - /** - * Return the MediaFormat with the supplied mime type - * - * @return MediaFormat associated with this mime type, - * or null if none match it. - */ - public static MediaFormat getFromMimeType(final String mimeType) { - return Arrays.stream(MediaFormat.values()) - .filter(mediaFormat -> mediaFormat.mimeType.equals(mimeType)) - .findFirst() - .orElse(null); - } - - /** - * Get the media format by its id. - * - * @param id the id - * @return the id of the media format or null. - */ - public static MediaFormat getFormatById(final int id) { - return getById(id, mediaFormat -> mediaFormat, null); - } - - public static MediaFormat getFromSuffix(final String suffix) { - return Arrays.stream(MediaFormat.values()) - .filter(mediaFormat -> mediaFormat.suffix.equals(suffix)) - .findFirst() - .orElse(null); - } - - /** - * Get the name of the format - * - * @return the name of the format - */ - public String getName() { - return name; - } - - /** - * Get the filename extension - * - * @return the filename extension - */ - public String getSuffix() { - return suffix; - } - - /** - * Get the mime type - * - * @return the mime type - */ - public String getMimeType() { - return mimeType; - } - -} 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 deleted file mode 100644 index 59cf9a3231..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java +++ /dev/null @@ -1,383 +0,0 @@ -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 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 . - */ - -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.services.youtube.ItagItem; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public final class AudioStream extends Stream { - public static final int UNKNOWN_BITRATE = -1; - - private final int averageBitrate; - - // Fields for DASH - private int itag = ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE; - private int bitrate; - private int initStart; - private int initEnd; - private int indexStart; - private int indexEnd; - private String quality; - private String codec; - @Nullable - private ItagItem itagItem; - - /** - * Class to build {@link AudioStream} 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 manifestUrl; - 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 AudioStream}. - * - *

- * It must not be 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 AudioStream}, which must not be 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 not be 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}, or {@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 not be null. - *

- * - *

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

- * - * @param deliveryMethod the {@link DeliveryMethod} of the {@link AudioStream}, which must - * not be null - * @return this {@link Builder} instance - */ - public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { - this.deliveryMethod = deliveryMethod; - return this; - } - - /** - * Sets the URL of the manifest this stream comes from (if applicable, otherwise null). - * - * @param manifestUrl the URL of the manifest this stream comes from or {@code null} - * @return this {@link Builder} instance - */ - public Builder setManifestUrl(@Nullable final String manifestUrl) { - this.manifestUrl = manifestUrl; - 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 have been 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, - manifestUrl, itagItem); - } - } - - - /** - * Create a new audio stream. - * - * @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 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) - */ - @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 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(); - } - this.averageBitrate = averageBitrate; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equalStats(final Stream cmp) { - return super.equalStats(cmp) && cmp instanceof AudioStream - && averageBitrate == ((AudioStream) cmp).averageBitrate; - } - - /** - * 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; - } -} 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 deleted file mode 100644 index ed98935725..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java +++ /dev/null @@ -1,53 +0,0 @@ -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 { - - /** - * Used for {@link Stream}s served using the progressive HTTP streaming method. - */ - PROGRESSIVE_HTTP, - - /** - * Used for {@link Stream}s served using the DASH (Dynamic Adaptive Streaming over HTTP) - * adaptive streaming method. - * - * @see the - * Dynamic Adaptive Streaming over HTTP Wikipedia page and - * DASH Industry Forum's website for more information about the DASH delivery method - */ - DASH, - - /** - * Used for {@link Stream}s served using the HLS (HTTP Live Streaming) adaptive streaming - * method. - * - * @see the HTTP Live Streaming - * page and Apple's developers website page - * about HTTP Live Streaming for more information about the HLS delivery method - */ - HLS, - - /** - * Used for {@link Stream}s served using the SmoothStreaming adaptive streaming method. - * - * @see Wikipedia's page about adaptive bitrate streaming, - * section Microsoft Smooth Streaming (MSS) for more information about the - * SmoothStreaming delivery method - */ - SS, - - /** - * Used for {@link Stream}s served via a torrent file. - * - * @see Wikipedia's BitTorrent's page, - * Wikipedia's page about torrent files - * and Bitorrent's website for more information - * about the BitTorrent protocol - */ - 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 deleted file mode 100644 index 04d2b3facb..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java +++ /dev/null @@ -1,207 +0,0 @@ -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 static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; - -/** - * Abstract class which represents streams in the extractor. - */ -public abstract class Stream implements Serializable { - public static final int FORMAT_ID_UNKNOWN = -1; - public static final String ID_UNKNOWN = " "; - - /** - * 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). - * - *

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

- */ - 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 manifestUrl; - - /** - * Instantiates a new {@code Stream} object. - * - * @param id the identifier 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 manifestUrl the URL of the manifest this stream comes from (if applicable, - * otherwise null) - */ - public Stream(final String id, - final String content, - final boolean isUrl, - @Nullable final MediaFormat format, - final DeliveryMethod deliveryMethod, - @Nullable final String manifestUrl) { - this.id = id; - this.content = content; - this.isUrl = isUrl; - this.mediaFormat = format; - this.deliveryMethod = deliveryMethod; - this.manifestUrl = manifestUrl; - } - - /** - * Checks if the list already contains a stream with the same statistics. - * - * @param stream the stream to be compared against the streams in the stream list - * @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, - final List streamList) { - if (isNullOrEmpty(streamList)) { - return false; - } - for (final Stream cmpStream : streamList) { - if (stream.equalStats(cmpStream)) { - return true; - } - } - return false; - } - - /** - * Reveals whether two streams have the same statistics ({@link MediaFormat media format} and - * {@link DeliveryMethod delivery method}). - * - *

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

- * - *

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

- * - * @param other 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 other) { - if (other == null || mediaFormat == null || other.mediaFormat == null) { - return false; - } - return mediaFormat.id == other.mediaFormat.id && deliveryMethod == other.deliveryMethod - && isUrl == other.isUrl; - } - - /** - * Gets the identifier of this stream, e.g. the itag for YouTube. - * - *

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

- * - * @return the identifier (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} otherwise. - * - * @return the URL if the content is a URL, {@code null} otherwise - * @deprecated Use {@link #getContent()} instead. - */ - @Deprecated - @Nullable - public String getUrl() { - return isUrl ? content : null; - } - - /** - * Gets the content or URL. - * - * @return the content or URL - */ - public String getContent() { - return content; - } - - /** - * Returns whether the content is a URL or not. - * - * @return {@code true} if the content of this stream is a URL, {@code false} if it's 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, which can be unknown. - * - * @return the format ID or {@link #FORMAT_ID_UNKNOWN} - */ - public int getFormatId() { - if (mediaFormat != null) { - return mediaFormat.id; - } - return FORMAT_ID_UNKNOWN; - } - - /** - * Gets the {@link DeliveryMethod}. - * - * @return the delivery method - */ - @Nonnull - public DeliveryMethod getDeliveryMethod() { - return deliveryMethod; - } - - /** - * Gets the URL of the manifest this stream comes from (if applicable, otherwise null). - * - * @return the URL of the manifest this stream comes from or {@code null} - */ - @Nullable - public String getManifestUrl() { - return manifestUrl; - } - - /** - * Gets the {@link ItagItem} of a stream. - * - *

- * If the stream is not from YouTube, this value will always be null. - *

- * - * @return the {@link ItagItem} of the stream or {@code null} - */ - @Nullable - public abstract ItagItem getItagItem(); -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java index ab922b1c98..12dc554833 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java @@ -20,11 +20,10 @@ * along with NewPipe. If not, see . */ +import org.schabi.newpipe.extractor.Extractor; import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.InfoItemsCollector; import org.schabi.newpipe.extractor.InfoItemExtractor; -import org.schabi.newpipe.extractor.Extractor; -import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.InfoItemsCollector; import org.schabi.newpipe.extractor.MetaInfo; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelExtractor; @@ -32,16 +31,20 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.localization.DateWrapper; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoStream; import org.schabi.newpipe.extractor.utils.Parser; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Locale; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * Scrapes information from a video/audio streaming service (eg, YouTube). */ @@ -50,7 +53,7 @@ public abstract class StreamExtractor extends Extractor { public static final int NO_AGE_LIMIT = 0; public static final long UNKNOWN_SUBSCRIBER_COUNT = -1; - public StreamExtractor(final StreamingService service, final LinkHandler linkHandler) { + protected StreamExtractor(final StreamingService service, final LinkHandler linkHandler) { super(service, linkHandler); } @@ -254,10 +257,20 @@ public String getSubChannelAvatarUrl() throws ParsingException { } /** - * Get the dash mpd url. If you don't know what a dash MPD is you can read about it + * Get the dash mpd url. + * + *

+ * If you don't know how DASH works take a look at + * + * here. + *

+ * + *

+ * If you don't know what a dash MPD is you can read about it * here. + *

* - * @return the url as a string or an empty string or an empty string if not available + * @return the url as a string or an empty string if not available * @throws ParsingException if an error occurs while reading */ @Nonnull @@ -266,15 +279,18 @@ public String getDashMpdUrl() throws ParsingException { } /** - * I am not sure if this is in use, and how this is used. However the frontend is missing - * support for HLS streams. Prove me if I am wrong. Please open an - * issue, - * or fix this description if you know whats up with this. + * Get the HLS master playlist url. + * + *

+ * If you don't know how HLS works take a look at + * + * here. + *

* * @return The Url to the hls stream or an empty string if not available. */ @Nonnull - public String getHlsUrl() throws ParsingException { + public String getHlsMasterPlaylistUrl() throws ParsingException { return ""; } @@ -286,10 +302,12 @@ public String getHlsUrl() throws ParsingException { * * @return a list of audio only streams in the format of AudioStream */ - public abstract List getAudioStreams() throws IOException, ExtractionException; + public List getAudioStreams() throws IOException, ExtractionException { + return Collections.emptyList(); + } /** - * This should return a list of available {@link VideoStream}s. + * This should return a list of available {@link VideoAudioStream}s. * Be aware this is the list of video streams which do contain an audio stream. * You can also return null or an empty list, however be aware that if you don't return anything * in getAudioStreams(), getVideoOnlyStreams() and getDashMpdUrl() either the Collector will @@ -297,7 +315,9 @@ public String getHlsUrl() throws ParsingException { * * @return a list of combined video and streams in the format of AudioStream */ - public abstract List getVideoStreams() throws IOException, ExtractionException; + public List getVideoStreams() throws IOException, ExtractionException { + return Collections.emptyList(); + } /** * This should return a list of available {@link VideoStream}s. @@ -308,29 +328,18 @@ public String getHlsUrl() throws ParsingException { * * @return a list of video and streams in the format of AudioStream */ - public abstract List getVideoOnlyStreams() throws IOException, ExtractionException; - - /** - * This will return a list of available {@link SubtitlesStream}s. - * If no subtitles are available an empty list can be returned. - * - * @return a list of available subtitles or an empty list - */ - @Nonnull - public List getSubtitlesDefault() throws IOException, ExtractionException { + public List getVideoOnlyStreams() throws IOException, ExtractionException { return Collections.emptyList(); } /** - * This will return a list of available {@link SubtitlesStream}s given by a specific type. - * If no subtitles in that specific format are available an empty list can be returned. + * This will return a list of available {@link SubtitleStream}s. + * If no subtitles are available an empty list can be returned. * - * @param format the media format by which the subtitles should be filtered * @return a list of available subtitles or an empty list */ @Nonnull - public List getSubtitles(final MediaFormat format) - throws IOException, ExtractionException { + public List getSubtitles() throws IOException, ExtractionException { return Collections.emptyList(); } @@ -339,8 +348,14 @@ public List getSubtitles(final MediaFormat format) * * @return the type of the stream */ + @Deprecated // TODO - kill public abstract StreamType getStreamType() throws ParsingException; + // TODO - Implement + public boolean isLive() { + return false; + } + /** * Should return a list of streams related to the current handled. Many services show suggested * streams. If you don't like suggested streams you should implement them anyway since they can diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index 8d3f2c5224..63546b98a6 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.extractor.stream; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.MetaInfo; @@ -9,6 +11,10 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.localization.DateWrapper; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoStream; import org.schabi.newpipe.extractor.utils.ExtractorHelper; import java.io.IOException; @@ -16,11 +22,10 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; import javax.annotation.Nonnull; -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; - /* * Created by Christian Schabesberger on 26.08.15. * @@ -136,18 +141,6 @@ private static void extractStreams(final StreamInfo streamInfo, // At least one type of stream has to be available, otherwise an exception will be thrown // directly into the frontend. - try { - streamInfo.setDashMpdUrl(extractor.getDashMpdUrl()); - } catch (final Exception e) { - streamInfo.addError(new ExtractionException("Couldn't get DASH manifest", e)); - } - - try { - streamInfo.setHlsUrl(extractor.getHlsUrl()); - } catch (final Exception e) { - streamInfo.addError(new ExtractionException("Couldn't get HLS manifest", e)); - } - /* Load and extract audio */ try { streamInfo.setAudioStreams(extractor.getAudioStreams()); @@ -286,7 +279,7 @@ private static void extractOptionalData(final StreamInfo streamInfo, streamInfo.addError(e); } try { - streamInfo.setSubtitles(extractor.getSubtitlesDefault()); + streamInfo.setSubtitles(extractor.getSubtitles()); } catch (final Exception e) { streamInfo.addError(e); } @@ -369,16 +362,14 @@ private static void extractOptionalData(final StreamInfo streamInfo, private String subChannelUrl = ""; private String subChannelAvatarUrl = ""; - private List videoStreams = new ArrayList<>(); + private List videoStreams = new ArrayList<>(); private List audioStreams = new ArrayList<>(); private List videoOnlyStreams = new ArrayList<>(); - private String dashMpdUrl = ""; - private String hlsUrl = ""; private List relatedItems = new ArrayList<>(); private long startPosition = 0; - private List subtitles = new ArrayList<>(); + private List subtitles = new ArrayList<>(); private String host = ""; private StreamExtractor.Privacy privacy; @@ -564,70 +555,41 @@ public void setSubChannelAvatarUrl(final String subChannelAvatarUrl) { this.subChannelAvatarUrl = subChannelAvatarUrl; } - public List getVideoStreams() { + @Nonnull + public List getVideoStreams() { return videoStreams; } - public void setVideoStreams(final List videoStreams) { - this.videoStreams = videoStreams; + public void setVideoStreams(@Nonnull final List videoStreams) { + this.videoStreams = Objects.requireNonNull(videoStreams); } + @Nonnull public List getAudioStreams() { return audioStreams; } - public void setAudioStreams(final List audioStreams) { - this.audioStreams = audioStreams; + public void setAudioStreams(@Nonnull final List audioStreams) { + this.audioStreams = Objects.requireNonNull(audioStreams); } + @Nonnull public List getVideoOnlyStreams() { return videoOnlyStreams; } - public void setVideoOnlyStreams(final List videoOnlyStreams) { - this.videoOnlyStreams = videoOnlyStreams; - } - - public String getDashMpdUrl() { - return dashMpdUrl; - } - - public void setDashMpdUrl(final String dashMpdUrl) { - this.dashMpdUrl = dashMpdUrl; - } - - public String getHlsUrl() { - return hlsUrl; - } - - public void setHlsUrl(final String hlsUrl) { - this.hlsUrl = hlsUrl; + public void setVideoOnlyStreams(@Nonnull final List videoOnlyStreams) { + this.videoOnlyStreams = Objects.requireNonNull(videoOnlyStreams); } public List getRelatedItems() { return relatedItems; } - /** - * @deprecated Use {@link #getRelatedItems()} - */ - @Deprecated - public List getRelatedStreams() { - return getRelatedItems(); - } - public void setRelatedItems(final List relatedItems) { this.relatedItems = relatedItems; } - /** - * @deprecated Use {@link #setRelatedItems(List)} - */ - @Deprecated - public void setRelatedStreams(final List relatedItemsToSet) { - setRelatedItems(relatedItemsToSet); - } - public long getStartPosition() { return startPosition; } @@ -636,12 +598,13 @@ public void setStartPosition(final long startPosition) { this.startPosition = startPosition; } - public List getSubtitles() { + @Nonnull + public List getSubtitles() { return subtitles; } - public void setSubtitles(final List subtitles) { - this.subtitles = subtitles; + public void setSubtitles(@Nonnull final List subtitles) { + this.subtitles = Objects.requireNonNull(subtitles); } public String getHost() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java index 7e668cbd46..98d8f8c06d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.extractor.stream; +// TODO: Remove this useless class /** * An enum representing the stream type of a {@link StreamInfo} extracted by a {@link * StreamExtractor}. @@ -11,6 +12,7 @@ public enum StreamType { * enum constant outside of the extractor as it will never be returned by an {@link * org.schabi.newpipe.extractor.Extractor} and is only used internally. */ + // TODO: useless NONE, /** @@ -18,6 +20,7 @@ public enum StreamType { * provide audio-only {@link AudioStream}s in addition to video or video-only {@link * VideoStream}s. */ + // TODO: Default VIDEO_STREAM, /** @@ -26,6 +29,7 @@ public enum StreamType { * ensure that no video stream is returned in {@link StreamExtractor#getVideoStreams()} and * {@link StreamExtractor#getVideoOnlyStreams()}. */ + // TODO: Can be replaced by checking #getVideoStreams or #getVideoOnlyStreams AUDIO_STREAM, /** @@ -33,6 +37,7 @@ public enum StreamType { * provide audio-only {@link AudioStream}s in addition to video or video-only {@link * VideoStream}s. */ + // TODO: Can be replaced by flag isLive LIVE_STREAM, /** @@ -41,6 +46,7 @@ public enum StreamType { * should ensure that no video stream is returned in {@link StreamExtractor#getVideoStreams()} * and {@link StreamExtractor#getVideoOnlyStreams()}. */ + // TODO: Can be replaced by flag isLive AUDIO_LIVE_STREAM, /** @@ -55,6 +61,7 @@ public enum StreamType { * the case on YouTube, for example. *

*/ + // TODO: Useless (only used internally inside YT) POST_LIVE_STREAM, /** @@ -70,5 +77,6 @@ public enum StreamType { * again later. *

*/ + // TODO: Useless (only used internally inside YT) POST_LIVE_AUDIO_STREAM } 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 deleted file mode 100644 index 778a85c93a..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java +++ /dev/null @@ -1,328 +0,0 @@ -package org.schabi.newpipe.extractor.stream; - -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.services.youtube.ItagItem; - -import java.util.Locale; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public final class SubtitlesStream extends Stream { - private final MediaFormat format; - private final Locale locale; - private final boolean autoGenerated; - private final String code; - - /** - * 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 manifestUrl; - 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 default values. - */ - public Builder() { - } - - /** - * Set the identifier of the {@link SubtitlesStream}. - * - * @param id the identifier of the {@link SubtitlesStream}, which should not be 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 not be null, and should be non empty. - *

- * - * @param content the content of the {@link SubtitlesStream}, which must not be null - * @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}, or {@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 not be null. - *

- * - *

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

- * - * @param deliveryMethod the {@link DeliveryMethod} of the {@link SubtitlesStream}, which - * must not be null - * @return this {@link Builder} instance - */ - public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { - this.deliveryMethod = deliveryMethod; - return this; - } - - /** - * Sets the URL of the manifest this stream comes from (if applicable, otherwise null). - * - * @param manifestUrl the URL of the manifest this stream comes from or {@code null} - * @return this {@link Builder} instance - */ - public Builder setManifestUrl(@Nullable final String manifestUrl) { - this.manifestUrl = manifestUrl; - return this; - } - - /** - * Set the language code of the {@link SubtitlesStream}. - * - *

- * It must not be null and should not be 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 auto-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 have been 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 - : ""); - } - - return new SubtitlesStream(id, content, isUrl, mediaFormat, deliveryMethod, - languageCode, autoGenerated, manifestUrl); - } - } - - /** - * Create a new subtitles stream. - * - * @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 mediaFormat 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 manifestUrl the URL of the manifest this stream comes from (if applicable, - * otherwise null) - */ - @SuppressWarnings("checkstyle:ParameterNumber") - private SubtitlesStream(@Nonnull final String id, - @Nonnull final String content, - final boolean isUrl, - @Nullable final MediaFormat mediaFormat, - @Nonnull final DeliveryMethod deliveryMethod, - @Nonnull final String languageCode, - final boolean autoGenerated, - @Nullable final String manifestUrl) { - super(id, content, isUrl, mediaFormat, deliveryMethod, manifestUrl); - - /* - * 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) { - 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 = mediaFormat; - 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) - && cmp instanceof SubtitlesStream - && code.equals(((SubtitlesStream) cmp).code) - && 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; - } -} 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 deleted file mode 100644 index 14952ebd1a..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java +++ /dev/null @@ -1,485 +0,0 @@ -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 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 . - */ - -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.services.youtube.ItagItem; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -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 = ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE; - private int bitrate; - private int initStart; - private int initEnd; - private int indexStart; - private int indexEnd; - private int width; - private int height; - private int fps; - private String quality; - private String codec; - @Nullable private ItagItem itagItem; - - /** - * 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 manifestUrl; - // 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 not be 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 not be 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 not be 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}, or {@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 not be null. - *

- * - *

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

- * - * @param deliveryMethod the {@link DeliveryMethod} of the {@link VideoStream}, which must - * not be null - * @return this {@link Builder} instance - */ - public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { - this.deliveryMethod = deliveryMethod; - return this; - } - - /** - * Sets the URL of the manifest this stream comes from (if applicable, otherwise null). - * - * @param manifestUrl the URL of the manifest this stream comes from or {@code null} - * @return this {@link Builder} instance - */ - public Builder setManifestUrl(@Nullable final String manifestUrl) { - this.manifestUrl = manifestUrl; - 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 - * have been 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, manifestUrl, itagItem); - } - } - - /** - * Create a new video stream. - * - * @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 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 manifestUrl the URL of the manifest this stream comes from (if applicable, - * otherwise null) - */ - @SuppressWarnings("checkstyle:ParameterNumber") - 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 manifestUrl, - @Nullable final ItagItem itagItem) { - super(id, content, isUrl, format, deliveryMethod, manifestUrl); - 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 - && resolution.equals(((VideoStream) cmp).resolution) - && isVideoOnly == ((VideoStream) cmp).isVideoOnly; - } - - /** - * Get 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; - } - - /** - * Return whether the stream is video-only. - * - *

- * 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; - } -} From 8b94206ef78174a24a76508364f20f65edc931eb Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 4 Jun 2022 23:31:16 +0200 Subject: [PATCH 03/43] Added Utility --- .../stream/util/NewPipeStreamCollectors.java | 100 ++++++++++++++++++ .../stream/util/NewPipeStreamUtil.java | 38 +++++++ 2 files changed, 138 insertions(+) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java new file mode 100644 index 0000000000..f9d1d03f04 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java @@ -0,0 +1,100 @@ +package org.schabi.newpipe.extractor.streamdata.stream.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Stream; + +public final class NewPipeStreamCollectors { + private NewPipeStreamCollectors() { + // No impl + } + + public static + Collector> toDistinctList() { + return deduplicateEqualStreams(x -> x); + } + + public static + Collector> toDistinctStream() { + return deduplicateEqualStreams(List::stream); + } + + public static + Collector deduplicateEqualStreams(final Function, R> finisher) { + return new CollectorImpl<>( + (Supplier>) ArrayList::new, + List::add, + (left, right) -> { + for(final T rightElement : right) { + if (NewPipeStreamUtil.containSimilarStream(rightElement, left)) { + left.add(rightElement); + } + } + return left; + }, + finisher, + CH_ID); + } + + /** + * Copied from {@link java.util.stream.Collectors} + */ + static final Set CH_ID = + Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); + + /** + * Copied from {@link java.util.stream.Collectors} + */ + static class CollectorImpl implements Collector { + private final Supplier supplier; + private final BiConsumer accumulator; + private final BinaryOperator combiner; + private final Function finisher; + private final Set characteristics; + + CollectorImpl(final Supplier supplier, + final BiConsumer accumulator, + final BinaryOperator combiner, + final Function finisher, + final Set characteristics) { + this.supplier = supplier; + this.accumulator = accumulator; + this.combiner = combiner; + this.finisher = finisher; + this.characteristics = characteristics; + } + + @Override + public BiConsumer accumulator() { + return accumulator; + } + + @Override + public Supplier supplier() { + return supplier; + } + + @Override + public BinaryOperator combiner() { + return combiner; + } + + @Override + public Function finisher() { + return finisher; + } + + @Override + public Set characteristics() { + return characteristics; + } + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java new file mode 100644 index 0000000000..78a62a9454 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.extractor.streamdata.stream.util; + +import org.schabi.newpipe.extractor.streamdata.stream.Stream; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class NewPipeStreamUtil { + private NewPipeStreamUtil() { + // No impl + } + + /** + * Checks if the list already contains a stream with the same statistics. + * + * @param stream the stream to be compared against the streams in the stream list + * @param streams 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, + final Collection streams) { + if (streams == null || streams.isEmpty()) { + return false; + } + return streams.stream().anyMatch(stream::equalsStream); + } + + public List removeEqualStreams(final Collection streams) { + final List returnList = new ArrayList<>(); + for(final T stream : streams) { + if (!containSimilarStream(stream, returnList)) { + returnList.add(stream); + } + } + return returnList; + } +} From 73bd33d88987f520830cac96460ac8750694ab59 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 4 Jun 2022 23:31:53 +0200 Subject: [PATCH 04/43] Rewrote extractors part 1 --- .../extractors/BandcampStreamExtractor.java | 37 +- .../MediaCCCLiveStreamExtractor.java | 137 +++--- .../extractors/MediaCCCParsingHelper.java | 24 +- .../extractors/MediaCCCStreamExtractor.java | 114 ++--- .../extractors/PeertubeStreamExtractor.java | 456 ++++++++---------- .../extractors/SoundcloudStreamExtractor.java | 224 +++++---- 6 files changed, 447 insertions(+), 545 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java index e4120bed89..f4b75b731f 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java @@ -11,7 +11,6 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -19,11 +18,13 @@ import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector; -import org.schabi.newpipe.extractor.stream.AudioStream; 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.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.Utils; @@ -143,15 +144,17 @@ public Description getDescription() { @Override public List getAudioStreams() { - return Collections.singletonList(new AudioStream.Builder() - .setId("mp3-128") - .setContent(albumJson.getArray("trackinfo") - .getObject(0) - .getObject("file") - .getString("mp3-128"), true) - .setMediaFormat(MediaFormat.MP3) - .setAverageBitrate(128) - .build()); + return Collections.singletonList( + new SimpleAudioStreamImpl( + new SimpleProgressiveHTTPDeliveryDataImpl(albumJson + .getArray("trackinfo") + .getObject(0) + .getObject("file") + .getString("mp3-128")), + AudioFormatRegistry.MP3, + 128 + ) + ); } @Override @@ -160,16 +163,6 @@ public long getLength() throws ParsingException { .getDouble("duration"); } - @Override - public List getVideoStreams() { - return Collections.emptyList(); - } - - @Override - public List getVideoOnlyStreams() { - return Collections.emptyList(); - } - @Override public StreamType getStreamType() { return StreamType.AUDIO_STREAM; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 9271bbc322..8afc1a8b9e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -6,25 +6,29 @@ import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.LinkHandler; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; -import org.schabi.newpipe.extractor.stream.Stream; 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.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleDASHUrlDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl; import java.io.IOException; -import java.util.Collections; import java.util.List; -import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nonnull; @@ -45,7 +49,8 @@ public MediaCCCLiveStreamExtractor(final StreamingService service, @Override public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException { - final JsonArray doc = MediaCCCParsingHelper.getLiveStreams(downloader, + final JsonArray doc = MediaCCCParsingHelper.getLiveStreams( + downloader, getExtractorLocalization()); // Find the correct room for (int c = 0; c < doc.size(); c++) { @@ -137,7 +142,7 @@ public String getDashMpdUrl() throws ParsingException { */ @Nonnull @Override - public String getHlsUrl() { + public String getHlsMasterPlaylistUrl() { return getManifestOfDeliveryMethodWanted("hls"); } @@ -155,77 +160,54 @@ private String getManifestOfDeliveryMethodWanted(@Nonnull final String deliveryM @Override public List getAudioStreams() throws IOException, ExtractionException { - return getStreams("audio", - dto -> { - final AudioStream.Builder builder = new AudioStream.Builder() - .setId(dto.urlValue.getString("tech", ID_UNKNOWN)) - .setContent(dto.urlValue.getString(URL), true) - .setAverageBitrate(UNKNOWN_BITRATE); - - if ("hls".equals(dto.urlKey)) { - // We don't know with the type string what media format will - // have HLS streams. - // However, the tech string may contain some information - // about the media format used. - return builder.setDeliveryMethod(DeliveryMethod.HLS) - .build(); + return getStreamDTOs("audio") + .map(dto -> { + final String url = dto.getUrlValue().getString(URL); + final DeliveryData deliveryData; + if ("hls".equals(dto.getUrlKey())) { + deliveryData = new SimpleHLSDeliveryDataImpl(url); + } else if ("dash".equals(dto.getUrlKey())) { + deliveryData = new SimpleDASHUrlDeliveryDataImpl(url); + } else { + deliveryData = new SimpleProgressiveHTTPDeliveryDataImpl(url); } - return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey)) - .build(); - }); + return new SimpleAudioStreamImpl( + deliveryData, + // TODO: This looks wrong + new AudioFormatRegistry().getFromSuffix(dto.getUrlKey()) + ); + }) + .collect(Collectors.toList()); } @Override - public List getVideoStreams() throws IOException, ExtractionException { - return getStreams("video", - dto -> { - final JsonArray videoSize = dto.streamJsonObj.getArray("videoSize"); - - final VideoStream.Builder builder = new VideoStream.Builder() - .setId(dto.urlValue.getString("tech", ID_UNKNOWN)) - .setContent(dto.urlValue.getString(URL), true) - .setIsVideoOnly(false) - .setResolution(videoSize.getInt(0) + "x" + videoSize.getInt(1)); - - if ("hls".equals(dto.urlKey)) { - // We don't know with the type string what media format will - // have HLS streams. - // However, the tech string may contain some information - // about the media format used. - return builder.setDeliveryMethod(DeliveryMethod.HLS) - .build(); + public List getVideoStreams() throws IOException, ExtractionException { + return getStreamDTOs("video") + .map(dto -> { + final String url = dto.getUrlValue().getString(URL); + final DeliveryData deliveryData; + if ("hls".equals(dto.getUrlKey())) { + deliveryData = new SimpleHLSDeliveryDataImpl(url); + } else if ("dash".equals(dto.getUrlKey())) { + deliveryData = new SimpleDASHUrlDeliveryDataImpl(url); + } else { + deliveryData = new SimpleProgressiveHTTPDeliveryDataImpl(url); } - return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey)) - .build(); - }); - } - - - /** - * This is just an internal class used in {@link #getStreams(String, Function)} to tie together - * the stream json object, its URL key and its URL value. An object of this class would be - * temporary and the three values it holds would be converted to a proper {@link Stream} - * object based on the wanted stream type. - */ - private static final class MediaCCCLiveStreamMapperDTO { - final JsonObject streamJsonObj; - final String urlKey; - final JsonObject urlValue; + final JsonArray videoSize = dto.getStreamJsonObj().getArray("videoSize"); - MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj, - final String urlKey, - final JsonObject urlValue) { - this.streamJsonObj = streamJsonObj; - this.urlKey = urlKey; - this.urlValue = urlValue; - } + return new SimpleVideoAudioStreamImpl( + deliveryData, + // TODO: This looks wrong + new VideoAudioFormatRegistry().getFromSuffix(dto.getUrlKey()), + videoSize.getInt(0) + "x" + videoSize.getInt(1) + ); + }) + .collect(Collectors.toList()); } - private List getStreams( - @Nonnull final String streamType, - @Nonnull final Function converter) { + private Stream getStreamDTOs(@Nonnull final String streamType) { return room.getArray(STREAMS).stream() // Ensure that we use only process JsonObjects .filter(JsonObject.class::isInstance) @@ -238,22 +220,17 @@ private List getStreams( .map(e -> new MediaCCCLiveStreamMapperDTO( streamJsonObj, e.getKey(), - (JsonObject) e.getValue()))) - // The DASH manifest will be extracted with getDashMpdUrl - .filter(dto -> !"dash".equals(dto.urlKey)) - // Convert - .map(converter) - .collect(Collectors.toList()); + (JsonObject) e.getValue()))); } @Override - public List getVideoOnlyStreams() { - return Collections.emptyList(); + public StreamType getStreamType() throws ParsingException { + return StreamType.LIVE_STREAM; } @Override - public StreamType getStreamType() throws ParsingException { - return StreamType.LIVE_STREAM; + public boolean isLive() { + return true; } @Nonnull diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java index ad07c4720d..84b62e225e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java @@ -3,6 +3,7 @@ import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; + import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; @@ -43,13 +44,21 @@ public static boolean isLiveStreamId(final String id) { /** * Get currently available live streams from * - * https://streaming.media.ccc.de/streams/v2.json. + * https://streaming.media.ccc.de/streams/v2.json. + *

* Use this method to cache requests, because they can get quite big. + *

+ * + *

+ * For more information see also: + * https://github.com/voc/streaming-website#json-api. + *

* TODO: implement better caching policy (max-age: 3 min) - * @param downloader The downloader to use for making the request + * + * @param downloader The downloader to use for making the request * @param localization The localization to be used. Will most likely be ignored. * @return {@link JsonArray} containing current conferences and info about their rooms and - * streams. + * streams. * @throws ExtractionException if the data could not be fetched or the retrieved data could not * be parsed to a {@link JsonArray} */ @@ -58,13 +67,14 @@ public static JsonArray getLiveStreams(final Downloader downloader, throws ExtractionException { if (liveStreams == null) { try { - final String site = downloader.get("https://streaming.media.ccc.de/streams/v2.json", - localization).responseBody(); + final String site = downloader + .get("https://streaming.media.ccc.de/streams/v2.json", localization) + .responseBody(); liveStreams = JsonParser.array().from(site); } catch (final IOException | ReCaptchaException e) { - throw new ExtractionException("Could not get live stream JSON.", e); + throw new ExtractionException("Could not get live stream JSON", e); } catch (final JsonParserException e) { - throw new ExtractionException("Could not parse JSON.", e); + throw new ExtractionException("Could not parse JSON", e); } } return liveStreams; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index 0a086fcc6b..10772010ad 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -1,14 +1,9 @@ package org.schabi.newpipe.extractor.services.media_ccc.extractors; -import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; -import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN; - -import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -18,18 +13,23 @@ import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferenceLinkHandlerFactory; import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCStreamLinkHandlerFactory; -import org.schabi.newpipe.extractor.stream.AudioStream; 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.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl; import org.schabi.newpipe.extractor.utils.JsonUtils; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nonnull; @@ -96,75 +96,33 @@ public String getUploaderAvatarUrl() { @Override public List getAudioStreams() throws ExtractionException { - final JsonArray recordings = data.getArray("recordings"); - final List audioStreams = new ArrayList<>(); - for (int i = 0; i < recordings.size(); i++) { - final JsonObject recording = recordings.getObject(i); - final String mimeType = recording.getString("mime_type"); - if (mimeType.startsWith("audio")) { - // First we need to resolve the actual video data from the CDN - final MediaFormat mediaFormat; - if (mimeType.endsWith("opus")) { - mediaFormat = MediaFormat.OPUS; - } else if (mimeType.endsWith("mpeg")) { - mediaFormat = MediaFormat.MP3; - } else if (mimeType.endsWith("ogg")) { - mediaFormat = MediaFormat.OGG; - } else { - mediaFormat = null; - } - - // Not checking containsSimilarStream here, since MediaCCC does not provide enough - // information to decide whether two streams are similar. Hence that method would - // always return false, e.g. even for different language variations. - audioStreams.add(new AudioStream.Builder() - .setId(recording.getString("filename", ID_UNKNOWN)) - .setContent(recording.getString("recording_url"), true) - .setMediaFormat(mediaFormat) - .setAverageBitrate(UNKNOWN_BITRATE) - .build()); - } - } - return audioStreams; - } - - @Override - public List getVideoStreams() throws ExtractionException { - final JsonArray recordings = data.getArray("recordings"); - final List videoStreams = new ArrayList<>(); - for (int i = 0; i < recordings.size(); i++) { - final JsonObject recording = recordings.getObject(i); - final String mimeType = recording.getString("mime_type"); - if (mimeType.startsWith("video")) { - // First we need to resolve the actual video data from the CDN - final MediaFormat mediaFormat; - if (mimeType.endsWith("webm")) { - mediaFormat = MediaFormat.WEBM; - } else if (mimeType.endsWith("mp4")) { - mediaFormat = MediaFormat.MPEG_4; - } else { - mediaFormat = null; - } - - // Not checking containsSimilarStream here, since MediaCCC does not provide enough - // information to decide whether two streams are similar. Hence that method would - // always return false, e.g. even for different language variations. - videoStreams.add(new VideoStream.Builder() - .setId(recording.getString("filename", ID_UNKNOWN)) - .setContent(recording.getString("recording_url"), true) - .setIsVideoOnly(false) - .setMediaFormat(mediaFormat) - .setResolution(recording.getInt("height") + "p") - .build()); - } - } - - return videoStreams; - } - - @Override - public List getVideoOnlyStreams() { - return Collections.emptyList(); + return getRecordingsByMimeType("audio") + .map(o -> new SimpleAudioStreamImpl( + new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url")), + new AudioFormatRegistry().getFromMimeType(o.getString("mime_type")) + )) + .collect(Collectors.toList()); + } + + @Override + public List getVideoStreams() throws ExtractionException { + return getRecordingsByMimeType("video") + .map(o -> new SimpleVideoAudioStreamImpl( + new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url")), + null, + AudioStream.UNKNOWN_BITRATE, + new VideoAudioFormatRegistry().getFromMimeType(o.getString("mime_type")), + o.getInt("height") + "p" + )) + .collect(Collectors.toList()); + } + + private Stream getRecordingsByMimeType(final String startsWithMimeType) { + return data.getArray("recordings").stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .filter(rec -> rec.getString("mime_type", "") + .startsWith(startsWithMimeType)); } @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index d70abf11d1..6ed0199517 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.extractor.services.peertube.extractors; -import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; import static org.schabi.newpipe.extractor.utils.Utils.UTF_8; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; @@ -9,7 +8,6 @@ import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; @@ -22,15 +20,24 @@ import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper; import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeStreamLinkHandlerFactory; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; -import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.extractor.stream.SubtitlesStream; -import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleTorrentDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.format.registry.SubtitleFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.Stream; +import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleSubtitleStreamImpl; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.Utils; @@ -41,6 +48,9 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -59,9 +69,9 @@ public class PeertubeStreamExtractor extends StreamExtractor { private final String baseUrl; private JsonObject json; - private final List subtitles = new ArrayList<>(); - private final List audioStreams = new ArrayList<>(); - private final List videoStreams = new ArrayList<>(); + private List subtitles = null; + private List audioStreams = null; + private List videoStreams = null; public PeertubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler) throws ParsingException { @@ -118,12 +128,7 @@ public Description getDescription() throws ParsingException { @Override public int getAgeLimit() throws ParsingException { - final boolean isNSFW = JsonUtils.getBoolean(json, "nsfw"); - if (isNSFW) { - return 18; - } else { - return NO_AGE_LIMIT; - } + return JsonUtils.getBoolean(json, "nsfw") ? 18 : NO_AGE_LIMIT; } @Override @@ -136,12 +141,9 @@ public long getTimeStamp() throws ParsingException { final long timestamp = getTimestampSeconds( "((#|&|\\?)start=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)"); - if (timestamp == -2) { - // regex for timestamp was not found - return 0; - } else { - return timestamp; - } + return (timestamp == -2) + ? 0 // regex for timestamp was not found + : timestamp; } @Override @@ -212,15 +214,16 @@ public String getSubChannelAvatarUrl() { @Nonnull @Override - public String getHlsUrl() { + public String getHlsMasterPlaylistUrl() throws ParsingException { assertPageFetched(); - if (getStreamType() == StreamType.VIDEO_STREAM - && !isNullOrEmpty(json.getObject(FILES))) { + if (!isLive() && !isNullOrEmpty(json.getObject(FILES))) { return json.getObject(FILES).getString(PLAYLIST_URL, ""); } - return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL, ""); + return json.getArray(STREAMING_PLAYLISTS) + .getObject(0) + .getString(PLAYLIST_URL, ""); } @Override @@ -234,51 +237,35 @@ public List getAudioStreams() throws ParsingException { That's why the extraction of audio streams is only run when there are video streams extracted and when the content is not a livestream. */ - if (audioStreams.isEmpty() && videoStreams.isEmpty() - && getStreamType() == StreamType.VIDEO_STREAM) { - getStreams(); - } + tryExtractStreams(); return audioStreams; } @Override - public List getVideoStreams() throws ExtractionException { + public List getVideoStreams() throws ExtractionException { assertPageFetched(); - if (videoStreams.isEmpty()) { - if (getStreamType() == StreamType.VIDEO_STREAM) { - getStreams(); - } else { - extractLiveVideoStreams(); - } - } + tryExtractStreams(); return videoStreams; } - @Override - public List getVideoOnlyStreams() { - return Collections.emptyList(); - } - @Nonnull @Override - public List getSubtitlesDefault() { + public List getSubtitles() { + assertPageFetched(); return subtitles; } - @Nonnull @Override - public List getSubtitles(final MediaFormat format) { - return subtitles.stream() - .filter(sub -> sub.getFormat() == format) - .collect(Collectors.toList()); + public StreamType getStreamType() { + return isLive() ? StreamType.LIVE_STREAM : StreamType.VIDEO_STREAM; } @Override - public StreamType getStreamType() { - return json.getBoolean("isLive") ? StreamType.LIVE_STREAM : StreamType.VIDEO_STREAM; + public boolean isLive() { + return json.getBoolean("isLive"); } @Nullable @@ -383,7 +370,7 @@ public void onFetchPage(@Nonnull final Downloader downloader) throw new ExtractionException("Could not extract PeerTube channel data"); } - loadSubtitles(); + tryExtractSubtitles(); } private void setInitialData(final String responseBody) throws ExtractionException { @@ -398,246 +385,193 @@ private void setInitialData(final String responseBody) throws ExtractionExceptio PeertubeParsingHelper.validate(json); } - private void loadSubtitles() { - if (subtitles.isEmpty()) { - try { - final Response response = getDownloader().get(baseUrl - + PeertubeStreamLinkHandlerFactory.VIDEO_API_ENDPOINT - + getId() + "/captions"); - final JsonObject captionsJson = JsonParser.object().from(response.responseBody()); - final JsonArray captions = JsonUtils.getArray(captionsJson, "data"); - for (final Object c : captions) { - if (c instanceof JsonObject) { - final JsonObject caption = (JsonObject) c; - final String url = baseUrl + JsonUtils.getString(caption, "captionPath"); - final String languageCode = JsonUtils.getString(caption, "language.id"); - final String ext = url.substring(url.lastIndexOf(".") + 1); - final MediaFormat fmt = MediaFormat.getFromSuffix(ext); - if (fmt != null && !isNullOrEmpty(languageCode)) { - subtitles.add(new SubtitlesStream.Builder() - .setContent(url, true) - .setMediaFormat(fmt) - .setLanguageCode(languageCode) - .setAutoGenerated(false) - .build()); - } - } - } - } catch (final Exception ignored) { - // Ignore all exceptions - } + private void tryExtractSubtitles() { + if (subtitles != null) { + return; } - } - - private void extractLiveVideoStreams() throws ParsingException { try { - final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS); - streamingPlaylists.stream() + final Response response = getDownloader().get(baseUrl + + PeertubeStreamLinkHandlerFactory.VIDEO_API_ENDPOINT + + getId() + "/captions"); + final JsonObject captionsJson = JsonParser.object().from(response.responseBody()); + final JsonArray captions = JsonUtils.getArray(captionsJson, "data"); + + subtitles = captions.stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) - .map(stream -> new VideoStream.Builder() - .setId(String.valueOf(stream.getInt("id", -1))) - .setContent(stream.getString(PLAYLIST_URL, ""), true) - .setIsVideoOnly(false) - .setResolution("") - .setMediaFormat(MediaFormat.MPEG_4) - .setDeliveryMethod(DeliveryMethod.HLS) - .build()) - // Don't use the containsSimilarStream method because it will always return - // false so if there are multiples HLS URLs returned, only the first will be - // extracted in this case. - .forEachOrdered(videoStreams::add); - } catch (final Exception e) { - throw new ParsingException("Could not get video streams", e); + .map(caption -> { + try { + final String url = baseUrl + JsonUtils.getString(caption, + "captionPath"); + + return new SimpleSubtitleStreamImpl( + new SimpleProgressiveHTTPDeliveryDataImpl(url), + new SubtitleFormatRegistry() + .getFromSuffix( + url.substring(url.lastIndexOf(".") + 1)), + false, + JsonUtils.getString(caption, "language.id") + ); + } catch (final Exception ignored) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } catch (final Exception ignored) { + subtitles = Collections.emptyList(); } } - private void getStreams() throws ParsingException { + private void tryExtractStreams() throws ParsingException { + if (audioStreams != null && videoStreams != null) { + return; + } + + // Initialize + audioStreams = new ArrayList<>(); + videoStreams = new ArrayList<>(); + + if (isLive()) { + extractLiveVideoStreams(); + return; + } + // Progressive streams - getStreamsFromArray(json.getArray(FILES), ""); + try { + addStreamsFromArray( + json.getArray(FILES), + null); + } catch (final Exception e) { + throw new ParsingException("Could not add HLS streams", e); + } // HLS streams try { - for (final JsonObject playlist : json.getArray(STREAMING_PLAYLISTS).stream() + json.getArray(STREAMING_PLAYLISTS).stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) - .collect(Collectors.toList())) { - getStreamsFromArray(playlist.getArray(FILES), playlist.getString(PLAYLIST_URL)); - } + .forEach(playlist -> addStreamsFromArray( + playlist.getArray(FILES), + playlist.getString(PLAYLIST_URL))); } catch (final Exception e) { - throw new ParsingException("Could not get streams", e); + throw new ParsingException("Could not add HLS streams", e); } } - private void getStreamsFromArray(@Nonnull final JsonArray streams, - final String playlistUrl) throws ParsingException { + private void extractLiveVideoStreams() throws ParsingException { try { - /* - Starting with version 3.4.0 of PeerTube, the HLS playlist of stream resolutions - contains the UUID of the streams, so we can't use the same method to get the URL of - the HLS playlist without fetching the master playlist. - These UUIDs are the same as the ones returned into the fileUrl and fileDownloadUrl - strings. - */ - final boolean isInstanceUsingRandomUuidsForHlsStreams = !isNullOrEmpty(playlistUrl) - && playlistUrl.endsWith("-master.m3u8"); - - for (final JsonObject stream : streams.stream() + json.getArray(STREAMING_PLAYLISTS) + .stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) - .collect(Collectors.toList())) { - - // Extract stream version of streams first - final String url = JsonUtils.getString(stream, - stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL); - if (isNullOrEmpty(url)) { - // Not a valid stream URL - return; - } - - final String resolution = JsonUtils.getString(stream, "resolution.label"); - final String idSuffix = stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL; - - if (resolution.toLowerCase().contains("audio")) { - // An audio stream - addNewAudioStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution, - idSuffix, url, playlistUrl); - } else { - // A video stream - addNewVideoStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution, - idSuffix, url, playlistUrl); - } - } + .map(stream -> new SimpleVideoAudioStreamImpl( + new SimpleHLSDeliveryDataImpl(stream.getString(PLAYLIST_URL, "")), + VideoAudioFormatRegistry.MPEG_4) + ) + // Don't use the containsSimilarStream method because it will always + // return + // false so if there are multiples HLS URLs returned, only the first + // will be + // extracted in this case. + .forEachOrdered(videoStreams::add); } catch (final Exception e) { - throw new ParsingException("Could not get streams from array", e); + throw new ParsingException("Could not get video streams", e); } } - @Nonnull - private String getHlsPlaylistUrlFromFragmentedFileUrl( - @Nonnull final JsonObject streamJsonObject, - @Nonnull final String idSuffix, - @Nonnull final String format, - @Nonnull final String url) throws ParsingException { - final String streamUrl = FILE_DOWNLOAD_URL.equals(idSuffix) - ? JsonUtils.getString(streamJsonObject, FILE_URL) - : url; - return streamUrl.replace("-fragmented." + format, ".m3u8"); + private void addStreamsFromArray( + @Nonnull final JsonArray streams, + final String playlistUrl + ) { + streams.stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .filter(stream -> !isNullOrEmpty(getUrlFromStream(stream))) + .forEach(stream -> { + final String resolution = getResolutionFromStream(stream); + + if (resolution.toLowerCase().contains("audio")) { + // An audio stream + addNewStreams( + this.audioStreams, + stream, + playlistUrl, + (s, dd) -> new SimpleAudioStreamImpl( + dd, + new AudioFormatRegistry() + .getFromSuffix(getExtensionFromStream(s)) + ) + ); + + } else { + // A video stream + addNewStreams( + this.videoStreams, + stream, + playlistUrl, + (s, dd) -> new SimpleVideoAudioStreamImpl( + dd, + new VideoAudioFormatRegistry() + .getFromSuffix(getExtensionFromStream(s)), + resolution + ) + ); + } + }); } - @Nonnull - private String getHlsPlaylistUrlFromMasterPlaylist(@Nonnull final JsonObject streamJsonObject, - @Nonnull final String playlistUrl) - throws ParsingException { - return playlistUrl.replace("master", JsonUtils.getNumber(streamJsonObject, - RESOLUTION_ID).toString()); - } - - private void addNewAudioStream(@Nonnull final JsonObject streamJsonObject, - final boolean isInstanceUsingRandomUuidsForHlsStreams, - @Nonnull final String resolution, - @Nonnull final String idSuffix, - @Nonnull final String url, - @Nullable final String playlistUrl) throws ParsingException { - final String extension = url.substring(url.lastIndexOf(".") + 1); - final MediaFormat format = MediaFormat.getFromSuffix(extension); - final String id = resolution + "-" + extension; - - // Add progressive HTTP streams first - audioStreams.add(new AudioStream.Builder() - .setId(id + "-" + idSuffix + "-" + DeliveryMethod.PROGRESSIVE_HTTP) - .setContent(url, true) - .setMediaFormat(format) - .setAverageBitrate(UNKNOWN_BITRATE) - .build()); - - // Then add HLS streams - if (!isNullOrEmpty(playlistUrl)) { - final String hlsStreamUrl; - if (isInstanceUsingRandomUuidsForHlsStreams) { - hlsStreamUrl = getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix, - extension, url); - - } else { - hlsStreamUrl = getHlsPlaylistUrlFromMasterPlaylist(streamJsonObject, playlistUrl); - } - final AudioStream audioStream = new AudioStream.Builder() - .setId(id + "-" + DeliveryMethod.HLS) - .setContent(hlsStreamUrl, true) - .setDeliveryMethod(DeliveryMethod.HLS) - .setMediaFormat(format) - .setAverageBitrate(UNKNOWN_BITRATE) - .setManifestUrl(playlistUrl) - .build(); - if (!Stream.containSimilarStream(audioStream, audioStreams)) { - audioStreams.add(audioStream); - } - } + private static String getResolutionFromStream(@Nonnull final JsonObject stream) { + return stream.getObject("resolution") + .getString("label"); + } - // Finally, add torrent URLs - final String torrentUrl = JsonUtils.getString(streamJsonObject, "torrentUrl"); - if (!isNullOrEmpty(torrentUrl)) { - audioStreams.add(new AudioStream.Builder() - .setId(id + "-" + idSuffix + "-" + DeliveryMethod.TORRENT) - .setContent(torrentUrl, true) - .setDeliveryMethod(DeliveryMethod.TORRENT) - .setMediaFormat(format) - .setAverageBitrate(UNKNOWN_BITRATE) - .build()); - } + private static String getStreamUrlKeyFromStream(@Nonnull final JsonObject stream) { + return stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL; } - private void addNewVideoStream(@Nonnull final JsonObject streamJsonObject, - final boolean isInstanceUsingRandomUuidsForHlsStreams, - @Nonnull final String resolution, - @Nonnull final String idSuffix, - @Nonnull final String url, - @Nullable final String playlistUrl) throws ParsingException { - final String extension = url.substring(url.lastIndexOf(".") + 1); - final MediaFormat format = MediaFormat.getFromSuffix(extension); - final String id = resolution + "-" + extension; - - // Add progressive HTTP streams first - videoStreams.add(new VideoStream.Builder() - .setId(id + "-" + idSuffix + "-" + DeliveryMethod.PROGRESSIVE_HTTP) - .setContent(url, true) - .setIsVideoOnly(false) - .setResolution(resolution) - .setMediaFormat(format) - .build()); - - // Then add HLS streams - if (!isNullOrEmpty(playlistUrl)) { - final String hlsStreamUrl = isInstanceUsingRandomUuidsForHlsStreams - ? getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix, extension, - url) - : getHlsPlaylistUrlFromMasterPlaylist(streamJsonObject, playlistUrl); - - final VideoStream videoStream = new VideoStream.Builder() - .setId(id + "-" + DeliveryMethod.HLS) - .setContent(hlsStreamUrl, true) - .setIsVideoOnly(false) - .setDeliveryMethod(DeliveryMethod.HLS) - .setResolution(resolution) - .setMediaFormat(format) - .setManifestUrl(playlistUrl) - .build(); - if (!Stream.containSimilarStream(videoStream, videoStreams)) { - videoStreams.add(videoStream); - } + private static String getUrlFromStream(@Nonnull final JsonObject stream) { + return stream.getString(getStreamUrlKeyFromStream(stream)); + } + + private static String getExtensionFromStream(@Nonnull final JsonObject stream) { + final String url = stream.getString(getStreamUrlKeyFromStream(stream)); + return url.substring(url.lastIndexOf(".") + 1); + } + + private void addNewStreams( + final List streams, + @Nonnull final JsonObject stream, + final String playlistUrl, + @Nonnull final BiFunction buildStream + ) { + final Consumer addDeliveryDataToStream = + dd -> { + try { + streams.add(buildStream.apply(stream, dd)); + } catch (final Exception ignored) { + // Ignore exception when a single stream couldn't be added + } + }; + + // Add Progressive HTTP (this is also done for HLS streams because the source file can + // also be streamed over progressive HTTP) + addDeliveryDataToStream.accept( + new SimpleProgressiveHTTPDeliveryDataImpl(getUrlFromStream(stream))); + + // Add HLS (only for PeerTube 3.4+) + if (!isNullOrEmpty(playlistUrl) + && playlistUrl.endsWith("-master.m3u8") + && !isNullOrEmpty(stream.getString(FILE_URL)) + ) { + addDeliveryDataToStream.accept(new SimpleHLSDeliveryDataImpl(stream.getString(FILE_URL) + .replace("-fragmented." + getExtensionFromStream(stream), ".m3u8"))); } - // Add finally torrent URLs - final String torrentUrl = JsonUtils.getString(streamJsonObject, "torrentUrl"); - if (!isNullOrEmpty(torrentUrl)) { - videoStreams.add(new VideoStream.Builder() - .setId(id + "-" + idSuffix + "-" + DeliveryMethod.TORRENT) - .setContent(torrentUrl, true) - .setIsVideoOnly(false) - .setDeliveryMethod(DeliveryMethod.TORRENT) - .setResolution(resolution) - .setMediaFormat(format) - .build()); + // Add torrent + if (!isNullOrEmpty(stream.getString("torrentUrl"))) { + addDeliveryDataToStream.accept( + new SimpleTorrentDeliveryDataImpl(stream.getString("torrentUrl"))); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 0d28ed9544..8872c9495d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -2,8 +2,7 @@ import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL; import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.clientId; -import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; -import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN; +import static org.schabi.newpipe.extractor.utils.Utils.HTTPS; import static org.schabi.newpipe.extractor.utils.Utils.UTF_8; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; @@ -12,7 +11,6 @@ import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; @@ -20,18 +18,22 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; -import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; +import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; +import org.schabi.newpipe.extractor.streamdata.stream.util.NewPipeStreamCollectors; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -39,6 +41,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Optional; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -162,45 +166,41 @@ public String getUploaderAvatarUrl() { @Override public List getAudioStreams() throws ExtractionException { - final List audioStreams = new ArrayList<>(); // Streams can be streamable and downloadable - or explicitly not. // For playing the track, it is only necessary to have a streamable track. // If this is not the case, this track might not be published yet. + // If audio streams were calculated, return the calculated result if (!track.getBoolean("streamable") || !isAvailable) { - return audioStreams; + return Collections.emptyList(); } try { - final JsonArray transcodings = track.getObject("media").getArray("transcodings"); - if (!isNullOrEmpty(transcodings)) { - // Get information about what stream formats are available - extractAudioStreams(transcodings, checkMp3ProgressivePresence(transcodings), - audioStreams); - } - - extractDownloadableFileIfAvailable(audioStreams); + final List audioStreams = new ArrayList<>(extractAudioStreams()); + extractDownloadableFileIfAvailable().ifPresent(audioStreams::add); + return audioStreams; } catch (final NullPointerException e) { - throw new ExtractionException("Could not get audio streams", e); + throw new ExtractionException("Could not get SoundCloud's tracks audio URL", e); } - - return audioStreams; } private static boolean checkMp3ProgressivePresence(@Nonnull final JsonArray transcodings) { return transcodings.stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) - .anyMatch(transcodingJsonObject -> transcodingJsonObject.getString("preset") - .contains("mp3") && transcodingJsonObject.getObject("format") - .getString("protocol").equals("progressive")); + .anyMatch(transcoding -> transcoding.getString("preset").contains("mp3") + && transcoding.getObject("format").getString("protocol") + .equals("progressive")); } @Nonnull - private String getTranscodingUrl(final String endpointUrl) + private String getTranscodingUrl(final String endpointUrl, + final String protocol) throws IOException, ExtractionException { - final String apiStreamUrl = endpointUrl + "?client_id=" + clientId(); - final String response = NewPipe.getDownloader().get(apiStreamUrl).responseBody(); + final Downloader downloader = NewPipe.getDownloader(); + final String apiStreamUrl = endpointUrl + "?client_id=" + + clientId(); + final String response = downloader.get(apiStreamUrl).responseBody(); final JsonObject urlObject; try { urlObject = JsonParser.object().from(response); @@ -230,59 +230,59 @@ private String getDownloadUrl(@Nonnull final String trackId) return null; } - private void extractAudioStreams(@Nonnull final JsonArray transcodings, - final boolean mp3ProgressiveInStreams, - final List audioStreams) { - transcodings.stream() + private List extractAudioStreams() { + final JsonArray transcodings = track.getObject("media").getArray("transcodings"); + if (isNullOrEmpty(transcodings)) { + return Collections.emptyList(); + } + + final boolean mp3ProgressiveInStreams = checkMp3ProgressivePresence(transcodings); + + return transcodings.stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) - .forEachOrdered(transcoding -> { - final String url = transcoding.getString("url"); - if (isNullOrEmpty(url)) { - return; - } - + .filter(transcoding -> !isNullOrEmpty(transcoding.getString("url"))) + .map(transcoding -> { + final String protocol = transcoding + .getObject("format") + .getString("protocol"); + final String mediaUrl; try { - final String preset = transcoding.getString("preset", ID_UNKNOWN); - final String protocol = transcoding.getObject("format") - .getString("protocol"); - final AudioStream.Builder builder = new AudioStream.Builder() - .setId(preset); - - final boolean isHls = protocol.equals("hls"); - if (isHls) { - builder.setDeliveryMethod(DeliveryMethod.HLS); - } + mediaUrl = getTranscodingUrl(transcoding.getString("url"), protocol); + } catch (final Exception e) { + return null; // Abort if something went wrong + } + if (isNullOrEmpty(mediaUrl)) { + return null; // Ignore invalid urls + } - builder.setContent(getTranscodingUrl(url), true); - - if (preset.contains("mp3")) { - // Don't add the MP3 HLS stream if there is a progressive stream - // present because both have the same bitrate - if (mp3ProgressiveInStreams && isHls) { - return; - } - - builder.setMediaFormat(MediaFormat.MP3); - builder.setAverageBitrate(128); - } else if (preset.contains("opus")) { - builder.setMediaFormat(MediaFormat.OPUS); - builder.setAverageBitrate(64); - builder.setDeliveryMethod(DeliveryMethod.HLS); - } else { - // Unknown format, skip to the next audio stream - return; - } + final String preset = transcoding.getString("preset", ""); - final AudioStream audioStream = builder.build(); - if (!Stream.containSimilarStream(audioStream, audioStreams)) { - audioStreams.add(audioStream); + AudioMediaFormat mediaFormat = null; + int averageBitrate = AudioStream.UNKNOWN_BITRATE; + if (preset.contains("mp3")) { + // Don't add the MP3 HLS stream if there is a progressive stream present + // because the two have the same bitrate + if (mp3ProgressiveInStreams && protocol.equals("hls")) { + return null; } - } catch (final ExtractionException | IOException ignored) { - // Something went wrong when trying to get and add this audio stream, - // skip to the next one + mediaFormat = AudioFormatRegistry.MP3; + averageBitrate = 128; + } else if (preset.contains("opus")) { + mediaFormat = AudioFormatRegistry.OPUS; + averageBitrate = 64; } - }); + + return (AudioStream) new SimpleAudioStreamImpl( + protocol.equals("hls") + ? new SimpleHLSDeliveryDataImpl(mediaUrl) + : new SimpleProgressiveHTTPDeliveryDataImpl(mediaUrl), + mediaFormat, + averageBitrate + ); + }) + .filter(Objects::nonNull) + .collect(NewPipeStreamCollectors.toDistinctList()); } /** @@ -298,26 +298,66 @@ private void extractAudioStreams(@Nonnull final JsonArray transcodings, * downloaded, and not otherwise. *

* - * @param audioStreams the audio streams to which the downloadable file is added + * @return An {@link Optional} that may contain the extracted audio stream */ - public void extractDownloadableFileIfAvailable(final List audioStreams) { - if (track.getBoolean("downloadable") && track.getBoolean("has_downloads_left")) { - try { - final String downloadUrl = getDownloadUrl(getId()); - if (!isNullOrEmpty(downloadUrl)) { - audioStreams.add(new AudioStream.Builder() - .setId("original-format") - .setContent(downloadUrl, true) - .setAverageBitrate(UNKNOWN_BITRATE) - .build()); - } - } catch (final Exception ignored) { - // If something went wrong when trying to get the download URL, ignore the - // exception throw because this "stream" is not necessary to play the track + @Nonnull + private Optional extractDownloadableFileIfAvailable() { + if (!track.getBoolean("downloadable") || !track.getBoolean("has_downloads_left")) { + return Optional.empty(); + } + + try { + final String downloadUrl = getDownloadUrl(getId()); + if (isNullOrEmpty(downloadUrl)) { + return Optional.empty(); } + return Optional.of(new SimpleAudioStreamImpl( + new SimpleProgressiveHTTPDeliveryDataImpl(downloadUrl) + )); + } catch (final Exception ignored) { + // If something went wrong when trying to get the download URL, ignore the + // exception throw because this "stream" is not necessary to play the track + return Optional.empty(); } } + /** + * Parses a SoundCloud HLS manifest to get a single URL of HLS streams. + * + *

+ * This method downloads the provided manifest URL, finds all web occurrences in the manifest, + * gets the last segment URL, changes its segment range to {@code 0/track-length}, and return + * this as a string. + *

+ * + * @param hlsManifestUrl the URL of the manifest to be parsed + * @return a single URL that contains a range equal to the length of the track + */ + @Nonnull + private static String getSingleUrlFromHlsManifest(@Nonnull final String hlsManifestUrl) + throws ParsingException { + final Downloader dl = NewPipe.getDownloader(); + final String hlsManifestResponse; + + try { + hlsManifestResponse = dl.get(hlsManifestUrl).responseBody(); + } catch (final IOException | ReCaptchaException e) { + throw new ParsingException("Could not get SoundCloud HLS manifest"); + } + + final String[] lines = hlsManifestResponse.split("\\r?\\n"); + for (int l = lines.length - 1; l >= 0; l--) { + final String line = lines[l]; + // Get the last URL from manifest, because it contains the range of the stream + if (line.trim().length() != 0 && !line.startsWith("#") && line.startsWith("https")) { + final String[] hlsLastRangeUrlArray = line.split("/"); + return HTTPS + hlsLastRangeUrlArray[2] + "/media/0/" + hlsLastRangeUrlArray[5] + + "/" + hlsLastRangeUrlArray[6]; + } + } + throw new ParsingException("Could not get any URL from HLS manifest"); + } + private static String urlEncode(final String value) { try { return URLEncoder.encode(value, UTF_8); @@ -326,16 +366,6 @@ private static String urlEncode(final String value) { } } - @Override - public List getVideoStreams() { - return Collections.emptyList(); - } - - @Override - public List getVideoOnlyStreams() { - return Collections.emptyList(); - } - @Override public StreamType getStreamType() { return StreamType.AUDIO_STREAM; From d0bae9e2a113df4841aef9099af900a686213c67 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 5 Jun 2022 17:00:22 +0200 Subject: [PATCH 05/43] Added VideoQualityData --- .../MediaCCCLiveStreamExtractor.java | 6 ++- .../extractors/MediaCCCStreamExtractor.java | 10 ++-- .../extractors/PeertubeStreamExtractor.java | 17 +++---- .../streamdata/stream/VideoStream.java | 12 ++--- .../stream/quality/VideoQualityData.java | 46 +++++++++++++++++++ .../SimpleVideoAudioStreamImpl.java | 17 +++---- .../simpleimpl/SimpleVideoStreamImpl.java | 17 +++---- 7 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 8afc1a8b9e..28424b2633 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -22,6 +22,7 @@ import org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry; import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl; @@ -201,7 +202,10 @@ public List getVideoStreams() throws IOException, ExtractionEx deliveryData, // TODO: This looks wrong new VideoAudioFormatRegistry().getFromSuffix(dto.getUrlKey()), - videoSize.getInt(0) + "x" + videoSize.getInt(1) + new VideoQualityData( + /*height=*/videoSize.getInt(1, VideoQualityData.UNKNOWN), + /*width=*/videoSize.getInt(0, VideoQualityData.UNKNOWN), + VideoQualityData.UNKNOWN) ); }) .collect(Collectors.toList()); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index 10772010ad..3d22717229 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -21,6 +21,7 @@ import org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry; import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl; import org.schabi.newpipe.extractor.utils.JsonUtils; @@ -112,7 +113,11 @@ public List getVideoStreams() throws ExtractionException { null, AudioStream.UNKNOWN_BITRATE, new VideoAudioFormatRegistry().getFromMimeType(o.getString("mime_type")), - o.getInt("height") + "p" + new VideoQualityData( + o.getInt("height", VideoQualityData.UNKNOWN), + o.getInt("width", VideoQualityData.UNKNOWN), + VideoQualityData.UNKNOWN + ) )) .collect(Collectors.toList()); } @@ -139,8 +144,7 @@ public void onFetchPage(@Nonnull final Downloader downloader) conferenceData = JsonParser.object() .from(downloader.get(data.getString("conference_url")).responseBody()); } catch (final JsonParserException jpe) { - throw new ExtractionException("Could not parse json returned by URL: " + videoUrl, - jpe); + throw new ExtractionException("Could not parse json returned by URL: " + videoUrl, jpe); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index 6ed0199517..7314bf90cf 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -35,6 +35,7 @@ import org.schabi.newpipe.extractor.streamdata.stream.Stream; import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream; import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleSubtitleStreamImpl; import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl; @@ -465,6 +466,7 @@ private void extractLiveVideoStreams() throws ParsingException { .stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) + // TODO Check! This is the master playlist! .map(stream -> new SimpleVideoAudioStreamImpl( new SimpleHLSDeliveryDataImpl(stream.getString(PLAYLIST_URL, "")), VideoAudioFormatRegistry.MPEG_4) @@ -489,9 +491,10 @@ private void addStreamsFromArray( .map(JsonObject.class::cast) .filter(stream -> !isNullOrEmpty(getUrlFromStream(stream))) .forEach(stream -> { - final String resolution = getResolutionFromStream(stream); - - if (resolution.toLowerCase().contains("audio")) { + final JsonObject resJson = stream.getObject("resolution"); + if (resJson.getString("label", "") + .toLowerCase() + .contains("audio")) { // An audio stream addNewStreams( this.audioStreams, @@ -514,17 +517,15 @@ private void addStreamsFromArray( dd, new VideoAudioFormatRegistry() .getFromSuffix(getExtensionFromStream(s)), - resolution + new VideoQualityData( + resJson.getInt("id", VideoQualityData.UNKNOWN), + stream.getInt("fps", VideoQualityData.UNKNOWN)) ) ); } }); } - private static String getResolutionFromStream(@Nonnull final JsonObject stream) { - return stream.getObject("resolution") - .getString("label"); - } private static String getStreamUrlKeyFromStream(@Nonnull final JsonObject stream) { return stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java index 63b298bfcc..902f81f0af 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java @@ -1,29 +1,23 @@ package org.schabi.newpipe.extractor.streamdata.stream; import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; import java.util.Objects; -import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * Represents a video (only) stream. */ public interface VideoStream extends Stream { - String UNKNOWN_RESOLUTION = ""; - // TODO: Check if this can be non-null @Nullable default VideoAudioMediaFormat videoMediaFormat() { return null; } - // TODO: This should be a separate entity (containing e.g. height x width + fps) - @Nonnull - default String resolution() { - return UNKNOWN_RESOLUTION; - } + VideoQualityData videoQualityData(); @Override default boolean equalsStream(@Nullable final Stream other) { @@ -33,6 +27,6 @@ default boolean equalsStream(@Nullable final Stream other) { final VideoStream otherVideoStream = (VideoStream) other; return Objects.equals(videoMediaFormat(), otherVideoStream.videoMediaFormat()) - && Objects.equals(resolution(), otherVideoStream.resolution()); + && videoQualityData().equalsVideoQualityData(otherVideoStream.videoQualityData()); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java new file mode 100644 index 0000000000..33b94fe2f4 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java @@ -0,0 +1,46 @@ +package org.schabi.newpipe.extractor.streamdata.stream.quality; + +public class VideoQualityData { + public static int UNKNOWN = -1; + + private final int height; + private final int width; + private final int fps; + + public VideoQualityData(final int height, final int width, final int fps) { + this.height = height; + this.width = width; + this.fps = fps; + } + + public VideoQualityData(final int height, final int fps) { + this(height, UNKNOWN, fps); + } + + public VideoQualityData(final int height) { + this(height, UNKNOWN); + } + + public VideoQualityData() { + this(UNKNOWN); + } + + + public int height() { + return height; + } + + public int width() { + return width; + } + + public int fps() { + return fps; + } + + public boolean equalsVideoQualityData(final VideoQualityData other) { + return height() == other.height() + && width() == other.width() + && fps() == other.fps(); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java index 5986c1a0b1..3f4d10dba7 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java @@ -4,6 +4,7 @@ import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; import java.util.Objects; @@ -18,35 +19,35 @@ public class SimpleVideoAudioStreamImpl extends AbstractStreamImpl implements Vi @Nullable private final VideoAudioMediaFormat videoAudioMediaFormat; @Nonnull - private final String resolution; + private final VideoQualityData videoQualityData; public SimpleVideoAudioStreamImpl( @Nonnull final DeliveryData deliveryData, @Nullable final AudioMediaFormat audioMediaFormat, final int averageBitrate, @Nullable final VideoAudioMediaFormat videoAudioMediaFormat, - @Nonnull final String resolution + @Nonnull final VideoQualityData videoQualityData ) { super(deliveryData); this.audioMediaFormat = audioMediaFormat; this.averageBitrate = averageBitrate; this.videoAudioMediaFormat = videoAudioMediaFormat; - this.resolution = Objects.requireNonNull(resolution); + this.videoQualityData = Objects.requireNonNull(videoQualityData); } public SimpleVideoAudioStreamImpl( @Nonnull final DeliveryData deliveryData, @Nullable final VideoAudioMediaFormat videoAudioMediaFormat, - @Nonnull final String resolution + @Nonnull final VideoQualityData videoQualityData ) { - this(deliveryData, null, UNKNOWN_BITRATE, videoAudioMediaFormat, resolution); + this(deliveryData, null, UNKNOWN_BITRATE, videoAudioMediaFormat, videoQualityData); } public SimpleVideoAudioStreamImpl( @Nonnull final DeliveryData deliveryData, @Nullable final VideoAudioMediaFormat videoAudioMediaFormat ) { - this(deliveryData, videoAudioMediaFormat, UNKNOWN_RESOLUTION); + this(deliveryData, videoAudioMediaFormat, new VideoQualityData()); } @Nullable @@ -68,7 +69,7 @@ public VideoAudioMediaFormat videoMediaFormat() { @Nonnull @Override - public String resolution() { - return resolution; + public VideoQualityData videoQualityData() { + return videoQualityData; } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java index 247099216f..99d24160b2 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java @@ -3,6 +3,7 @@ import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; import org.schabi.newpipe.extractor.streamdata.stream.VideoStream; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; import java.util.Objects; @@ -13,27 +14,27 @@ public class SimpleVideoStreamImpl extends AbstractStreamImpl implements VideoSt @Nullable private final VideoAudioMediaFormat videoAudioMediaFormat; @Nonnull - private final String resolution; + private final VideoQualityData videoQualityData; public SimpleVideoStreamImpl( @Nonnull final DeliveryData deliveryData, @Nullable final VideoAudioMediaFormat videoAudioMediaFormat, - @Nonnull final String resolution + @Nonnull final VideoQualityData videoQualityData ) { super(deliveryData); this.videoAudioMediaFormat = videoAudioMediaFormat; - this.resolution = Objects.requireNonNull(resolution); + this.videoQualityData = Objects.requireNonNull(videoQualityData); } public SimpleVideoStreamImpl( @Nonnull final DeliveryData deliveryData, - @Nonnull final String resolution + @Nonnull final VideoQualityData videoQualityData ) { - this(deliveryData, null, resolution); + this(deliveryData, null, videoQualityData); } public SimpleVideoStreamImpl(@Nonnull final DeliveryData deliveryData) { - this(deliveryData, null, UNKNOWN_RESOLUTION); + this(deliveryData, new VideoQualityData()); } @Nullable @@ -44,7 +45,7 @@ public VideoAudioMediaFormat videoMediaFormat() { @Nonnull @Override - public String resolution() { - return resolution; + public VideoQualityData videoQualityData() { + return videoQualityData; } } From 9055f55270d25cab8f8a3a19e9159f77594355a4 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 5 Jun 2022 17:04:18 +0200 Subject: [PATCH 06/43] Reordered code inside StreamInfo + Added dashMpdUrl and hlsMasterPlaylistUrl --- .../org/schabi/newpipe/extractor/Info.java | 12 +- .../newpipe/extractor/stream/StreamInfo.java | 812 +++++++++--------- 2 files changed, 427 insertions(+), 397 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/Info.java b/extractor/src/main/java/org/schabi/newpipe/extractor/Info.java index 78a15553b1..83a5707243 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/Info.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/Info.java @@ -42,11 +42,11 @@ public void addAllErrors(final Collection throwables) { this.errors.addAll(throwables); } - public Info(final int serviceId, - final String id, - final String url, - final String originalUrl, - final String name) { + protected Info(final int serviceId, + final String id, + final String url, + final String originalUrl, + final String name) { this.serviceId = serviceId; this.id = id; this.url = url; @@ -54,7 +54,7 @@ public Info(final int serviceId, this.name = name; } - public Info(final int serviceId, final LinkHandler linkHandler, final String name) { + protected Info(final int serviceId, final LinkHandler linkHandler, final String name) { this(serviceId, linkHandler.getId(), linkHandler.getUrl(), diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index 63546b98a6..5bb615b4b9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -51,11 +51,51 @@ */ public class StreamInfo extends Info { - public static class StreamExtractException extends ExtractionException { - StreamExtractException(final String message) { - super(message); - } - } + private StreamType streamType; + private String thumbnailUrl = ""; + private String textualUploadDate; + private DateWrapper uploadDate; + private long duration = -1; + private int ageLimit; + private Description description; + + private long viewCount = -1; + private long likeCount = -1; + private long dislikeCount = -1; + + private String uploaderName = ""; + private String uploaderUrl = ""; + private String uploaderAvatarUrl = ""; + private boolean uploaderVerified = false; + private long uploaderSubscriberCount = -1; + + private String subChannelName = ""; + private String subChannelUrl = ""; + private String subChannelAvatarUrl = ""; + + @Nonnull + private String dashMpdUrl = ""; + @Nonnull + private String hlsMasterPlaylistUrl = ""; + + private List videoStreams = new ArrayList<>(); + private List audioStreams = new ArrayList<>(); + private List videoOnlyStreams = new ArrayList<>(); + + private List relatedItems = new ArrayList<>(); + + private long startPosition = 0; + private List subtitles = new ArrayList<>(); + + private String host = ""; + private StreamExtractor.Privacy privacy; + private String category = ""; + private String licence = ""; + private String supportInfo = ""; + private Locale language = null; + private List tags = new ArrayList<>(); + private List streamSegments = new ArrayList<>(); + private List metaInfo = new ArrayList<>(); public StreamInfo(final int serviceId, final String url, @@ -69,414 +109,102 @@ public StreamInfo(final int serviceId, this.ageLimit = ageLimit; } - public static StreamInfo getInfo(final String url) throws IOException, ExtractionException { - return getInfo(NewPipe.getServiceByUrl(url), url); + /** + * Preview frames, e.g. for the storyboard / seekbar thumbnail preview + */ + private List previewFrames = Collections.emptyList(); + + /** + * Get the stream type + * + * @return the stream type + */ + public StreamType getStreamType() { + return streamType; } - public static StreamInfo getInfo(@Nonnull final StreamingService service, - final String url) throws IOException, ExtractionException { - return getInfo(service.getStreamExtractor(url)); + public void setStreamType(final StreamType streamType) { + this.streamType = streamType; } - public static StreamInfo getInfo(@Nonnull final StreamExtractor extractor) - throws ExtractionException, IOException { - extractor.fetchPage(); - final StreamInfo streamInfo; - try { - streamInfo = extractImportantData(extractor); - extractStreams(streamInfo, extractor); - extractOptionalData(streamInfo, extractor); - return streamInfo; + /** + * Get the thumbnail url + * + * @return the thumbnail url as a string + */ + public String getThumbnailUrl() { + return thumbnailUrl; + } - } catch (final ExtractionException e) { - // Currently, YouTube does not distinguish between age restricted videos and videos - // blocked by country. This means that during the initialisation of the extractor, the - // extractor will assume that a video is age restricted while in reality it is blocked - // by country. - // - // We will now detect whether the video is blocked by country or not. + public void setThumbnailUrl(final String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + } - final String errorMessage = extractor.getErrorMessage(); - if (isNullOrEmpty(errorMessage)) { - throw e; - } else { - throw new ContentNotAvailableException(errorMessage, e); - } - } + public String getTextualUploadDate() { + return textualUploadDate; } - @Nonnull - private static StreamInfo extractImportantData(@Nonnull final StreamExtractor extractor) - throws ExtractionException { - // Important data, without it the content can't be displayed. - // If one of these is not available, the frontend will receive an exception directly. + public void setTextualUploadDate(final String textualUploadDate) { + this.textualUploadDate = textualUploadDate; + } - final int serviceId = extractor.getServiceId(); - final String url = extractor.getUrl(); - final String originalUrl = extractor.getOriginalUrl(); - final StreamType streamType = extractor.getStreamType(); - final String id = extractor.getId(); - final String name = extractor.getName(); - final int ageLimit = extractor.getAgeLimit(); + public DateWrapper getUploadDate() { + return uploadDate; + } - // Suppress always-non-null warning as here we double-check it really is not null - //noinspection ConstantConditions - if (streamType == StreamType.NONE - || isNullOrEmpty(url) - || isNullOrEmpty(id) - || name == null /* but it can be empty of course */ - || ageLimit == -1) { - throw new ExtractionException("Some important stream information was not given."); - } + public void setUploadDate(final DateWrapper uploadDate) { + this.uploadDate = uploadDate; + } - return new StreamInfo(extractor.getServiceId(), url, extractor.getOriginalUrl(), - streamType, id, name, ageLimit); + /** + * Get the duration in seconds + * + * @return the duration in seconds + */ + public long getDuration() { + return duration; } + public void setDuration(final long duration) { + this.duration = duration; + } - private static void extractStreams(final StreamInfo streamInfo, - final StreamExtractor extractor) - throws ExtractionException { - /* ---- Stream extraction goes here ---- */ - // At least one type of stream has to be available, otherwise an exception will be thrown - // directly into the frontend. + public int getAgeLimit() { + return ageLimit; + } - /* Load and extract audio */ - try { - streamInfo.setAudioStreams(extractor.getAudioStreams()); - } catch (final ContentNotSupportedException e) { - throw e; - } catch (final Exception e) { - streamInfo.addError(new ExtractionException("Couldn't get audio streams", e)); - } + public void setAgeLimit(final int ageLimit) { + this.ageLimit = ageLimit; + } - /* Extract video stream url */ - try { - streamInfo.setVideoStreams(extractor.getVideoStreams()); - } catch (final Exception e) { - streamInfo.addError(new ExtractionException("Couldn't get video streams", e)); - } + public Description getDescription() { + return description; + } - /* Extract video only stream url */ - try { - streamInfo.setVideoOnlyStreams(extractor.getVideoOnlyStreams()); - } catch (final Exception e) { - streamInfo.addError(new ExtractionException("Couldn't get video only streams", e)); - } + public void setDescription(final Description description) { + this.description = description; + } - // Lists can be null if an exception was thrown during extraction - if (streamInfo.getVideoStreams() == null) { - streamInfo.setVideoStreams(Collections.emptyList()); - } - if (streamInfo.getVideoOnlyStreams() == null) { - streamInfo.setVideoOnlyStreams(Collections.emptyList()); - } - if (streamInfo.getAudioStreams() == null) { - streamInfo.setAudioStreams(Collections.emptyList()); - } + public long getViewCount() { + return viewCount; + } - // Either audio or video has to be available, otherwise we didn't get a stream (since - // videoOnly are optional, they don't count). - if ((streamInfo.videoStreams.isEmpty()) && (streamInfo.audioStreams.isEmpty())) { - throw new StreamExtractException( - "Could not get any stream. See error variable to get further details."); - } + public void setViewCount(final long viewCount) { + this.viewCount = viewCount; } - @SuppressWarnings("MethodLength") - private static void extractOptionalData(final StreamInfo streamInfo, - final StreamExtractor extractor) { - /* ---- Optional data goes here: ---- */ - // If one of these fails, the frontend needs to handle that they are not available. - // Exceptions are therefore not thrown into the frontend, but stored into the error list, - // so the frontend can afterwards check where errors happened. + /** + * Get the number of likes. + * + * @return The number of likes or -1 if this information is not available + */ + public long getLikeCount() { + return likeCount; + } - try { - streamInfo.setThumbnailUrl(extractor.getThumbnailUrl()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setDuration(extractor.getLength()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setUploaderName(extractor.getUploaderName()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setUploaderUrl(extractor.getUploaderUrl()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setUploaderAvatarUrl(extractor.getUploaderAvatarUrl()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setUploaderVerified(extractor.isUploaderVerified()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setUploaderSubscriberCount(extractor.getUploaderSubscriberCount()); - } catch (final Exception e) { - streamInfo.addError(e); - } - - try { - streamInfo.setSubChannelName(extractor.getSubChannelName()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setSubChannelUrl(extractor.getSubChannelUrl()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setSubChannelAvatarUrl(extractor.getSubChannelAvatarUrl()); - } catch (final Exception e) { - streamInfo.addError(e); - } - - try { - streamInfo.setDescription(extractor.getDescription()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setViewCount(extractor.getViewCount()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setTextualUploadDate(extractor.getTextualUploadDate()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setUploadDate(extractor.getUploadDate()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setStartPosition(extractor.getTimeStamp()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setLikeCount(extractor.getLikeCount()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setDislikeCount(extractor.getDislikeCount()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setSubtitles(extractor.getSubtitles()); - } catch (final Exception e) { - streamInfo.addError(e); - } - - // Additional info - try { - streamInfo.setHost(extractor.getHost()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setPrivacy(extractor.getPrivacy()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setCategory(extractor.getCategory()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setLicence(extractor.getLicence()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setLanguageInfo(extractor.getLanguageInfo()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setTags(extractor.getTags()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setSupportInfo(extractor.getSupportInfo()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setStreamSegments(extractor.getStreamSegments()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setMetaInfo(extractor.getMetaInfo()); - } catch (final Exception e) { - streamInfo.addError(e); - } - try { - streamInfo.setPreviewFrames(extractor.getFrames()); - } catch (final Exception e) { - streamInfo.addError(e); - } - - streamInfo.setRelatedItems(ExtractorHelper.getRelatedItemsOrLogError(streamInfo, - extractor)); - } - - private StreamType streamType; - private String thumbnailUrl = ""; - private String textualUploadDate; - private DateWrapper uploadDate; - private long duration = -1; - private int ageLimit; - private Description description; - - private long viewCount = -1; - private long likeCount = -1; - private long dislikeCount = -1; - - private String uploaderName = ""; - private String uploaderUrl = ""; - private String uploaderAvatarUrl = ""; - private boolean uploaderVerified = false; - private long uploaderSubscriberCount = -1; - - private String subChannelName = ""; - private String subChannelUrl = ""; - private String subChannelAvatarUrl = ""; - - private List videoStreams = new ArrayList<>(); - private List audioStreams = new ArrayList<>(); - private List videoOnlyStreams = new ArrayList<>(); - - private List relatedItems = new ArrayList<>(); - - private long startPosition = 0; - private List subtitles = new ArrayList<>(); - - private String host = ""; - private StreamExtractor.Privacy privacy; - private String category = ""; - private String licence = ""; - private String supportInfo = ""; - private Locale language = null; - private List tags = new ArrayList<>(); - private List streamSegments = new ArrayList<>(); - private List metaInfo = new ArrayList<>(); - - /** - * Preview frames, e.g. for the storyboard / seekbar thumbnail preview - */ - private List previewFrames = Collections.emptyList(); - - /** - * Get the stream type - * - * @return the stream type - */ - public StreamType getStreamType() { - return streamType; - } - - public void setStreamType(final StreamType streamType) { - this.streamType = streamType; - } - - /** - * Get the thumbnail url - * - * @return the thumbnail url as a string - */ - public String getThumbnailUrl() { - return thumbnailUrl; - } - - public void setThumbnailUrl(final String thumbnailUrl) { - this.thumbnailUrl = thumbnailUrl; - } - - public String getTextualUploadDate() { - return textualUploadDate; - } - - public void setTextualUploadDate(final String textualUploadDate) { - this.textualUploadDate = textualUploadDate; - } - - public DateWrapper getUploadDate() { - return uploadDate; - } - - public void setUploadDate(final DateWrapper uploadDate) { - this.uploadDate = uploadDate; - } - - /** - * Get the duration in seconds - * - * @return the duration in seconds - */ - public long getDuration() { - return duration; - } - - public void setDuration(final long duration) { - this.duration = duration; - } - - public int getAgeLimit() { - return ageLimit; - } - - public void setAgeLimit(final int ageLimit) { - this.ageLimit = ageLimit; - } - - public Description getDescription() { - return description; - } - - public void setDescription(final Description description) { - this.description = description; - } - - public long getViewCount() { - return viewCount; - } - - public void setViewCount(final long viewCount) { - this.viewCount = viewCount; - } - - /** - * Get the number of likes. - * - * @return The number of likes or -1 if this information is not available - */ - public long getLikeCount() { - return likeCount; - } - - public void setLikeCount(final long likeCount) { - this.likeCount = likeCount; - } + public void setLikeCount(final long likeCount) { + this.likeCount = likeCount; + } /** * Get the number of dislikes. @@ -582,6 +310,24 @@ public void setVideoOnlyStreams(@Nonnull final List videoOnlyStream this.videoOnlyStreams = Objects.requireNonNull(videoOnlyStreams); } + @Nonnull + public String getDashMpdUrl() { + return dashMpdUrl; + } + + public void setDashMpdUrl(@Nonnull final String dashMpdUrl) { + this.dashMpdUrl = Objects.requireNonNull(dashMpdUrl); + } + + @Nonnull + public String getHlsMasterPlaylistUrl() { + return hlsMasterPlaylistUrl; + } + + public void setHlsMasterPlaylistUrl(@Nonnull final String hlsMasterPlaylistUrl) { + this.hlsMasterPlaylistUrl = Objects.requireNonNull(hlsMasterPlaylistUrl); + } + public List getRelatedItems() { return relatedItems; } @@ -687,4 +433,288 @@ public void setPreviewFrames(final List previewFrames) { public List getMetaInfo() { return this.metaInfo; } + + public static StreamInfo getInfo(final String url) throws IOException, ExtractionException { + return getInfo(NewPipe.getServiceByUrl(url), url); + } + + public static StreamInfo getInfo(@Nonnull final StreamingService service, + final String url) throws IOException, ExtractionException { + return getInfo(service.getStreamExtractor(url)); + } + + public static StreamInfo getInfo(@Nonnull final StreamExtractor extractor) + throws ExtractionException, IOException { + extractor.fetchPage(); + + try { + final StreamInfo streamInfo = extractImportantData(extractor); + extractStreams(streamInfo, extractor); + extractOptionalData(streamInfo, extractor); + return streamInfo; + + } catch (final ExtractionException e) { + // Currently, YouTube does not distinguish between age restricted videos and videos + // blocked by country. This means that during the initialisation of the extractor, the + // extractor will assume that a video is age restricted while in reality it is blocked + // by country. + // + // We will now detect whether the video is blocked by country or not. + // TODO: An error message is not a valid indicator if the video a blocked in a country + final String errorMessage = extractor.getErrorMessage(); + if (isNullOrEmpty(errorMessage)) { + throw e; + } else { + throw new ContentNotAvailableException(errorMessage, e); + } + } + } + + @Nonnull + private static StreamInfo extractImportantData(@Nonnull final StreamExtractor extractor) + throws ExtractionException { + // Important data, without it the content can't be displayed. + // If one of these is not available, the frontend will receive an exception directly. + + extractor.getServiceId(); // Check if a exception is thrown + final String url = extractor.getUrl(); + extractor.getOriginalUrl(); // Check if a exception is thrown + final StreamType streamType = extractor.getStreamType(); + final String id = extractor.getId(); + final String name = extractor.getName(); + final int ageLimit = extractor.getAgeLimit(); + + // Suppress always-non-null warning as here we double-check it really is not null + //noinspection ConstantConditions + if (streamType == StreamType.NONE + || isNullOrEmpty(url) + || isNullOrEmpty(id) + || name == null /* but it can be empty of course */ + || ageLimit == -1) { + throw new ExtractionException("Some important stream information was not given."); + } + + return new StreamInfo(extractor.getServiceId(), url, extractor.getOriginalUrl(), + streamType, id, name, ageLimit); + } + + + private static void extractStreams(final StreamInfo streamInfo, + final StreamExtractor extractor) + throws ExtractionException { + /* ---- Stream extraction goes here ---- */ + // At least one type of stream has to be available, otherwise an exception will be thrown + // directly into the frontend. + + /* Load and extract audio */ + try { + streamInfo.setAudioStreams(extractor.getAudioStreams()); + } catch (final ContentNotSupportedException e) { + throw e; + } catch (final Exception e) { + streamInfo.addError(new ExtractionException("Couldn't get audio streams", e)); + } + + /* Extract video stream url */ + try { + streamInfo.setVideoStreams(extractor.getVideoStreams()); + } catch (final Exception e) { + streamInfo.addError(new ExtractionException("Couldn't get video streams", e)); + } + + /* Extract video only stream url */ + try { + streamInfo.setVideoOnlyStreams(extractor.getVideoOnlyStreams()); + } catch (final Exception e) { + streamInfo.addError(new ExtractionException("Couldn't get video only streams", e)); + } + + /* Extract DASH-MPD url */ + try { + streamInfo.setDashMpdUrl(extractor.getDashMpdUrl()); + } catch (final Exception e) { + streamInfo.addError(new ExtractionException("Couldn't get DASH-MPD url", e)); + } + + /* Extract hls master playlist url */ + try { + streamInfo.setHlsMasterPlaylistUrl(extractor.getHlsMasterPlaylistUrl()); + } catch (final Exception e) { + streamInfo.addError(new ExtractionException("Couldn't get HLS master playlist", e)); + } + + // Check if any data for streaming is available + if (streamInfo.getVideoStreams().isEmpty() + && streamInfo.getVideoOnlyStreams().isEmpty() + && streamInfo.getAudioStreams().isEmpty() + && streamInfo.getDashMpdUrl().trim().isEmpty() + && streamInfo.getHlsMasterPlaylistUrl().trim().isEmpty() + ) { + throw new StreamExtractException("Could not get any required streaming-data. " + + "See error variable to get further details."); + } + } + + @SuppressWarnings("MethodLength") + private static void extractOptionalData(final StreamInfo streamInfo, + final StreamExtractor extractor) { + /* ---- Optional data goes here: ---- */ + // If one of these fails, the frontend needs to handle that they are not available. + // Exceptions are therefore not thrown into the frontend, but stored into the error list, + // so the frontend can afterwards check where errors happened. + + try { + streamInfo.setThumbnailUrl(extractor.getThumbnailUrl()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setDuration(extractor.getLength()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setUploaderName(extractor.getUploaderName()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setUploaderUrl(extractor.getUploaderUrl()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setUploaderAvatarUrl(extractor.getUploaderAvatarUrl()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setUploaderVerified(extractor.isUploaderVerified()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setUploaderSubscriberCount(extractor.getUploaderSubscriberCount()); + } catch (final Exception e) { + streamInfo.addError(e); + } + + try { + streamInfo.setSubChannelName(extractor.getSubChannelName()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setSubChannelUrl(extractor.getSubChannelUrl()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setSubChannelAvatarUrl(extractor.getSubChannelAvatarUrl()); + } catch (final Exception e) { + streamInfo.addError(e); + } + + try { + streamInfo.setDescription(extractor.getDescription()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setViewCount(extractor.getViewCount()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setTextualUploadDate(extractor.getTextualUploadDate()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setUploadDate(extractor.getUploadDate()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setStartPosition(extractor.getTimeStamp()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setLikeCount(extractor.getLikeCount()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setDislikeCount(extractor.getDislikeCount()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setSubtitles(extractor.getSubtitles()); + } catch (final Exception e) { + streamInfo.addError(e); + } + + // Additional info + try { + streamInfo.setHost(extractor.getHost()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setPrivacy(extractor.getPrivacy()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setCategory(extractor.getCategory()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setLicence(extractor.getLicence()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setLanguageInfo(extractor.getLanguageInfo()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setTags(extractor.getTags()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setSupportInfo(extractor.getSupportInfo()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setStreamSegments(extractor.getStreamSegments()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setMetaInfo(extractor.getMetaInfo()); + } catch (final Exception e) { + streamInfo.addError(e); + } + try { + streamInfo.setPreviewFrames(extractor.getFrames()); + } catch (final Exception e) { + streamInfo.addError(e); + } + + streamInfo.setRelatedItems( + ExtractorHelper.getRelatedItemsOrLogError(streamInfo, extractor)); + } + + public static class StreamExtractException extends ExtractionException { + StreamExtractException(final String message) { + super(message); + } + } } From 489b0cd6159384ddd54fca51b35e5beef7c68fe4 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 5 Jun 2022 17:04:35 +0200 Subject: [PATCH 07/43] Added more TODOs --- .../schabi/newpipe/extractor/services/youtube/DeliveryType.java | 1 + 1 file changed, 1 insertion(+) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java index 17833dc5fc..5ce0010f8c 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java @@ -7,6 +7,7 @@ * It is different from {@link org.schabi.newpipe.extractor.stream.DeliveryMethod delivery methods}! *

*/ +// TODO: Kill public enum DeliveryType { /** From ca5a141846d045b24265ac343a666cf928a9f286 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 5 Jun 2022 21:18:29 +0200 Subject: [PATCH 08/43] Reworked VideoQualityData --- .../MediaCCCLiveStreamExtractor.java | 5 ++-- .../extractors/MediaCCCStreamExtractor.java | 6 ++-- .../extractors/PeertubeStreamExtractor.java | 2 +- .../stream/quality/VideoQualityData.java | 28 +++++++++++-------- .../SimpleVideoAudioStreamImpl.java | 2 +- .../simpleimpl/SimpleVideoStreamImpl.java | 2 +- 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 28424b2633..7ad137db97 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -202,10 +202,9 @@ public List getVideoStreams() throws IOException, ExtractionEx deliveryData, // TODO: This looks wrong new VideoAudioFormatRegistry().getFromSuffix(dto.getUrlKey()), - new VideoQualityData( + VideoQualityData.fromHeightWidth( /*height=*/videoSize.getInt(1, VideoQualityData.UNKNOWN), - /*width=*/videoSize.getInt(0, VideoQualityData.UNKNOWN), - VideoQualityData.UNKNOWN) + /*width=*/videoSize.getInt(0, VideoQualityData.UNKNOWN)) ); }) .collect(Collectors.toList()); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index 3d22717229..f884e7e4be 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -113,11 +113,9 @@ public List getVideoStreams() throws ExtractionException { null, AudioStream.UNKNOWN_BITRATE, new VideoAudioFormatRegistry().getFromMimeType(o.getString("mime_type")), - new VideoQualityData( + VideoQualityData.fromHeightWidth( o.getInt("height", VideoQualityData.UNKNOWN), - o.getInt("width", VideoQualityData.UNKNOWN), - VideoQualityData.UNKNOWN - ) + o.getInt("width", VideoQualityData.UNKNOWN)) )) .collect(Collectors.toList()); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index 7314bf90cf..b957b3dd06 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -517,7 +517,7 @@ private void addStreamsFromArray( dd, new VideoAudioFormatRegistry() .getFromSuffix(getExtensionFromStream(s)), - new VideoQualityData( + VideoQualityData.fromHeightFps( resJson.getInt("id", VideoQualityData.UNKNOWN), stream.getInt("fps", VideoQualityData.UNKNOWN)) ) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java index 33b94fe2f4..02022a2301 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java @@ -13,18 +13,6 @@ public VideoQualityData(final int height, final int width, final int fps) { this.fps = fps; } - public VideoQualityData(final int height, final int fps) { - this(height, UNKNOWN, fps); - } - - public VideoQualityData(final int height) { - this(height, UNKNOWN); - } - - public VideoQualityData() { - this(UNKNOWN); - } - public int height() { return height; @@ -43,4 +31,20 @@ public boolean equalsVideoQualityData(final VideoQualityData other) { && width() == other.width() && fps() == other.fps(); } + + public static VideoQualityData fromHeightWidth(final int height, final int width) { + return new VideoQualityData(height, width, UNKNOWN); + } + + public static VideoQualityData fromHeightFps(final int height, final int fps) { + return new VideoQualityData(height, UNKNOWN, fps); + } + + public static VideoQualityData fromHeight(final int height) { + return new VideoQualityData(height, UNKNOWN, UNKNOWN); + } + + public static VideoQualityData fromUnknown() { + return new VideoQualityData(UNKNOWN, UNKNOWN, UNKNOWN); + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java index 3f4d10dba7..822b5dd22b 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java @@ -47,7 +47,7 @@ public SimpleVideoAudioStreamImpl( @Nonnull final DeliveryData deliveryData, @Nullable final VideoAudioMediaFormat videoAudioMediaFormat ) { - this(deliveryData, videoAudioMediaFormat, new VideoQualityData()); + this(deliveryData, videoAudioMediaFormat, VideoQualityData.fromUnknown()); } @Nullable diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java index 99d24160b2..9ce99603ae 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java @@ -34,7 +34,7 @@ public SimpleVideoStreamImpl( } public SimpleVideoStreamImpl(@Nonnull final DeliveryData deliveryData) { - this(deliveryData, new VideoQualityData()); + this(deliveryData, VideoQualityData.fromUnknown()); } @Nullable From f632fc98f8decc1f4c5f05e704be6376adf8775c Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 5 Jun 2022 21:18:51 +0200 Subject: [PATCH 09/43] Re-Implemented Itags --- .../youtube/itag/AudioItagFormat.java | 12 ++ .../services/youtube/itag/ItagFormat.java | 9 ++ .../youtube/itag/ItagFormatRegistry.java | 116 ++++++++++++++++++ .../youtube/itag/VideoAudioItagFormat.java | 5 + .../youtube/itag/VideoItagFormat.java | 14 +++ .../delivery/DASHItagFormatDeliveryData.java | 5 + .../delivery/HLSItagFormatDeliveryData.java | 5 + .../itag/delivery/ItagFormatDeliveryData.java | 5 + ...ProgressiveHTTPItagFormatDeliveryData.java | 5 + .../SimpleDASHItagFormatDeliveryData.java | 7 ++ .../SimpleHLSItagFormatDeliveryData.java | 7 ++ .../SimpleItagDeliveryDataBuilder.java | 23 ++++ ...ProgressiveHTTPItagFormatDeliveryData.java | 7 ++ .../itag/simpleimpl/AbstractItagFormat.java | 30 +++++ .../simpleimpl/SimpleAudioItagFormat.java | 40 ++++++ .../SimpleVideoAudioItagFormat.java | 35 ++++++ .../simpleimpl/SimpleVideoItagFormat.java | 46 +++++++ 17 files changed, 371 insertions(+) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/AudioItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormatRegistry.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoAudioItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/DASHItagFormatDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/HLSItagFormatDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ItagFormatDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ProgressiveHTTPItagFormatDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleDASHItagFormatDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleHLSItagFormatDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleItagDeliveryDataBuilder.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/AbstractItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleAudioItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoAudioItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoItagFormat.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/AudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/AudioItagFormat.java new file mode 100644 index 0000000000..13e1499200 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/AudioItagFormat.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.extractor.services.youtube.itag; + +import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; + +import javax.annotation.Nonnull; + +public interface AudioItagFormat extends ItagFormat { + @Nonnull + AudioMediaFormat audioMediaFormat(); + + int averageBitrate(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormat.java new file mode 100644 index 0000000000..a1e55672cb --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormat.java @@ -0,0 +1,9 @@ +package org.schabi.newpipe.extractor.services.youtube.itag; + +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; + +public interface ItagFormat { + int id(); + + ItagFormatDeliveryData deliveryData(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormatRegistry.java new file mode 100644 index 0000000000..7640345066 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormatRegistry.java @@ -0,0 +1,116 @@ +package org.schabi.newpipe.extractor.services.youtube.itag; + +import static org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder.dash; +import static org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder.hls; +import static org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry.MPEG_4; +import static org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry.V3GPP; +import static org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry.WEBM; +import static org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData.fromHeight; +import static org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData.fromHeightFps; +import static org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData.fromHeightWidth; + +import org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl.SimpleAudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl.SimpleVideoAudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl.SimpleVideoItagFormat; +import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; + +/** + * https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1195 + */ +public final class ItagFormatRegistry { + + public static final VideoAudioItagFormat[] VIDEO_AUDIO_FORMATS = new VideoAudioItagFormat[] { + // v-- Video-codec: mp4v; Audio-codec: aac --v + new SimpleVideoAudioItagFormat(17, V3GPP, fromHeightWidth(144, 176), 24), + // v-- Video-codec: h264; Audio-codec: aac --v + new SimpleVideoAudioItagFormat(18, MPEG_4, fromHeightWidth(360, 640), 96), + new SimpleVideoAudioItagFormat(22, MPEG_4, fromHeightWidth(720, 1280), 192), + + // Note: According to yt-dl Itag 34 and 35 are flv-files + new SimpleVideoAudioItagFormat(34, MPEG_4, fromHeightWidth(360, 640), 128), + new SimpleVideoAudioItagFormat(35, MPEG_4, fromHeightWidth(480, 854), 128), + + // Itag 36 is no longer used because the height is unstable and it's not returned by YT + // see also: https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1204 + new SimpleVideoAudioItagFormat(37, MPEG_4, fromHeightWidth(1080, 1920), 192), + new SimpleVideoAudioItagFormat(38, MPEG_4, fromHeightWidth(3072, 4092), 192), + + // v-- Video-codec: vp8; Audio-codec: vorbis --v + new SimpleVideoAudioItagFormat(43, WEBM, fromHeightWidth(360, 640), 128), + new SimpleVideoAudioItagFormat(44, WEBM, fromHeightWidth(480, 854), 128), + new SimpleVideoAudioItagFormat(45, WEBM, fromHeightWidth(720, 1280), 192), + new SimpleVideoAudioItagFormat(46, WEBM, fromHeightWidth(1080, 1920), 192), + + // HLS (used for live streaming) + // v-- Video-codec: h264; Audio-codec: acc --v + new SimpleVideoAudioItagFormat(91, MPEG_4, fromHeight(144), 48, hls()), + new SimpleVideoAudioItagFormat(92, MPEG_4, fromHeight(240), 48, hls()), + new SimpleVideoAudioItagFormat(93, MPEG_4, fromHeight(360), 128, hls()), + new SimpleVideoAudioItagFormat(94, MPEG_4, fromHeight(480), 128, hls()), + new SimpleVideoAudioItagFormat(95, MPEG_4, fromHeight(720), 256, hls()), + new SimpleVideoAudioItagFormat(96, MPEG_4, fromHeight(1080), 256, hls()), + new SimpleVideoAudioItagFormat(132, MPEG_4, fromHeight(240), 48, hls()), + new SimpleVideoAudioItagFormat(151, MPEG_4, fromHeight(72), 24, hls()) + }; + + public static final AudioItagFormat[] AUDIO_FORMATS = new AudioItagFormat[] { + // DASH MP4 audio + // v-- Audio-codec: aac --v + new SimpleAudioItagFormat(139, AudioFormatRegistry.M4A, 48, dash()), + new SimpleAudioItagFormat(140, AudioFormatRegistry.M4A, 128, dash()), + new SimpleAudioItagFormat(141, AudioFormatRegistry.M4A, 256, dash()), + + // DASH WEBM audio + // v-- Audio-codec: vorbis --v + new SimpleAudioItagFormat(171, AudioFormatRegistry.WEBMA, 128, dash()), + new SimpleAudioItagFormat(172, AudioFormatRegistry.WEBMA, 256, dash()), + + // DASH WEBM audio with opus inside + // v-- Audio-codec: opus --v + new SimpleAudioItagFormat(249, AudioFormatRegistry.WEBMA_OPUS, 50, dash()), + new SimpleAudioItagFormat(250, AudioFormatRegistry.WEBMA_OPUS, 70, dash()), + new SimpleAudioItagFormat(251, AudioFormatRegistry.WEBMA_OPUS, 160, dash()) + }; + + public static final VideoItagFormat[] VIDEO_FORMATS = new VideoItagFormat[] { + // DASH MP4 video + // v-- Video-codec: h264 --v + new SimpleVideoItagFormat(133, MPEG_4, fromHeight(240), dash()), + new SimpleVideoItagFormat(134, MPEG_4, fromHeight(360), dash()), + new SimpleVideoItagFormat(135, MPEG_4, fromHeight(480), dash()), + new SimpleVideoItagFormat(136, MPEG_4, fromHeight(720), dash()), + new SimpleVideoItagFormat(137, MPEG_4, fromHeight(1080), dash()), + // Itag 138 has an unknown height and is ignored + new SimpleVideoItagFormat(160, MPEG_4, fromHeight(144), dash()), + new SimpleVideoItagFormat(212, MPEG_4, fromHeight(480), dash()), + new SimpleVideoItagFormat(298, MPEG_4, fromHeightFps(720, 60), dash()), + new SimpleVideoItagFormat(299, MPEG_4, fromHeightFps(1080, 60), dash()), + new SimpleVideoItagFormat(266, MPEG_4, fromHeight(2160), dash()), + + // DASH WEBM video + // v-- Video-codec: vp9 --v + new SimpleVideoItagFormat(278, WEBM, fromHeight(144), dash()), + new SimpleVideoItagFormat(242, WEBM, fromHeight(240), dash()), + new SimpleVideoItagFormat(243, WEBM, fromHeight(360), dash()), + // Itag 244, 245 and 246 are identical? + new SimpleVideoItagFormat(244, WEBM, fromHeight(480), dash()), + new SimpleVideoItagFormat(245, WEBM, fromHeight(480), dash()), + new SimpleVideoItagFormat(246, WEBM, fromHeight(480), dash()), + new SimpleVideoItagFormat(247, WEBM, fromHeight(720), dash()), + new SimpleVideoItagFormat(248, WEBM, fromHeight(1080), dash()), + new SimpleVideoItagFormat(271, WEBM, fromHeight(1440), dash()), + // Itag 272 is either 3840x2160 (RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug) + new SimpleVideoItagFormat(272, WEBM, fromHeight(2160), dash()), + + new SimpleVideoItagFormat(302, WEBM, fromHeightFps(720, 60), dash()), + new SimpleVideoItagFormat(303, WEBM, fromHeightFps(1080, 60), dash()), + new SimpleVideoItagFormat(308, WEBM, fromHeightFps(1440, 60), dash()), + new SimpleVideoItagFormat(312, WEBM, fromHeight(2160), dash()), + new SimpleVideoItagFormat(315, WEBM, fromHeightFps(2160, 60), dash()), + }; + + + private ItagFormatRegistry() { + // No impl + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoAudioItagFormat.java new file mode 100644 index 0000000000..2ac7932a03 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoAudioItagFormat.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.services.youtube.itag; + +public interface VideoAudioItagFormat extends VideoItagFormat { + int averageBitrate(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoItagFormat.java new file mode 100644 index 0000000000..31ad0e0bcd --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoItagFormat.java @@ -0,0 +1,14 @@ +package org.schabi.newpipe.extractor.services.youtube.itag; + +import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; + +import javax.annotation.Nonnull; + +public interface VideoItagFormat extends ItagFormat { + @Nonnull + VideoAudioMediaFormat videoMediaFormat(); + + @Nonnull + VideoQualityData videoQualityData(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/DASHItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/DASHItagFormatDeliveryData.java new file mode 100644 index 0000000000..26df3ff3c7 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/DASHItagFormatDeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.delivery; + +public interface DASHItagFormatDeliveryData extends ItagFormatDeliveryData { + // Just a marker interface for now +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/HLSItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/HLSItagFormatDeliveryData.java new file mode 100644 index 0000000000..aed19acaa2 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/HLSItagFormatDeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.delivery; + +public interface HLSItagFormatDeliveryData extends ItagFormatDeliveryData { + // Just a marker interface for now +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ItagFormatDeliveryData.java new file mode 100644 index 0000000000..c66bb41736 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ItagFormatDeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.delivery; + +public interface ItagFormatDeliveryData { + // Just a marker interface +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ProgressiveHTTPItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ProgressiveHTTPItagFormatDeliveryData.java new file mode 100644 index 0000000000..825f702f5c --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ProgressiveHTTPItagFormatDeliveryData.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.delivery; + +public interface ProgressiveHTTPItagFormatDeliveryData extends ItagFormatDeliveryData { + // Just a marker interface for now +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleDASHItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleDASHItagFormatDeliveryData.java new file mode 100644 index 0000000000..6f41ac7ad8 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleDASHItagFormatDeliveryData.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.DASHItagFormatDeliveryData; + +public class SimpleDASHItagFormatDeliveryData implements DASHItagFormatDeliveryData { + // Just a marker for now +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleHLSItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleHLSItagFormatDeliveryData.java new file mode 100644 index 0000000000..8fedc97336 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleHLSItagFormatDeliveryData.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.HLSItagFormatDeliveryData; + +public class SimpleHLSItagFormatDeliveryData implements HLSItagFormatDeliveryData { + // Just a marker for now +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleItagDeliveryDataBuilder.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleItagDeliveryDataBuilder.java new file mode 100644 index 0000000000..70f90a8e3d --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleItagDeliveryDataBuilder.java @@ -0,0 +1,23 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.DASHItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.HLSItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ProgressiveHTTPItagFormatDeliveryData; + +public final class SimpleItagDeliveryDataBuilder { + private SimpleItagDeliveryDataBuilder() { + // No impl + } + + public static ProgressiveHTTPItagFormatDeliveryData progressiveHTTP() { + return new SimpleProgressiveHTTPItagFormatDeliveryData(); + } + + public static HLSItagFormatDeliveryData hls() { + return new SimpleHLSItagFormatDeliveryData(); + } + + public static DASHItagFormatDeliveryData dash() { + return new SimpleDASHItagFormatDeliveryData(); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java new file mode 100644 index 0000000000..c01473bd3f --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ProgressiveHTTPItagFormatDeliveryData; + +public class SimpleProgressiveHTTPItagFormatDeliveryData implements ProgressiveHTTPItagFormatDeliveryData { + // Just a marker for now +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/AbstractItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/AbstractItagFormat.java new file mode 100644 index 0000000000..d0a69a991d --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/AbstractItagFormat.java @@ -0,0 +1,30 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.ItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder; + +public abstract class AbstractItagFormat implements ItagFormat { + + private final int id; + private final ItagFormatDeliveryData deliveryData; + + protected AbstractItagFormat(final int id, final ItagFormatDeliveryData deliveryData) { + this.id = id; + this.deliveryData = deliveryData; + } + + protected AbstractItagFormat(final int id) { + this(id, SimpleItagDeliveryDataBuilder.progressiveHTTP()); + } + + @Override + public int id() { + return id; + } + + @Override + public ItagFormatDeliveryData deliveryData() { + return deliveryData; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleAudioItagFormat.java new file mode 100644 index 0000000000..b5cbed9ba4 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleAudioItagFormat.java @@ -0,0 +1,40 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.AudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; + +import javax.annotation.Nonnull; + +public class SimpleAudioItagFormat extends AbstractItagFormat implements AudioItagFormat { + private final AudioMediaFormat audioMediaFormat; + private final int averageBitrate; + + public SimpleAudioItagFormat(final int id, + final AudioMediaFormat audioMediaFormat, + final int averageBitrate, + final ItagFormatDeliveryData deliveryData) { + super(id, deliveryData); + this.audioMediaFormat = audioMediaFormat; + this.averageBitrate = averageBitrate; + } + + public SimpleAudioItagFormat(final int id, + final AudioMediaFormat audioMediaFormat, + final int averageBitrate) { + super(id); + this.audioMediaFormat = audioMediaFormat; + this.averageBitrate = averageBitrate; + } + + @Nonnull + @Override + public AudioMediaFormat audioMediaFormat() { + return audioMediaFormat; + } + + @Override + public int averageBitrate() { + return averageBitrate; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoAudioItagFormat.java new file mode 100644 index 0000000000..30e86187fe --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoAudioItagFormat.java @@ -0,0 +1,35 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.VideoAudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; + +import javax.annotation.Nonnull; + +public class SimpleVideoAudioItagFormat extends SimpleVideoItagFormat + implements VideoAudioItagFormat { + private final int averageBitrate; + + public SimpleVideoAudioItagFormat(final int id, + @Nonnull final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final VideoQualityData videoQualityData, + final int averageBitrate, + @Nonnull final ItagFormatDeliveryData deliveryData) { + super(id, videoAudioMediaFormat, videoQualityData, deliveryData); + this.averageBitrate = averageBitrate; + } + + public SimpleVideoAudioItagFormat(final int id, + @Nonnull final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final VideoQualityData videoQualityData, + final int averageBitrate) { + super(id, videoAudioMediaFormat, videoQualityData); + this.averageBitrate = averageBitrate; + } + + @Override + public int averageBitrate() { + return averageBitrate; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoItagFormat.java new file mode 100644 index 0000000000..4da1d05af1 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoItagFormat.java @@ -0,0 +1,46 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.VideoItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; + +import java.util.Objects; + +import javax.annotation.Nonnull; + +public class SimpleVideoItagFormat extends AbstractItagFormat implements VideoItagFormat { + @Nonnull + private final VideoAudioMediaFormat videoAudioMediaFormat; + @Nonnull + private final VideoQualityData videoQualityData; + + public SimpleVideoItagFormat(final int id, + @Nonnull final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final VideoQualityData videoQualityData, + @Nonnull final ItagFormatDeliveryData deliveryData) { + super(id, deliveryData); + this.videoAudioMediaFormat = Objects.requireNonNull(videoAudioMediaFormat); + this.videoQualityData = Objects.requireNonNull(videoQualityData); + } + + public SimpleVideoItagFormat(final int id, + @Nonnull final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final VideoQualityData videoQualityData) { + super(id); + this.videoAudioMediaFormat = Objects.requireNonNull(videoAudioMediaFormat); + this.videoQualityData = Objects.requireNonNull(videoQualityData); + } + + @Nonnull + @Override + public VideoAudioMediaFormat videoMediaFormat() { + return videoAudioMediaFormat; + } + + @Nonnull + @Override + public VideoQualityData videoQualityData() { + return videoQualityData; + } +} From a77865b2eb00f13caf779bd3ea4f1ae7ef934eb3 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Mon, 6 Jun 2022 17:05:40 +0200 Subject: [PATCH 10/43] More refactoring + started implementing ItagInfo --- .../extractors/BandcampStreamExtractor.java | 2 +- .../MediaCCCLiveStreamExtractor.java | 6 +- .../extractors/MediaCCCStreamExtractor.java | 8 +- .../extractors/PeertubeStreamExtractor.java | 19 +- .../extractors/SoundcloudStreamExtractor.java | 50 +++- .../youtube/itag/AudioItagFormat.java | 12 - .../services/youtube/itag/ItagFormat.java | 9 - .../youtube/itag/VideoAudioItagFormat.java | 5 - .../youtube/itag/format/AudioItagFormat.java | 7 + .../itag/format/BaseAudioItagFormat.java | 8 + .../youtube/itag/format/ItagFormat.java | 21 ++ .../itag/format/VideoAudioItagFormat.java | 5 + .../itag/{ => format}/VideoItagFormat.java | 7 +- .../registry}/ItagFormatRegistry.java | 35 ++- .../simpleimpl/AbstractSimpleItagFormat.java | 46 +++ .../simpleimpl/SimpleAudioItagFormat.java | 30 ++ .../SimpleVideoAudioItagFormat.java | 12 +- .../simpleimpl/SimpleVideoItagFormat.java | 25 +- .../services/youtube/itag/info/ItagInfo.java | 278 ++++++++++++++++++ .../youtube/itag/info/ItagInfoRange.java | 19 ++ .../info/builder/ItagInfoRangeHelper.java | 24 ++ .../itag/simpleimpl/AbstractItagFormat.java | 30 -- .../simpleimpl/SimpleAudioItagFormat.java | 40 --- .../format/AbstractMediaFormat.java | 21 +- .../streamdata/format/AudioMediaFormat.java | 2 - .../streamdata/format/MediaFormat.java | 12 + .../format/registry/MediaFormatRegistry.java | 10 +- .../streamdata/stream/AudioStream.java | 22 +- .../streamdata/stream/BaseAudioStream.java | 25 ++ .../extractor/streamdata/stream/Stream.java | 12 +- .../streamdata/stream/SubtitleStream.java | 7 +- .../streamdata/stream/VideoAudioStream.java | 4 +- .../streamdata/stream/VideoStream.java | 10 +- .../stream/simpleimpl/AbstractStreamImpl.java | 16 +- .../simpleimpl/SimpleAudioStreamImpl.java | 35 +-- .../simpleimpl/SimpleSubtitleStreamImpl.java | 16 +- .../SimpleVideoAudioStreamImpl.java | 53 ++-- .../simpleimpl/SimpleVideoStreamImpl.java | 27 +- 38 files changed, 669 insertions(+), 301 deletions(-) delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/AudioItagFormat.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormat.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoAudioItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/AudioItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/BaseAudioItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/ItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoAudioItagFormat.java rename extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/{ => format}/VideoItagFormat.java (58%) rename extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/{ => format/registry}/ItagFormatRegistry.java (83%) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/AbstractSimpleItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleAudioItagFormat.java rename extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/{ => format}/simpleimpl/SimpleVideoAudioItagFormat.java (78%) rename extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/{ => format}/simpleimpl/SimpleVideoItagFormat.java (61%) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/builder/ItagInfoRangeHelper.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/AbstractItagFormat.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleAudioItagFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/MediaFormat.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/BaseAudioStream.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java index f4b75b731f..14bfed226e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java @@ -146,12 +146,12 @@ public Description getDescription() { public List getAudioStreams() { return Collections.singletonList( new SimpleAudioStreamImpl( + AudioFormatRegistry.MP3, new SimpleProgressiveHTTPDeliveryDataImpl(albumJson .getArray("trackinfo") .getObject(0) .getObject("file") .getString("mp3-128")), - AudioFormatRegistry.MP3, 128 ) ); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 7ad137db97..481a66bc93 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -174,9 +174,9 @@ public List getAudioStreams() throws IOException, ExtractionExcepti } return new SimpleAudioStreamImpl( - deliveryData, // TODO: This looks wrong - new AudioFormatRegistry().getFromSuffix(dto.getUrlKey()) + new AudioFormatRegistry().getFromSuffix(dto.getUrlKey()), + deliveryData ); }) .collect(Collectors.toList()); @@ -199,9 +199,9 @@ public List getVideoStreams() throws IOException, ExtractionEx final JsonArray videoSize = dto.getStreamJsonObj().getArray("videoSize"); return new SimpleVideoAudioStreamImpl( - deliveryData, // TODO: This looks wrong new VideoAudioFormatRegistry().getFromSuffix(dto.getUrlKey()), + deliveryData, VideoQualityData.fromHeightWidth( /*height=*/videoSize.getInt(1, VideoQualityData.UNKNOWN), /*width=*/videoSize.getInt(0, VideoQualityData.UNKNOWN)) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index f884e7e4be..b82868c112 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -99,8 +99,8 @@ public String getUploaderAvatarUrl() { public List getAudioStreams() throws ExtractionException { return getRecordingsByMimeType("audio") .map(o -> new SimpleAudioStreamImpl( - new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url")), - new AudioFormatRegistry().getFromMimeType(o.getString("mime_type")) + new AudioFormatRegistry().getFromMimeType(o.getString("mime_type")), + new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url")) )) .collect(Collectors.toList()); } @@ -109,10 +109,8 @@ public List getAudioStreams() throws ExtractionException { public List getVideoStreams() throws ExtractionException { return getRecordingsByMimeType("video") .map(o -> new SimpleVideoAudioStreamImpl( - new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url")), - null, - AudioStream.UNKNOWN_BITRATE, new VideoAudioFormatRegistry().getFromMimeType(o.getString("mime_type")), + new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url")), VideoQualityData.fromHeightWidth( o.getInt("height", VideoQualityData.UNKNOWN), o.getInt("width", VideoQualityData.UNKNOWN)) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index b957b3dd06..8ca48ee4da 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -402,14 +402,15 @@ private void tryExtractSubtitles() { .map(JsonObject.class::cast) .map(caption -> { try { - final String url = baseUrl + JsonUtils.getString(caption, - "captionPath"); + final String url = + baseUrl + JsonUtils.getString(caption, "captionPath"); return new SimpleSubtitleStreamImpl( - new SimpleProgressiveHTTPDeliveryDataImpl(url), + // TODO: Check for null new SubtitleFormatRegistry() .getFromSuffix( url.substring(url.lastIndexOf(".") + 1)), + new SimpleProgressiveHTTPDeliveryDataImpl(url), false, JsonUtils.getString(caption, "language.id") ); @@ -467,9 +468,9 @@ private void extractLiveVideoStreams() throws ParsingException { .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) // TODO Check! This is the master playlist! - .map(stream -> new SimpleVideoAudioStreamImpl( - new SimpleHLSDeliveryDataImpl(stream.getString(PLAYLIST_URL, "")), - VideoAudioFormatRegistry.MPEG_4) + .map(s -> new SimpleVideoAudioStreamImpl( + VideoAudioFormatRegistry.MPEG_4, + new SimpleHLSDeliveryDataImpl(s.getString(PLAYLIST_URL, ""))) ) // Don't use the containsSimilarStream method because it will always // return @@ -501,9 +502,9 @@ private void addStreamsFromArray( stream, playlistUrl, (s, dd) -> new SimpleAudioStreamImpl( - dd, new AudioFormatRegistry() - .getFromSuffix(getExtensionFromStream(s)) + .getFromSuffix(getExtensionFromStream(s)), + dd ) ); @@ -514,9 +515,9 @@ private void addStreamsFromArray( stream, playlistUrl, (s, dd) -> new SimpleVideoAudioStreamImpl( - dd, new VideoAudioFormatRegistry() .getFromSuffix(getExtensionFromStream(s)), + dd, VideoQualityData.fromHeightFps( resJson.getInt("id", VideoQualityData.UNKNOWN), stream.getInt("fps", VideoQualityData.UNKNOWN)) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 8872c9495d..40572d35a0 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -14,6 +14,7 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; +import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; @@ -258,8 +259,8 @@ private List extractAudioStreams() { final String preset = transcoding.getString("preset", ""); - AudioMediaFormat mediaFormat = null; - int averageBitrate = AudioStream.UNKNOWN_BITRATE; + final AudioMediaFormat mediaFormat; + final int averageBitrate; if (preset.contains("mp3")) { // Don't add the MP3 HLS stream if there is a progressive stream present // because the two have the same bitrate @@ -271,13 +272,15 @@ private List extractAudioStreams() { } else if (preset.contains("opus")) { mediaFormat = AudioFormatRegistry.OPUS; averageBitrate = 64; + } else { + return null; } return (AudioStream) new SimpleAudioStreamImpl( + mediaFormat, protocol.equals("hls") ? new SimpleHLSDeliveryDataImpl(mediaUrl) : new SimpleProgressiveHTTPDeliveryDataImpl(mediaUrl), - mediaFormat, averageBitrate ); }) @@ -311,7 +314,17 @@ private Optional extractDownloadableFileIfAvailable() { if (isNullOrEmpty(downloadUrl)) { return Optional.empty(); } + + // Find out what type of file is served + final String fileType = determineFileTypeFromDownloadUrl(downloadUrl); + + // No fileType found -> ignore it + if (isNullOrEmpty(fileType)) { + return Optional.empty(); + } + return Optional.of(new SimpleAudioStreamImpl( + new AudioFormatRegistry().getFromSuffix(fileType), new SimpleProgressiveHTTPDeliveryDataImpl(downloadUrl) )); } catch (final Exception ignored) { @@ -321,6 +334,37 @@ private Optional extractDownloadableFileIfAvailable() { } } + /** + * Determines the file type/extension of the download url. + *

+ * Note: Uses HTTP FETCH for inspection. + *

+ */ + @Nullable + private String determineFileTypeFromDownloadUrl(final String downloadUrl) + throws IOException, ReCaptchaException { + + final Response response = NewPipe.getDownloader().head(downloadUrl); + + // As of 2022-06 Soundcloud uses AWS S3 + // Use the AWS header to identify the filetype first because it's simpler + final String amzMetaFileType = response.getHeader("x-amz-meta-file-type"); + if (!isNullOrEmpty(amzMetaFileType)) { + return amzMetaFileType; + } + + // If the AWS header was not present try extract the filetype + // by inspecting the download file name + // Example-Value: + // attachment;filename="SoundCloud%20Download"; filename*=utf-8''song.mp3 + final String contentDisp = response.getHeader("Content-Disposition"); + if (!isNullOrEmpty(contentDisp) && contentDisp.contains(".")) { + return contentDisp.substring(contentDisp.lastIndexOf(".") + 1); + } + + return null; + } + /** * Parses a SoundCloud HLS manifest to get a single URL of HLS streams. * diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/AudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/AudioItagFormat.java deleted file mode 100644 index 13e1499200..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/AudioItagFormat.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.itag; - -import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; - -import javax.annotation.Nonnull; - -public interface AudioItagFormat extends ItagFormat { - @Nonnull - AudioMediaFormat audioMediaFormat(); - - int averageBitrate(); -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormat.java deleted file mode 100644 index a1e55672cb..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormat.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.itag; - -import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; - -public interface ItagFormat { - int id(); - - ItagFormatDeliveryData deliveryData(); -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoAudioItagFormat.java deleted file mode 100644 index 2ac7932a03..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoAudioItagFormat.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.itag; - -public interface VideoAudioItagFormat extends VideoItagFormat { - int averageBitrate(); -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/AudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/AudioItagFormat.java new file mode 100644 index 0000000000..e4268a24f5 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/AudioItagFormat.java @@ -0,0 +1,7 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.format; + +import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; + +public interface AudioItagFormat extends ItagFormat, BaseAudioItagFormat { + // Nothing additional to implement +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/BaseAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/BaseAudioItagFormat.java new file mode 100644 index 0000000000..d1faef556f --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/BaseAudioItagFormat.java @@ -0,0 +1,8 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.format; + +public interface BaseAudioItagFormat { + /** + * Average audio bitrate in KBit/s + */ + int averageBitrate(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/ItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/ItagFormat.java new file mode 100644 index 0000000000..dc9136ee02 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/ItagFormat.java @@ -0,0 +1,21 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.format; + +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.MediaFormat; + +import javax.annotation.Nonnull; + +public interface ItagFormat { + int id(); + + /** + * The (container) media format, e.g. mp3 for audio streams or webm for video(+audio) streams. + * + * @return The (container) media format + */ + @Nonnull + M mediaFormat(); + + @Nonnull + ItagFormatDeliveryData deliveryData(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoAudioItagFormat.java new file mode 100644 index 0000000000..396cd51d4a --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoAudioItagFormat.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.format; + +public interface VideoAudioItagFormat extends VideoItagFormat, BaseAudioItagFormat { + // Nothing additional to implement +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoItagFormat.java similarity index 58% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoItagFormat.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoItagFormat.java index 31ad0e0bcd..366b351700 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/VideoItagFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoItagFormat.java @@ -1,14 +1,11 @@ -package org.schabi.newpipe.extractor.services.youtube.itag; +package org.schabi.newpipe.extractor.services.youtube.itag.format; import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; import javax.annotation.Nonnull; -public interface VideoItagFormat extends ItagFormat { - @Nonnull - VideoAudioMediaFormat videoMediaFormat(); - +public interface VideoItagFormat extends ItagFormat { @Nonnull VideoQualityData videoQualityData(); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java similarity index 83% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormatRegistry.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java index 7640345066..da36291f06 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/ItagFormatRegistry.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.extractor.services.youtube.itag; +package org.schabi.newpipe.extractor.services.youtube.itag.format.registry; import static org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder.dash; import static org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder.hls; @@ -9,17 +9,21 @@ import static org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData.fromHeightFps; import static org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData.fromHeightWidth; -import org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl.SimpleAudioItagFormat; -import org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl.SimpleVideoAudioItagFormat; -import org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl.SimpleVideoItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.AudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.ItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoAudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl.SimpleAudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl.SimpleVideoAudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl.SimpleVideoItagFormat; import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; -/** - * https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1195 - */ +import java.util.stream.Stream; + +// https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1195 public final class ItagFormatRegistry { - public static final VideoAudioItagFormat[] VIDEO_AUDIO_FORMATS = new VideoAudioItagFormat[] { + public static final VideoAudioItagFormat[] VIDEO_AUDIO_FORMATS = new VideoAudioItagFormat[]{ // v-- Video-codec: mp4v; Audio-codec: aac --v new SimpleVideoAudioItagFormat(17, V3GPP, fromHeightWidth(144, 176), 24), // v-- Video-codec: h264; Audio-codec: aac --v @@ -109,8 +113,21 @@ public final class ItagFormatRegistry { new SimpleVideoItagFormat(315, WEBM, fromHeightFps(2160, 60), dash()), }; - private ItagFormatRegistry() { // No impl } + + public static boolean isSupported(final int id) { + return Stream.of(VIDEO_AUDIO_FORMATS, AUDIO_FORMATS, VIDEO_FORMATS) + .flatMap(Stream::of) + .anyMatch(itagFormat -> itagFormat.id() == id); + } + + public static ItagFormat getById(final int id) { + return Stream.of(VIDEO_AUDIO_FORMATS, AUDIO_FORMATS, VIDEO_FORMATS) + .flatMap(Stream::of) + .filter(itagFormat -> itagFormat.id() == id) + .findFirst() + .orElse(null); + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/AbstractSimpleItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/AbstractSimpleItagFormat.java new file mode 100644 index 0000000000..0fad80bca4 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/AbstractSimpleItagFormat.java @@ -0,0 +1,46 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder; +import org.schabi.newpipe.extractor.services.youtube.itag.format.ItagFormat; +import org.schabi.newpipe.extractor.streamdata.format.MediaFormat; + +import java.util.Objects; + +import javax.annotation.Nonnull; + +public abstract class AbstractSimpleItagFormat implements ItagFormat { + + private final int id; + private final M mediaFormat; + private final ItagFormatDeliveryData deliveryData; + + protected AbstractSimpleItagFormat( + final int id, + final M mediaFormat, + final ItagFormatDeliveryData deliveryData) { + this.id = id; + this.mediaFormat = Objects.requireNonNull(mediaFormat); + this.deliveryData = Objects.requireNonNull(deliveryData); + } + + protected AbstractSimpleItagFormat(final int id, final M mediaFormat) { + this(id, mediaFormat, SimpleItagDeliveryDataBuilder.progressiveHTTP()); + } + + @Override + public int id() { + return id; + } + + @Nonnull + @Override + public M mediaFormat() { + return mediaFormat; + } + + @Override + public ItagFormatDeliveryData deliveryData() { + return deliveryData; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleAudioItagFormat.java new file mode 100644 index 0000000000..565b8de70f --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleAudioItagFormat.java @@ -0,0 +1,30 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl; + +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.format.AudioItagFormat; +import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; + +public class SimpleAudioItagFormat extends AbstractSimpleItagFormat + implements AudioItagFormat { + private final int averageBitrate; + + public SimpleAudioItagFormat(final int id, + final AudioMediaFormat mediaFormat, + final int averageBitrate, + final ItagFormatDeliveryData deliveryData) { + super(id, mediaFormat, deliveryData); + this.averageBitrate = averageBitrate; + } + + public SimpleAudioItagFormat(final int id, + final AudioMediaFormat mediaFormat, + final int averageBitrate) { + super(id, mediaFormat); + this.averageBitrate = averageBitrate; + } + + @Override + public int averageBitrate() { + return averageBitrate; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoAudioItagFormat.java similarity index 78% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoAudioItagFormat.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoAudioItagFormat.java index 30e86187fe..09dfd7ce16 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoAudioItagFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoAudioItagFormat.java @@ -1,7 +1,7 @@ -package org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl; +package org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl; -import org.schabi.newpipe.extractor.services.youtube.itag.VideoAudioItagFormat; import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoAudioItagFormat; import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; @@ -12,19 +12,19 @@ public class SimpleVideoAudioItagFormat extends SimpleVideoItagFormat private final int averageBitrate; public SimpleVideoAudioItagFormat(final int id, - @Nonnull final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final VideoAudioMediaFormat mediaFormat, @Nonnull final VideoQualityData videoQualityData, final int averageBitrate, @Nonnull final ItagFormatDeliveryData deliveryData) { - super(id, videoAudioMediaFormat, videoQualityData, deliveryData); + super(id, mediaFormat, videoQualityData, deliveryData); this.averageBitrate = averageBitrate; } public SimpleVideoAudioItagFormat(final int id, - @Nonnull final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final VideoAudioMediaFormat mediaFormat, @Nonnull final VideoQualityData videoQualityData, final int averageBitrate) { - super(id, videoAudioMediaFormat, videoQualityData); + super(id, mediaFormat, videoQualityData); this.averageBitrate = averageBitrate; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoItagFormat.java similarity index 61% rename from extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoItagFormat.java rename to extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoItagFormat.java index 4da1d05af1..82f12075fa 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleVideoItagFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoItagFormat.java @@ -1,7 +1,7 @@ -package org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl; +package org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl; -import org.schabi.newpipe.extractor.services.youtube.itag.VideoItagFormat; import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat; import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; @@ -9,35 +9,26 @@ import javax.annotation.Nonnull; -public class SimpleVideoItagFormat extends AbstractItagFormat implements VideoItagFormat { - @Nonnull - private final VideoAudioMediaFormat videoAudioMediaFormat; +public class SimpleVideoItagFormat extends AbstractSimpleItagFormat + implements VideoItagFormat { @Nonnull private final VideoQualityData videoQualityData; public SimpleVideoItagFormat(final int id, - @Nonnull final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final VideoAudioMediaFormat mediaFormat, @Nonnull final VideoQualityData videoQualityData, @Nonnull final ItagFormatDeliveryData deliveryData) { - super(id, deliveryData); - this.videoAudioMediaFormat = Objects.requireNonNull(videoAudioMediaFormat); + super(id, mediaFormat, deliveryData); this.videoQualityData = Objects.requireNonNull(videoQualityData); } public SimpleVideoItagFormat(final int id, - @Nonnull final VideoAudioMediaFormat videoAudioMediaFormat, + @Nonnull final VideoAudioMediaFormat mediaFormat, @Nonnull final VideoQualityData videoQualityData) { - super(id); - this.videoAudioMediaFormat = Objects.requireNonNull(videoAudioMediaFormat); + super(id, mediaFormat); this.videoQualityData = Objects.requireNonNull(videoQualityData); } - @Nonnull - @Override - public VideoAudioMediaFormat videoMediaFormat() { - return videoAudioMediaFormat; - } - @Nonnull @Override public VideoQualityData videoQualityData() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java new file mode 100644 index 0000000000..1eda56723c --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java @@ -0,0 +1,278 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.info; + +import org.schabi.newpipe.extractor.services.youtube.itag.format.BaseAudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.ItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; + +import java.util.Optional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class ItagInfo { + + @Nonnull + private final ItagFormat itagFormat; + + // TODO: Maybe generate the streamUrl on-demand and not always instantly? + @Nonnull + private final String streamUrl; + + // region Audio + + @Nullable + private Integer averageBitRate; + + @Nullable + private Integer audioSampleRate; + + @Nullable + private Integer audioChannels; + + // endregion + + // region Video + + @Nullable + private Integer width; + + @Nullable + private Integer height; + + @Nullable + private Integer fps; + + // endregion + + @Nullable + private Integer bitRate; + + @Nullable + private String quality; + + @Nullable + private String codec; + + @Nullable + private ItagInfoRange initRange; + + @Nullable + private ItagInfoRange indexRange; + + @Nullable + private Long contentLength; + + @Nullable + private Long approxDurationMs; + + @Nullable + private String type; + + // region live or post-live + + @Nullable + private Integer targetDurationSec; + + // endregion + + + public ItagInfo( + @Nonnull final ItagFormat itagFormat, + @Nonnull final String streamUrl) { + this.itagFormat = itagFormat; + this.streamUrl = streamUrl; + } + + // region Getters + Setters + + @Nonnull + public ItagFormat getItagFormat() { + return itagFormat; + } + + @Nonnull + public String getStreamUrl() { + return streamUrl; + } + + @Nullable + public Integer getAverageBitRate() { + return averageBitRate; + } + + public void setAverageBitRate(@Nullable final Integer averageBitRate) { + this.averageBitRate = averageBitRate; + } + + @Nullable + public Integer getAudioSampleRate() { + return audioSampleRate; + } + + public void setAudioSampleRate(@Nullable final Integer audioSampleRate) { + this.audioSampleRate = audioSampleRate; + } + + @Nullable + public Integer getAudioChannels() { + return audioChannels; + } + + public void setAudioChannels(@Nullable final Integer audioChannels) { + this.audioChannels = audioChannels; + } + + @Nullable + public Integer getWidth() { + return width; + } + + public void setWidth(@Nullable final Integer width) { + this.width = width; + } + + @Nullable + public Integer getHeight() { + return height; + } + + public void setHeight(@Nullable final Integer height) { + this.height = height; + } + + @Nullable + public Integer getFps() { + return fps; + } + + public void setFps(@Nullable final Integer fps) { + this.fps = fps; + } + + @Nullable + public Integer getBitRate() { + return bitRate; + } + + public void setBitRate(@Nullable final Integer bitRate) { + this.bitRate = bitRate; + } + + @Nullable + public String getQuality() { + return quality; + } + + public void setQuality(@Nullable final String quality) { + this.quality = quality; + } + + @Nullable + public String getCodec() { + return codec; + } + + public void setCodec(@Nullable final String codec) { + this.codec = codec; + } + + @Nullable + public ItagInfoRange getInitRange() { + return initRange; + } + + public void setInitRange(@Nullable final ItagInfoRange initRange) { + this.initRange = initRange; + } + + @Nullable + public ItagInfoRange getIndexRange() { + return indexRange; + } + + public void setIndexRange(@Nullable final ItagInfoRange indexRange) { + this.indexRange = indexRange; + } + + @Nullable + public Long getContentLength() { + return contentLength; + } + + public void setContentLength(@Nullable final Long contentLength) { + this.contentLength = contentLength; + } + + @Nullable + public Long getApproxDurationMs() { + return approxDurationMs; + } + + public void setApproxDurationMs(@Nullable final Long approxDurationMs) { + this.approxDurationMs = approxDurationMs; + } + + @Nullable + public String getType() { + return type; + } + + public void setType(@Nullable final String type) { + this.type = type; + } + + @Nullable + public Integer getTargetDurationSec() { + return targetDurationSec; + } + + public void setTargetDurationSec(@Nullable final Integer targetDurationSec) { + this.targetDurationSec = targetDurationSec; + } + + // endregion + + /** + * Returns the combined averageBitrate from the current information and {@link #itagFormat}. + * @return averageBitRate in KBit/s or -1 + */ + public int getCombinedAverageBitrate() { + if (averageBitRate != null) { + return (int) Math.round(averageBitRate / 1000d); + } + + if (itagFormat instanceof BaseAudioItagFormat) { + return ((BaseAudioItagFormat)itagFormat).averageBitrate(); + } + + return -1; + } + + /** + * Returns the combined video-quality data from the current information and {@link #itagFormat}. + * @return video-quality data + */ + @Nonnull + public VideoQualityData getCombinedVideoQualityData() { + final Optional optVideoItagFormatQualityData = + itagFormat instanceof VideoItagFormat + ? Optional.of(((VideoItagFormat) itagFormat).videoQualityData()) + : Optional.empty(); + + return new VideoQualityData( + Optional.ofNullable(height) + .orElse(optVideoItagFormatQualityData + .map(VideoQualityData::height) + .orElse(VideoQualityData.UNKNOWN)), + Optional.ofNullable(width) + .orElse(optVideoItagFormatQualityData + .map(VideoQualityData::width) + .orElse(VideoQualityData.UNKNOWN)), + Optional.ofNullable(fps) + .orElse(optVideoItagFormatQualityData + .map(VideoQualityData::fps) + .orElse(VideoQualityData.UNKNOWN)) + ); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java new file mode 100644 index 0000000000..2e38f0b115 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java @@ -0,0 +1,19 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.info; + +public class ItagInfoRange { + private final int start; + private final int end; + + public ItagInfoRange(final int start, final int end) { + this.start = start; + this.end = end; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/builder/ItagInfoRangeHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/builder/ItagInfoRangeHelper.java new file mode 100644 index 0000000000..aad2a8325f --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/builder/ItagInfoRangeHelper.java @@ -0,0 +1,24 @@ +package org.schabi.newpipe.extractor.services.youtube.itag.info.builder; + +import com.grack.nanojson.JsonObject; + +import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfoRange; + +import javax.annotation.Nonnull; + +public final class ItagInfoRangeHelper { + private ItagInfoRangeHelper() { + // No impl + } + + public static ItagInfoRange buildFrom(@Nonnull final JsonObject jsonRangeObj) { + try { + return new ItagInfoRange( + Integer.parseInt(jsonRangeObj.getString("start", "-1")), + Integer.parseInt(jsonRangeObj.getString("end", "-1")) + ); + } catch (final NumberFormatException ignored) { + return null; + } + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/AbstractItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/AbstractItagFormat.java deleted file mode 100644 index d0a69a991d..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/AbstractItagFormat.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl; - -import org.schabi.newpipe.extractor.services.youtube.itag.ItagFormat; -import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; -import org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder; - -public abstract class AbstractItagFormat implements ItagFormat { - - private final int id; - private final ItagFormatDeliveryData deliveryData; - - protected AbstractItagFormat(final int id, final ItagFormatDeliveryData deliveryData) { - this.id = id; - this.deliveryData = deliveryData; - } - - protected AbstractItagFormat(final int id) { - this(id, SimpleItagDeliveryDataBuilder.progressiveHTTP()); - } - - @Override - public int id() { - return id; - } - - @Override - public ItagFormatDeliveryData deliveryData() { - return deliveryData; - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleAudioItagFormat.java deleted file mode 100644 index b5cbed9ba4..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/simpleimpl/SimpleAudioItagFormat.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.itag.simpleimpl; - -import org.schabi.newpipe.extractor.services.youtube.itag.AudioItagFormat; -import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; -import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; - -import javax.annotation.Nonnull; - -public class SimpleAudioItagFormat extends AbstractItagFormat implements AudioItagFormat { - private final AudioMediaFormat audioMediaFormat; - private final int averageBitrate; - - public SimpleAudioItagFormat(final int id, - final AudioMediaFormat audioMediaFormat, - final int averageBitrate, - final ItagFormatDeliveryData deliveryData) { - super(id, deliveryData); - this.audioMediaFormat = audioMediaFormat; - this.averageBitrate = averageBitrate; - } - - public SimpleAudioItagFormat(final int id, - final AudioMediaFormat audioMediaFormat, - final int averageBitrate) { - super(id); - this.audioMediaFormat = audioMediaFormat; - this.averageBitrate = averageBitrate; - } - - @Nonnull - @Override - public AudioMediaFormat audioMediaFormat() { - return audioMediaFormat; - } - - @Override - public int averageBitrate() { - return averageBitrate; - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java index f765080077..adb770b120 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java @@ -2,7 +2,7 @@ import java.util.Objects; -public abstract class AbstractMediaFormat { +public abstract class AbstractMediaFormat implements MediaFormat { private final int id; private final String name; private final String suffix; @@ -20,19 +20,23 @@ protected AbstractMediaFormat( this.mimeType = mimeType; } - public int getId() { + @Override + public int id() { return id; } - public String getName() { + @Override + public String name() { return name; } - public String getSuffix() { + @Override + public String suffix() { return suffix; } - public String getMimeType() { + @Override + public String mimeType() { return mimeType; } @@ -41,11 +45,14 @@ public boolean equals(final Object o) { if (this == o) return true; if (!(o instanceof AbstractMediaFormat)) return false; final AbstractMediaFormat that = (AbstractMediaFormat) o; - return getId() == that.getId() && Objects.equals(getName(), that.getName()) && Objects.equals(getSuffix(), that.getSuffix()) && Objects.equals(getMimeType(), that.getMimeType()); + return id() == that.id() + && Objects.equals(name(), that.name()) + && Objects.equals(suffix(), that.suffix()) + && Objects.equals(mimeType(), that.mimeType()); } @Override public int hashCode() { - return Objects.hash(getId(), getName(), getSuffix(), getMimeType()); + return Objects.hash(id(), name(), suffix(), mimeType()); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java index a2fb322d24..8da6af9feb 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java @@ -9,6 +9,4 @@ public AudioMediaFormat( ) { super(id, name, suffix, mimeType); } - - } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/MediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/MediaFormat.java new file mode 100644 index 0000000000..bfea6ffd34 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/MediaFormat.java @@ -0,0 +1,12 @@ +package org.schabi.newpipe.extractor.streamdata.format; + +public interface MediaFormat { + + int id(); + + String name(); + + String suffix(); + + String mimeType(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java index 2a676b39fe..792da1acd6 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java @@ -24,7 +24,7 @@ public T getById(final int id, final Function field, final T orElse) { return Arrays.stream(values()) - .filter(mediaFormat -> mediaFormat.getId() == id) + .filter(mediaFormat -> mediaFormat.id() == id) .map(field) .findFirst() .orElse(orElse); @@ -39,7 +39,7 @@ public T getById(final int id, */ @Nonnull public String getNameById(final int id) { - return getById(id, AbstractMediaFormat::getName, ""); + return getById(id, AbstractMediaFormat::name, ""); } /** @@ -51,7 +51,7 @@ public String getNameById(final int id) { */ @Nullable public String getMimeById(final int id) { - return getById(id, AbstractMediaFormat::getMimeType, null); + return getById(id, AbstractMediaFormat::mimeType, null); } /** @@ -63,7 +63,7 @@ public String getMimeById(final int id) { @Nullable public F getFromMimeType(final String mimeType) { return Arrays.stream(values()) - .filter(mediaFormat -> mediaFormat.getMimeType().equals(mimeType)) + .filter(mediaFormat -> mediaFormat.mimeType().equals(mimeType)) .findFirst() .orElse(null); } @@ -71,7 +71,7 @@ public F getFromMimeType(final String mimeType) { @Nullable public F getFromSuffix(final String suffix) { return Arrays.stream(values()) - .filter(mediaFormat -> mediaFormat.getSuffix().equals(suffix)) + .filter(mediaFormat -> mediaFormat.suffix().equals(suffix)) .findFirst() .orElse(null); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java index 092efda509..b7290cb278 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java @@ -9,24 +9,8 @@ /** * Represents a audio (only) stream. */ -public interface AudioStream extends Stream { - int UNKNOWN_BITRATE = -1; - - // TODO: Check if this can be non-null - @Nullable - default AudioMediaFormat audioMediaFormat() { - return null; - } - - /** - * Get the average bitrate of the stream. - * - * @return the average bitrate or -1 if unknown - */ - default int averageBitrate() { - return UNKNOWN_BITRATE; - } - +public interface AudioStream extends Stream, BaseAudioStream { + @Override default boolean equalsStream(@Nullable final Stream other) { if (!(other instanceof AudioStream)) { @@ -34,7 +18,7 @@ default boolean equalsStream(@Nullable final Stream other) { } final AudioStream otherAudioStream = (AudioStream) other; - return Objects.equals(audioMediaFormat(), otherAudioStream.audioMediaFormat()) + return Objects.equals(mediaFormat(), otherAudioStream.mediaFormat()) && averageBitrate() == otherAudioStream.averageBitrate(); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/BaseAudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/BaseAudioStream.java new file mode 100644 index 0000000000..d6590b207b --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/BaseAudioStream.java @@ -0,0 +1,25 @@ +package org.schabi.newpipe.extractor.streamdata.stream; + +import javax.annotation.Nullable; + +public interface BaseAudioStream { + int UNKNOWN_AVG_BITRATE = -1; + + /** + * Average audio bitrate in KBit/s. + * + * @return the average bitrate or -1 if unknown + */ + default int averageBitrate() { + return UNKNOWN_AVG_BITRATE; + } + + default boolean equalsStream(@Nullable final Stream other) { + if (!(other instanceof BaseAudioStream)) { + return false; + } + + final BaseAudioStream otherAudioStream = (BaseAudioStream) other; + return averageBitrate() == otherAudioStream.averageBitrate(); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java index 8af60b482a..2346e982e1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java @@ -1,11 +1,21 @@ package org.schabi.newpipe.extractor.streamdata.stream; import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.MediaFormat; import javax.annotation.Nonnull; import javax.annotation.Nullable; -public interface Stream { +public interface Stream { + + /** + * The (container) media format, e.g. mp3 for audio streams or webm for video(+audio) streams. + * + * @return The (container) media format + */ + @Nonnull + M mediaFormat(); + @Nonnull DeliveryData deliveryData(); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java index f9ee57e631..4f7ca6c45b 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java @@ -11,10 +11,7 @@ /** * Represents a subtitle (only) stream. */ -public interface SubtitleStream extends Stream { - @Nonnull - SubtitleMediaFormat subtitleMediaFormat(); - +public interface SubtitleStream extends Stream { /** * Return whether if the subtitles are auto-generated. *

@@ -53,7 +50,7 @@ default boolean equalsStream(@Nullable final Stream other) { } final SubtitleStream otherSubtitleStream = (SubtitleStream) other; - return Objects.equals(subtitleMediaFormat(), otherSubtitleStream.subtitleMediaFormat()) + return Objects.equals(mediaFormat(), otherSubtitleStream.mediaFormat()) && autoGenerated() == otherSubtitleStream.autoGenerated() && Objects.equals(languageCode(), otherSubtitleStream.languageCode()); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java index b6132ed464..bffd0765e1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java @@ -5,13 +5,13 @@ /** * Represents a combined video+audio stream. */ -public interface VideoAudioStream extends VideoStream, AudioStream { +public interface VideoAudioStream extends VideoStream, BaseAudioStream { @Override default boolean equalsStream(@Nullable final Stream other) { if (!(other instanceof VideoAudioStream)) { return false; } - return VideoStream.super.equalsStream(other) && AudioStream.super.equalsStream(other); + return VideoStream.super.equalsStream(other) && BaseAudioStream.super.equalsStream(other); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java index 902f81f0af..e55e4df754 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java @@ -10,13 +10,7 @@ /** * Represents a video (only) stream. */ -public interface VideoStream extends Stream { - // TODO: Check if this can be non-null - @Nullable - default VideoAudioMediaFormat videoMediaFormat() { - return null; - } - +public interface VideoStream extends Stream { VideoQualityData videoQualityData(); @Override @@ -26,7 +20,7 @@ default boolean equalsStream(@Nullable final Stream other) { } final VideoStream otherVideoStream = (VideoStream) other; - return Objects.equals(videoMediaFormat(), otherVideoStream.videoMediaFormat()) + return Objects.equals(mediaFormat(), otherVideoStream.mediaFormat()) && videoQualityData().equalsVideoQualityData(otherVideoStream.videoQualityData()); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/AbstractStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/AbstractStreamImpl.java index 2a4abe0a52..72438779f4 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/AbstractStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/AbstractStreamImpl.java @@ -1,20 +1,32 @@ package org.schabi.newpipe.extractor.streamdata.stream.simpleimpl; import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.MediaFormat; import org.schabi.newpipe.extractor.streamdata.stream.Stream; import java.util.Objects; import javax.annotation.Nonnull; -public abstract class AbstractStreamImpl implements Stream { +public abstract class AbstractStreamImpl implements Stream { + @Nonnull + private final M mediaFormat; @Nonnull private final DeliveryData deliveryData; - protected AbstractStreamImpl(@Nonnull final DeliveryData deliveryData) { + protected AbstractStreamImpl( + @Nonnull final M mediaFormat, + @Nonnull final DeliveryData deliveryData) { + this.mediaFormat = Objects.requireNonNull(mediaFormat); this.deliveryData = Objects.requireNonNull(deliveryData); } + @Nonnull + @Override + public M mediaFormat() { + return mediaFormat; + } + @Nonnull @Override public DeliveryData deliveryData() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java index c360e3cb03..d301c38c28 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java @@ -5,47 +5,26 @@ import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; import javax.annotation.Nonnull; -import javax.annotation.Nullable; -public class SimpleAudioStreamImpl extends AbstractStreamImpl implements AudioStream { - @Nullable - private final AudioMediaFormat audioMediaFormat; +public class SimpleAudioStreamImpl extends AbstractStreamImpl implements AudioStream { private final int averageBitrate; public SimpleAudioStreamImpl( + @Nonnull final AudioMediaFormat mediaFormat, @Nonnull final DeliveryData deliveryData, - @Nullable final AudioMediaFormat audioMediaFormat, final int averageBitrate ) { - super(deliveryData); - this.audioMediaFormat = audioMediaFormat; + super(mediaFormat, deliveryData); this.averageBitrate = averageBitrate; } public SimpleAudioStreamImpl( - @Nonnull final DeliveryData deliveryData, - @Nullable final AudioMediaFormat audioMediaFormat - ) { - this(deliveryData, audioMediaFormat, UNKNOWN_BITRATE); - } - - public SimpleAudioStreamImpl( - @Nonnull final DeliveryData deliveryData, - final int averageBitrate + @Nonnull final AudioMediaFormat mediaFormat, + @Nonnull final DeliveryData deliveryData ) { - this(deliveryData, null, averageBitrate); + this(mediaFormat, deliveryData, UNKNOWN_AVG_BITRATE); } - - public SimpleAudioStreamImpl(@Nonnull final DeliveryData deliveryData) { - this(deliveryData, null); - } - - @Nullable - @Override - public AudioMediaFormat audioMediaFormat() { - return audioMediaFormat; - } - + @Override public int averageBitrate() { return averageBitrate; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleSubtitleStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleSubtitleStreamImpl.java index 2f41df6bf5..fe102bc18e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleSubtitleStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleSubtitleStreamImpl.java @@ -9,22 +9,20 @@ import javax.annotation.Nonnull; -public class SimpleSubtitleStreamImpl extends AbstractStreamImpl implements SubtitleStream { - @Nonnull - private final SubtitleMediaFormat subtitleMediaFormat; +public class SimpleSubtitleStreamImpl extends AbstractStreamImpl + implements SubtitleStream { private final boolean autogenerated; @Nonnull private final String languageCode; private final Locale locale; public SimpleSubtitleStreamImpl( + @Nonnull final SubtitleMediaFormat subtitleMediaFormat, @Nonnull final DeliveryData deliveryData, - final SubtitleMediaFormat subtitleMediaFormat, final boolean autogenerated, @Nonnull final String languageCode ) { - super(deliveryData); - this.subtitleMediaFormat = Objects.requireNonNull(subtitleMediaFormat); + super(subtitleMediaFormat, deliveryData); this.autogenerated = autogenerated; this.languageCode = Objects.requireNonNull(languageCode); /* @@ -47,12 +45,6 @@ public SimpleSubtitleStreamImpl( } } - @Nonnull - @Override - public SubtitleMediaFormat subtitleMediaFormat() { - return subtitleMediaFormat; - } - @Override public boolean autoGenerated() { return autogenerated; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java index 822b5dd22b..6d0bc18298 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.extractor.streamdata.stream.simpleimpl; import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; -import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; @@ -9,67 +8,49 @@ import java.util.Objects; import javax.annotation.Nonnull; -import javax.annotation.Nullable; -public class SimpleVideoAudioStreamImpl extends AbstractStreamImpl implements VideoAudioStream { - @Nullable - private final AudioMediaFormat audioMediaFormat; - private final int averageBitrate; +public class SimpleVideoAudioStreamImpl extends AbstractStreamImpl + implements VideoAudioStream { - @Nullable - private final VideoAudioMediaFormat videoAudioMediaFormat; @Nonnull private final VideoQualityData videoQualityData; + private final int averageBitrate; + public SimpleVideoAudioStreamImpl( + @Nonnull final VideoAudioMediaFormat mediaFormat, @Nonnull final DeliveryData deliveryData, - @Nullable final AudioMediaFormat audioMediaFormat, - final int averageBitrate, - @Nullable final VideoAudioMediaFormat videoAudioMediaFormat, - @Nonnull final VideoQualityData videoQualityData + @Nonnull final VideoQualityData videoQualityData, + final int averageBitrate ) { - super(deliveryData); - this.audioMediaFormat = audioMediaFormat; - this.averageBitrate = averageBitrate; - this.videoAudioMediaFormat = videoAudioMediaFormat; + super(mediaFormat, deliveryData); this.videoQualityData = Objects.requireNonNull(videoQualityData); + this.averageBitrate = averageBitrate; } public SimpleVideoAudioStreamImpl( + @Nonnull final VideoAudioMediaFormat mediaFormat, @Nonnull final DeliveryData deliveryData, - @Nullable final VideoAudioMediaFormat videoAudioMediaFormat, @Nonnull final VideoQualityData videoQualityData ) { - this(deliveryData, null, UNKNOWN_BITRATE, videoAudioMediaFormat, videoQualityData); + this(mediaFormat, deliveryData, videoQualityData, UNKNOWN_AVG_BITRATE); } public SimpleVideoAudioStreamImpl( - @Nonnull final DeliveryData deliveryData, - @Nullable final VideoAudioMediaFormat videoAudioMediaFormat + @Nonnull final VideoAudioMediaFormat mediaFormat, + @Nonnull final DeliveryData deliveryData ) { - this(deliveryData, videoAudioMediaFormat, VideoQualityData.fromUnknown()); + this(mediaFormat, deliveryData, VideoQualityData.fromUnknown()); } - @Nullable + @Nonnull @Override - public AudioMediaFormat audioMediaFormat() { - return audioMediaFormat; + public VideoQualityData videoQualityData() { + return videoQualityData; } @Override public int averageBitrate() { return averageBitrate; } - - @Nullable - @Override - public VideoAudioMediaFormat videoMediaFormat() { - return videoAudioMediaFormat; - } - - @Nonnull - @Override - public VideoQualityData videoQualityData() { - return videoQualityData; - } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java index 9ce99603ae..6f858e9f6d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java @@ -8,39 +8,26 @@ import java.util.Objects; import javax.annotation.Nonnull; -import javax.annotation.Nullable; -public class SimpleVideoStreamImpl extends AbstractStreamImpl implements VideoStream { - @Nullable - private final VideoAudioMediaFormat videoAudioMediaFormat; +public class SimpleVideoStreamImpl extends AbstractStreamImpl + implements VideoStream { @Nonnull private final VideoQualityData videoQualityData; public SimpleVideoStreamImpl( + @Nonnull final VideoAudioMediaFormat mediaFormat, @Nonnull final DeliveryData deliveryData, - @Nullable final VideoAudioMediaFormat videoAudioMediaFormat, @Nonnull final VideoQualityData videoQualityData ) { - super(deliveryData); - this.videoAudioMediaFormat = videoAudioMediaFormat; + super(mediaFormat, deliveryData); this.videoQualityData = Objects.requireNonNull(videoQualityData); } public SimpleVideoStreamImpl( - @Nonnull final DeliveryData deliveryData, - @Nonnull final VideoQualityData videoQualityData + @Nonnull final VideoAudioMediaFormat mediaFormat, + @Nonnull final DeliveryData deliveryData ) { - this(deliveryData, null, videoQualityData); - } - - public SimpleVideoStreamImpl(@Nonnull final DeliveryData deliveryData) { - this(deliveryData, VideoQualityData.fromUnknown()); - } - - @Nullable - @Override - public VideoAudioMediaFormat videoMediaFormat() { - return videoAudioMediaFormat; + this(mediaFormat, deliveryData, VideoQualityData.fromUnknown()); } @Nonnull From cd42e196af68f4f996b67b002b6a6746b6504d75 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 9 Jun 2022 20:38:25 +0200 Subject: [PATCH 11/43] More reworks * Killed StreamType * Move ``Privacy`` out of ``Extractor`` * Reworked YT stream extractor --- .../extractors/BandcampStreamExtractor.java | 11 +- .../MediaCCCLiveStreamExtractor.java | 6 - .../extractors/MediaCCCStreamExtractor.java | 6 - .../extractors/PeertubeStreamExtractor.java | 7 +- .../extractors/SoundcloudStreamExtractor.java | 23 +- .../extractor/services/youtube/ItagItem.java | 542 ---------------- .../services/youtube/extractors/ItagInfo.java | 80 --- .../extractors/YoutubeStreamExtractor.java | 609 +++++++++--------- .../format/registry/ItagFormatRegistry.java | 8 +- .../services/youtube/itag/info/ItagInfo.java | 27 +- .../newpipe/extractor/stream/Privacy.java | 9 + .../extractor/stream/StreamExtractor.java | 76 ++- .../newpipe/extractor/stream/StreamInfo.java | 68 +- .../newpipe/extractor/stream/StreamType.java | 82 --- .../newpipe/extractor/utils/Parser.java | 9 +- .../services/DefaultStreamExtractorTest.java | 40 +- .../YoutubeStreamExtractorDefaultTest.java | 3 +- .../YoutubeStreamExtractorUnlistedTest.java | 5 +- 18 files changed, 463 insertions(+), 1148 deletions(-) delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/stream/Privacy.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java index 14bfed226e..714dca714b 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java @@ -20,7 +20,6 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector; 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.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; @@ -158,14 +157,14 @@ public List getAudioStreams() { } @Override - public long getLength() throws ParsingException { - return (long) albumJson.getArray("trackinfo").getObject(0) - .getDouble("duration"); + public boolean isAudioOnly() { + return true; } @Override - public StreamType getStreamType() { - return StreamType.AUDIO_STREAM; + public long getLength() throws ParsingException { + return (long) albumJson.getArray("trackinfo").getObject(0) + .getDouble("duration"); } @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 481a66bc93..abd63eeaac 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -13,7 +13,6 @@ import org.schabi.newpipe.extractor.linkhandler.LinkHandler; 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.streamdata.delivery.DeliveryData; import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleDASHUrlDeliveryDataImpl; import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl; @@ -226,11 +225,6 @@ private Stream getStreamDTOs(@Nonnull final String (JsonObject) e.getValue()))); } - @Override - public StreamType getStreamType() throws ParsingException { - return StreamType.LIVE_STREAM; - } - @Override public boolean isLive() { return true; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index b82868c112..b70957c546 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -15,7 +15,6 @@ import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCStreamLinkHandlerFactory; 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.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; import org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry; @@ -126,11 +125,6 @@ private Stream getRecordingsByMimeType(final String startsWithMimeTy .startsWith(startsWithMimeType)); } - @Override - public StreamType getStreamType() { - return StreamType.VIDEO_STREAM; - } - @Override public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index 8ca48ee4da..ba52268677 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -21,9 +21,9 @@ import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeStreamLinkHandlerFactory; import org.schabi.newpipe.extractor.stream.Description; +import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl; import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; @@ -259,11 +259,6 @@ public List getSubtitles() { return subtitles; } - @Override - public StreamType getStreamType() { - return isLive() ? StreamType.LIVE_STREAM : StreamType.VIDEO_STREAM; - } - @Override public boolean isLive() { return json.getBoolean("isLive"); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 40572d35a0..f77c9683c0 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -25,9 +25,9 @@ import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.Description; +import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl; import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; @@ -402,17 +402,9 @@ private static String getSingleUrlFromHlsManifest(@Nonnull final String hlsManif throw new ParsingException("Could not get any URL from HLS manifest"); } - private static String urlEncode(final String value) { - try { - return URLEncoder.encode(value, UTF_8); - } catch (final UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } - } - @Override - public StreamType getStreamType() { - return StreamType.AUDIO_STREAM; + public boolean isAudioOnly() { + return true; } @Nullable @@ -470,4 +462,13 @@ public List getTags() { } return tags; } + + + private static String urlEncode(final String value) { + try { + return URLEncoder.encode(value, UTF_8); + } catch (final UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java deleted file mode 100644 index e0ff09a6f7..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java +++ /dev/null @@ -1,542 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube; - -import static org.schabi.newpipe.extractor.MediaFormat.M4A; -import static org.schabi.newpipe.extractor.MediaFormat.MPEG_4; -import static org.schabi.newpipe.extractor.MediaFormat.WEBM; -import static org.schabi.newpipe.extractor.MediaFormat.WEBMA; -import static org.schabi.newpipe.extractor.MediaFormat.WEBMA_OPUS; -import static org.schabi.newpipe.extractor.MediaFormat.v3GPP; -import static org.schabi.newpipe.extractor.services.youtube.ItagItem.ItagType.AUDIO; -import static org.schabi.newpipe.extractor.services.youtube.ItagItem.ItagType.VIDEO; -import static org.schabi.newpipe.extractor.services.youtube.ItagItem.ItagType.VIDEO_ONLY; - -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.exceptions.ParsingException; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import java.io.Serializable; - -public class ItagItem implements Serializable { - - /** - * List can be found here: - * https://github.com/ytdl-org/youtube-dl/blob/e988fa4/youtube_dl/extractor/youtube.py#L1195 - */ - private static final ItagItem[] ITAG_LIST = { - ///////////////////////////////////////////////////// - // VIDEO ID Type Format Resolution FPS //// - ///////////////////////////////////////////////////// - new ItagItem(17, VIDEO, v3GPP, "144p"), - new ItagItem(36, VIDEO, v3GPP, "240p"), - - new ItagItem(18, VIDEO, MPEG_4, "360p"), - new ItagItem(34, VIDEO, MPEG_4, "360p"), - new ItagItem(35, VIDEO, MPEG_4, "480p"), - new ItagItem(59, VIDEO, MPEG_4, "480p"), - new ItagItem(78, VIDEO, MPEG_4, "480p"), - new ItagItem(22, VIDEO, MPEG_4, "720p"), - new ItagItem(37, VIDEO, MPEG_4, "1080p"), - new ItagItem(38, VIDEO, MPEG_4, "1080p"), - - new ItagItem(43, VIDEO, WEBM, "360p"), - new ItagItem(44, VIDEO, WEBM, "480p"), - new ItagItem(45, VIDEO, WEBM, "720p"), - new ItagItem(46, VIDEO, WEBM, "1080p"), - - ////////////////////////////////////////////////////////////////// - // AUDIO ID ItagType Format Bitrate // - ////////////////////////////////////////////////////////////////// - new ItagItem(171, AUDIO, WEBMA, 128), - new ItagItem(172, AUDIO, WEBMA, 256), - new ItagItem(139, AUDIO, M4A, 48), - new ItagItem(140, AUDIO, M4A, 128), - new ItagItem(141, AUDIO, M4A, 256), - new ItagItem(249, AUDIO, WEBMA_OPUS, 50), - new ItagItem(250, AUDIO, WEBMA_OPUS, 70), - new ItagItem(251, AUDIO, WEBMA_OPUS, 160), - - /// VIDEO ONLY //////////////////////////////////////////// - // ID Type Format Resolution FPS //// - /////////////////////////////////////////////////////////// - new ItagItem(160, VIDEO_ONLY, MPEG_4, "144p"), - new ItagItem(133, VIDEO_ONLY, MPEG_4, "240p"), - new ItagItem(134, VIDEO_ONLY, MPEG_4, "360p"), - new ItagItem(135, VIDEO_ONLY, MPEG_4, "480p"), - new ItagItem(212, VIDEO_ONLY, MPEG_4, "480p"), - new ItagItem(136, VIDEO_ONLY, MPEG_4, "720p"), - new ItagItem(298, VIDEO_ONLY, MPEG_4, "720p60", 60), - new ItagItem(137, VIDEO_ONLY, MPEG_4, "1080p"), - new ItagItem(299, VIDEO_ONLY, MPEG_4, "1080p60", 60), - new ItagItem(266, VIDEO_ONLY, MPEG_4, "2160p"), - - new ItagItem(278, VIDEO_ONLY, WEBM, "144p"), - new ItagItem(242, VIDEO_ONLY, WEBM, "240p"), - new ItagItem(243, VIDEO_ONLY, WEBM, "360p"), - new ItagItem(244, VIDEO_ONLY, WEBM, "480p"), - new ItagItem(245, VIDEO_ONLY, WEBM, "480p"), - new ItagItem(246, VIDEO_ONLY, WEBM, "480p"), - new ItagItem(247, VIDEO_ONLY, WEBM, "720p"), - new ItagItem(248, VIDEO_ONLY, WEBM, "1080p"), - new ItagItem(271, VIDEO_ONLY, WEBM, "1440p"), - // #272 is either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug) - new ItagItem(272, VIDEO_ONLY, WEBM, "2160p"), - new ItagItem(302, VIDEO_ONLY, WEBM, "720p60", 60), - new ItagItem(303, VIDEO_ONLY, WEBM, "1080p60", 60), - new ItagItem(308, VIDEO_ONLY, WEBM, "1440p60", 60), - new ItagItem(313, VIDEO_ONLY, WEBM, "2160p"), - new ItagItem(315, VIDEO_ONLY, WEBM, "2160p60", 60) - }; - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - public static boolean isSupported(final int itag) { - for (final ItagItem item : ITAG_LIST) { - if (itag == item.id) { - return true; - } - } - return false; - } - - @Nonnull - public static ItagItem getItag(final int itagId) throws ParsingException { - for (final ItagItem item : ITAG_LIST) { - if (itagId == item.id) { - return new ItagItem(item); - } - } - throw new ParsingException("itag " + itagId + " is not supported"); - } - - /*////////////////////////////////////////////////////////////////////////// - // Static constants - //////////////////////////////////////////////////////////////////////////*/ - - public static final int AVERAGE_BITRATE_UNKNOWN = -1; - public static final int SAMPLE_RATE_UNKNOWN = -1; - public static final int FPS_NOT_APPLICABLE_OR_UNKNOWN = -1; - public static final int TARGET_DURATION_SEC_UNKNOWN = -1; - public static final int AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN = -1; - public static final long CONTENT_LENGTH_UNKNOWN = -1; - public static final long APPROX_DURATION_MS_UNKNOWN = -1; - - /*////////////////////////////////////////////////////////////////////////// - // Constructors and misc - //////////////////////////////////////////////////////////////////////////*/ - - public enum ItagType { - AUDIO, - VIDEO, - VIDEO_ONLY - } - - /** - * Call {@link #ItagItem(int, ItagType, MediaFormat, String, int)} with the fps set to 30. - */ - public ItagItem(final int id, - final ItagType type, - final MediaFormat format, - final String resolution) { - this.id = id; - this.itagType = type; - this.mediaFormat = format; - this.resolutionString = resolution; - this.fps = 30; - } - - /** - * Constructor for videos. - */ - public ItagItem(final int id, - final ItagType type, - final MediaFormat format, - final String resolution, - final int fps) { - this.id = id; - this.itagType = type; - this.mediaFormat = format; - this.resolutionString = resolution; - this.fps = fps; - } - - public ItagItem(final int id, - final ItagType type, - final MediaFormat format, - final int avgBitrate) { - this.id = id; - this.itagType = type; - this.mediaFormat = format; - this.avgBitrate = avgBitrate; - } - - /** - * Copy constructor of the {@link ItagItem} class. - * - * @param itagItem the {@link ItagItem} to copy its properties into a new {@link ItagItem} - */ - public ItagItem(@Nonnull final ItagItem itagItem) { - this.mediaFormat = itagItem.mediaFormat; - this.id = itagItem.id; - this.itagType = itagItem.itagType; - this.avgBitrate = itagItem.avgBitrate; - this.sampleRate = itagItem.sampleRate; - this.audioChannels = itagItem.audioChannels; - this.resolutionString = itagItem.resolutionString; - this.fps = itagItem.fps; - this.bitrate = itagItem.bitrate; - this.width = itagItem.width; - this.height = itagItem.height; - this.initStart = itagItem.initStart; - this.initEnd = itagItem.initEnd; - this.indexStart = itagItem.indexStart; - this.indexEnd = itagItem.indexEnd; - this.quality = itagItem.quality; - this.codec = itagItem.codec; - this.targetDurationSec = itagItem.targetDurationSec; - this.approxDurationMs = itagItem.approxDurationMs; - this.contentLength = itagItem.contentLength; - } - - public MediaFormat getMediaFormat() { - return mediaFormat; - } - - private final MediaFormat mediaFormat; - - public final int id; - public final ItagType itagType; - - // Audio fields - /** @deprecated Use {@link #getAverageBitrate()} instead. */ - @Deprecated - public int avgBitrate = AVERAGE_BITRATE_UNKNOWN; - private int sampleRate = SAMPLE_RATE_UNKNOWN; - private int audioChannels = AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN; - - // Video fields - /** @deprecated Use {@link #getResolutionString()} instead. */ - @Deprecated - public String resolutionString; - - /** @deprecated Use {@link #getFps()} and {@link #setFps(int)} instead. */ - @Deprecated - public int fps = FPS_NOT_APPLICABLE_OR_UNKNOWN; - - // Fields for Dash - private int bitrate; - private int width; - private int height; - private int initStart; - private int initEnd; - private int indexStart; - private int indexEnd; - private String quality; - private String codec; - private int targetDurationSec = TARGET_DURATION_SEC_UNKNOWN; - private long approxDurationMs = APPROX_DURATION_MS_UNKNOWN; - private long contentLength = CONTENT_LENGTH_UNKNOWN; - - public int getBitrate() { - return bitrate; - } - - public void setBitrate(final int bitrate) { - this.bitrate = bitrate; - } - - public int getWidth() { - return width; - } - - public void setWidth(final int width) { - this.width = width; - } - - public int getHeight() { - return height; - } - - public void setHeight(final int height) { - this.height = height; - } - - /** - * Get the frame rate. - * - *

- * It is set to the {@code fps} value returned in the corresponding itag in the YouTube player - * response. - *

- * - *

- * It defaults to the standard value associated with this itag. - *

- * - *

- * Note that this value is only known for video itags, so {@link - * #FPS_NOT_APPLICABLE_OR_UNKNOWN} is returned for non video itags. - *

- * - * @return the frame rate or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN} - */ - public int getFps() { - return fps; - } - - /** - * Set the frame rate. - * - *

- * It is only known for video itags, so {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN} is set/used for - * non video itags or if the sample rate value is less than or equal to 0. - *

- * - * @param fps the frame rate - */ - public void setFps(final int fps) { - this.fps = fps > 0 ? fps : FPS_NOT_APPLICABLE_OR_UNKNOWN; - } - - public int getInitStart() { - return initStart; - } - - public void setInitStart(final int initStart) { - this.initStart = initStart; - } - - public int getInitEnd() { - return initEnd; - } - - public void setInitEnd(final int initEnd) { - this.initEnd = initEnd; - } - - public int getIndexStart() { - return indexStart; - } - - public void setIndexStart(final int indexStart) { - this.indexStart = indexStart; - } - - public int getIndexEnd() { - return indexEnd; - } - - public void setIndexEnd(final int indexEnd) { - this.indexEnd = indexEnd; - } - - public String getQuality() { - return quality; - } - - public void setQuality(final String quality) { - this.quality = quality; - } - - /** - * Get the resolution string associated with this {@code ItagItem}. - * - *

- * It is only known for video itags. - *

- * - * @return the resolution string associated with this {@code ItagItem} or - * {@code null}. - */ - @Nullable - public String getResolutionString() { - return resolutionString; - } - - public String getCodec() { - return codec; - } - - public void setCodec(final String codec) { - this.codec = codec; - } - - /** - * Get the average bitrate. - * - *

- * It is only known for audio itags, so {@link #AVERAGE_BITRATE_UNKNOWN} is always returned for - * other itag types. - *

- * - *

- * Bitrate of video itags and precise bitrate of audio itags can be known using - * {@link #getBitrate()}. - *

- * - * @return the average bitrate or {@link #AVERAGE_BITRATE_UNKNOWN} - * @see #getBitrate() - */ - public int getAverageBitrate() { - return avgBitrate; - } - - /** - * Get the sample rate. - * - *

- * It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is returned for non audio - * itags, or if the sample rate is unknown. - *

- * - * @return the sample rate or {@link #SAMPLE_RATE_UNKNOWN} - */ - public int getSampleRate() { - return sampleRate; - } - - /** - * Set the sample rate. - * - *

- * It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is set/used for non audio - * itags, or if the sample rate value is less than or equal to 0. - *

- * - * @param sampleRate the sample rate of an audio itag - */ - public void setSampleRate(final int sampleRate) { - this.sampleRate = sampleRate > 0 ? sampleRate : SAMPLE_RATE_UNKNOWN; - } - - /** - * Get the number of audio channels. - * - *

- * It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is - * returned for non audio itags, or if it is unknown. - *

- * - * @return the number of audio channels or {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} - */ - public int getAudioChannels() { - return audioChannels; - } - - /** - * Set the number of audio channels. - * - *

- * It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is - * set/used for non audio itags, or if the {@code audioChannels} value is less than or equal to - * 0. - *

- * - * @param audioChannels the number of audio channels of an audio itag - */ - public void setAudioChannels(final int audioChannels) { - this.audioChannels = audioChannels > 0 - ? audioChannels - : AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN; - } - - /** - * Get the {@code targetDurationSec} value. - * - *

- * This value is the average time in seconds of the duration of sequences of livestreams and - * ended livestreams. It is only returned by YouTube for these stream types, and makes no sense - * for videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} is returned for those. - *

- * - * @return the {@code targetDurationSec} value or {@link #TARGET_DURATION_SEC_UNKNOWN} - */ - public int getTargetDurationSec() { - return targetDurationSec; - } - - /** - * Set the {@code targetDurationSec} value. - * - *

- * This value is the average time in seconds of the duration of sequences of livestreams and - * ended livestreams. - *

- * - *

- * It is only returned for these stream types by YouTube and makes no sense for videos, so - * {@link #TARGET_DURATION_SEC_UNKNOWN} will be set/used for video streams or if this value is - * less than or equal to 0. - *

- * - * @param targetDurationSec the target duration of a segment of streams which are using the - * live delivery method type - */ - public void setTargetDurationSec(final int targetDurationSec) { - this.targetDurationSec = targetDurationSec > 0 - ? targetDurationSec - : TARGET_DURATION_SEC_UNKNOWN; - } - - /** - * Get the {@code approxDurationMs} value. - * - *

- * It is only known for DASH progressive streams, so {@link #APPROX_DURATION_MS_UNKNOWN} is - * returned for other stream types or if this value is less than or equal to 0. - *

- * - * @return the {@code approxDurationMs} value or {@link #APPROX_DURATION_MS_UNKNOWN} - */ - public long getApproxDurationMs() { - return approxDurationMs; - } - - /** - * Set the {@code approxDurationMs} value. - * - *

- * It is only known for DASH progressive streams, so {@link #APPROX_DURATION_MS_UNKNOWN} is - * set/used for other stream types or if this value is less than or equal to 0. - *

- * - * @param approxDurationMs the approximate duration of a DASH progressive stream, in - * milliseconds - */ - public void setApproxDurationMs(final long approxDurationMs) { - this.approxDurationMs = approxDurationMs > 0 - ? approxDurationMs - : APPROX_DURATION_MS_UNKNOWN; - } - - /** - * Get the {@code contentLength} value. - * - *

- * It is only known for DASH progressive streams, so {@link #CONTENT_LENGTH_UNKNOWN} is - * returned for other stream types or if this value is less than or equal to 0. - *

- * - * @return the {@code contentLength} value or {@link #CONTENT_LENGTH_UNKNOWN} - */ - public long getContentLength() { - return contentLength; - } - - /** - * Set the content length of stream. - * - *

- * It is only known for DASH progressive streams, so {@link #CONTENT_LENGTH_UNKNOWN} is - * set/used for other stream types or if this value is less than or equal to 0. - *

- * - * @param contentLength the content length of a DASH progressive stream - */ - public void setContentLength(final long contentLength) { - this.contentLength = contentLength > 0 ? contentLength : CONTENT_LENGTH_UNKNOWN; - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java deleted file mode 100644 index c1ac4f5f6d..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.extractors; - -import org.schabi.newpipe.extractor.services.youtube.ItagItem; - -import javax.annotation.Nonnull; -import java.io.Serializable; - -/** - * Class to build easier {@link org.schabi.newpipe.extractor.stream.Stream}s for - * {@link YoutubeStreamExtractor}. - * - *

- * It stores, per stream: - *

    - *
  • its content (the URL/the base URL of streams);
  • - *
  • whether its content is the URL the content itself or the base URL;
  • - *
  • its associated {@link ItagItem}.
  • - *
- *

- */ -final class ItagInfo implements Serializable { - @Nonnull - private final String content; - @Nonnull - private final ItagItem itagItem; - private boolean isUrl; - - /** - * Creates a new {@code ItagInfo} instance. - * - * @param content the content of the stream, which must be not null - * @param itagItem the {@link ItagItem} associated with the stream, which must be not null - */ - ItagInfo(@Nonnull final String content, - @Nonnull final ItagItem itagItem) { - this.content = content; - this.itagItem = itagItem; - } - - /** - * Sets whether the stream is a URL. - * - * @param isUrl whether the content is a URL - */ - void setIsUrl(final boolean isUrl) { - this.isUrl = isUrl; - } - - /** - * Gets the content stored in this {@code ItagInfo} instance, which is either the URL to the - * content itself or the base URL. - * - * @return the content stored in this {@code ItagInfo} instance - */ - @Nonnull - String getContent() { - return content; - } - - /** - * Gets the {@link ItagItem} associated with this {@code ItagInfo} instance. - * - * @return the {@link ItagItem} associated with this {@code ItagInfo} instance, which is not - * null - */ - @Nonnull - ItagItem getItagItem() { - return itagItem; - } - - /** - * Gets whether the content stored is the URL to the content itself or the base URL of it. - * - * @return whether the content stored is the URL to the content itself or the base URL of it - * @see #getContent() for more details - */ - boolean getIsUrl() { - return isUrl; - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java index a5cb04b456..a45153b552 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java @@ -20,8 +20,6 @@ package org.schabi.newpipe.extractor.services.youtube.extractors; -import static org.schabi.newpipe.extractor.services.youtube.ItagItem.APPROX_DURATION_MS_UNKNOWN; -import static org.schabi.newpipe.extractor.services.youtube.ItagItem.CONTENT_LENGTH_UNKNOWN; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.CONTENT_CHECK_OK; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.CPN; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.RACY_CHECK_OK; @@ -46,7 +44,6 @@ import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.ScriptableObject; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MetaInfo; import org.schabi.newpipe.extractor.MultiInfoItemsCollector; import org.schabi.newpipe.extractor.StreamingService; @@ -65,21 +62,36 @@ import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.TimeAgoParser; import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager; -import org.schabi.newpipe.extractor.services.youtube.ItagItem; import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptExtractor; import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.services.youtube.YoutubeThrottlingDecrypter; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.HLSItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ProgressiveHTTPItagFormatDeliveryData; +import org.schabi.newpipe.extractor.services.youtube.itag.format.BaseAudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.ItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.registry.ItagFormatRegistry; +import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo; +import org.schabi.newpipe.extractor.services.youtube.itag.info.builder.ItagInfoRangeHelper; import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Frameset; -import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamSegment; -import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.extractor.stream.SubtitlesStream; -import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.format.registry.SubtitleFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoStream; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleSubtitleStreamImpl; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoStreamImpl; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.Pair; import org.schabi.newpipe.extractor.utils.Parser; @@ -97,7 +109,9 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.function.BiFunction; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -139,7 +153,9 @@ public static class DeobfuscateException extends ParsingException { private JsonObject videoSecondaryInfoRenderer; private JsonObject playerMicroFormatRenderer; private int ageLimit = -1; - private StreamType streamType; + + private Boolean isLive; + private Boolean isPostLive; // We need to store the contentPlaybackNonces because we need to append them to videoplayback // URLs (with the cpn parameter). @@ -198,7 +214,7 @@ public String getTextualUploadDate() throws ParsingException { } else if (!liveDetails.getString("startTimestamp", "").isEmpty()) { // a running live stream return liveDetails.getString("startTimestamp"); - } else if (getStreamType() == StreamType.LIVE_STREAM) { + } else if (isLive()) { // this should never be reached, but a live stream without upload date is valid return null; } @@ -352,23 +368,22 @@ public long getLength() throws ParsingException { } } - private int getDurationFromFirstAdaptiveFormat(@Nonnull final List streamingDatas) + private int getDurationFromFirstAdaptiveFormat(@Nonnull final List streamingData) throws ParsingException { - for (final JsonObject streamingData : streamingDatas) { - final JsonArray adaptiveFormats = streamingData.getArray(ADAPTIVE_FORMATS); - if (adaptiveFormats.isEmpty()) { - continue; - } - - final String durationMs = adaptiveFormats.getObject(0) - .getString("approxDurationMs"); - try { - return Math.round(Long.parseLong(durationMs) / 1000f); - } catch (final NumberFormatException ignored) { - } - } - - throw new ParsingException("Could not get duration"); + return streamingData.stream() + .map(s -> s.getArray(ADAPTIVE_FORMATS)) + .filter(af -> !af.isEmpty()) + .map(af -> af.getObject(0).getString("approxDurationMs")) + .map(durationMs -> { + try { + return Math.round(Long.parseLong(durationMs) / 1000f); + } catch (final NumberFormatException ignored) { + return null; + } + }) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(() -> new ParsingException("Could not get duration")); } /** @@ -549,31 +564,31 @@ public String getDashMpdUrl() throws ParsingException { // There is no DASH manifest available in the iOS clients and the DASH manifest of the // Android client doesn't contain all available streams (mainly the WEBM ones) return getManifestUrl( - "dash", + "dashManifestUrl", Arrays.asList(html5StreamingData, androidStreamingData)); } @Nonnull @Override - public String getHlsUrl() throws ParsingException { + public String getHlsMasterPlaylistUrl() throws ParsingException { assertPageFetched(); // Return HLS manifest of the iOS client first because on livestreams, the HLS manifest // returned has separated audio and video streams // Also, on videos, non-iOS clients don't have an HLS manifest URL in their player response return getManifestUrl( - "hls", + "hlsManifestUrl", Arrays.asList(iosStreamingData, html5StreamingData, androidStreamingData)); } @Nonnull - private static String getManifestUrl(@Nonnull final String manifestType, - @Nonnull final List streamingDataObjects) { - final String manifestKey = manifestType + "ManifestUrl"; - + private static String getManifestUrl( + @Nonnull final String manifestKey, + @Nonnull final List streamingDataObjects + ) { return streamingDataObjects.stream() .filter(Objects::nonNull) - .map(streamingDataObject -> streamingDataObject.getString(manifestKey)) + .map(streamingData -> streamingData.getString(manifestKey)) .filter(Objects::nonNull) .findFirst() .orElse(""); @@ -581,23 +596,38 @@ private static String getManifestUrl(@Nonnull final String manifestType, @Override public List getAudioStreams() throws ExtractionException { - assertPageFetched(); - return getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.AUDIO, - getAudioStreamBuilderHelper(), "audio"); + return getItags( + ADAPTIVE_FORMATS, + ItagFormatRegistry.AUDIO_FORMATS, + (itagInfo, deliveryData) -> new SimpleAudioStreamImpl( + itagInfo.getItagFormat().mediaFormat(), + deliveryData, + itagInfo.getCombinedAverageBitrate()), + "audio"); } @Override - public List getVideoStreams() throws ExtractionException { - assertPageFetched(); - return getItags(FORMATS, ItagItem.ItagType.VIDEO, - getVideoStreamBuilderHelper(false), "video"); + public List getVideoStreams() throws ExtractionException { + return getItags(FORMATS, + ItagFormatRegistry.VIDEO_AUDIO_FORMATS, + (itagInfo, deliveryData) -> new SimpleVideoAudioStreamImpl( + itagInfo.getItagFormat().mediaFormat(), + deliveryData, + itagInfo.getCombinedVideoQualityData(), + itagInfo.getCombinedAverageBitrate()), + "video"); } @Override public List getVideoOnlyStreams() throws ExtractionException { - assertPageFetched(); - return getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.VIDEO_ONLY, - getVideoStreamBuilderHelper(true), "video-only"); + return getItags( + ADAPTIVE_FORMATS, + ItagFormatRegistry.VIDEO_FORMATS, + (itagInfo, deliveryData) -> new SimpleVideoStreamImpl( + itagInfo.getItagFormat().mediaFormat(), + deliveryData, + itagInfo.getCombinedVideoQualityData()), + "video-only"); } /** @@ -621,63 +651,43 @@ private String tryDecryptUrl(final String streamingUrl, final String videoId) { @Override @Nonnull - public List getSubtitlesDefault() throws ParsingException { - return getSubtitles(MediaFormat.TTML); - } - - @Override - @Nonnull - public List getSubtitles(final MediaFormat format) throws ParsingException { + public List getSubtitles() throws ParsingException { assertPageFetched(); - // We cannot store the subtitles list because the media format may change - final List subtitlesToReturn = new ArrayList<>(); - final JsonObject renderer = playerResponse.getObject("captions") - .getObject("playerCaptionsTracklistRenderer"); - final JsonArray captionsArray = renderer.getArray("captionTracks"); - // TODO: use this to apply auto translation to different language from a source language - // final JsonArray autoCaptionsArray = renderer.getArray("translationLanguages"); - - for (int i = 0; i < captionsArray.size(); i++) { - final String languageCode = captionsArray.getObject(i).getString("languageCode"); - final String baseUrl = captionsArray.getObject(i).getString("baseUrl"); - final String vssId = captionsArray.getObject(i).getString("vssId"); - - if (languageCode != null && baseUrl != null && vssId != null) { - final boolean isAutoGenerated = vssId.startsWith("a."); - final String cleanUrl = baseUrl - // Remove preexisting format if exists - .replaceAll("&fmt=[^&]*", "") - // Remove translation language - .replaceAll("&tlang=[^&]*", ""); - - subtitlesToReturn.add(new SubtitlesStream.Builder() - .setContent(cleanUrl + "&fmt=" + format.getSuffix(), true) - .setMediaFormat(format) - .setLanguageCode(languageCode) - .setAutoGenerated(isAutoGenerated) - .build()); - } - } - - return subtitlesToReturn; + return playerResponse + .getObject("captions") + .getObject("playerCaptionsTracklistRenderer") + .getArray("captionTracks") + .stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .filter(jsObj -> jsObj.getString("languageCode") != null + && jsObj.getString("baseUrl") != null + && jsObj.getString("vssId") != null) + .map(jsObj -> new SimpleSubtitleStreamImpl( + SubtitleFormatRegistry.TTML, + new SimpleProgressiveHTTPDeliveryDataImpl(jsObj.getString("baseUrl") + // Remove preexisting format if exists + .replaceAll("&fmt=[^&]*", "") + // Remove translation language + .replaceAll("&tlang=[^&]*", "")), + jsObj.getString("vssId").startsWith("a."), + jsObj.getString("languageCode") + )) + .collect(Collectors.toList()); } @Override - public StreamType getStreamType() { + public boolean isLive() { assertPageFetched(); - return streamType; + return isLive; } - private void setStreamType() { - if (playerResponse.getObject("playabilityStatus").has("liveStreamability")) { - streamType = StreamType.LIVE_STREAM; - } else if (playerResponse.getObject("videoDetails").getBoolean("isPostLiveDvr", false)) { - streamType = StreamType.POST_LIVE_STREAM; - } else { - streamType = StreamType.VIDEO_STREAM; - } + public boolean isPostLive() { + assertPageFetched(); + + return isPostLive; } @Nullable @@ -793,8 +803,6 @@ public void onFetchPage(@Nonnull final Downloader downloader) final boolean isAgeRestricted = playabilityStatus.getString("reason", "") .contains("age"); - setStreamType(); - if (!playerResponse.has(STREAMING_DATA)) { try { fetchTvHtml5EmbedJsonPlayer(contentCountry, localization, videoId); @@ -802,9 +810,12 @@ public void onFetchPage(@Nonnull final Downloader downloader) } } + isLive = playerResponse.getObject("playabilityStatus").has("liveStreamability"); + isPostLive = !isLive + && playerResponse.getObject("videoDetails").getBoolean("isPostLiveDvr", false); + // Refresh the stream type because the stream type may be not properly known for // age-restricted videos - setStreamType(); if (html5StreamingData == null && playerResponse.has(STREAMING_DATA)) { html5StreamingData = playerResponse.getObject(STREAMING_DATA); @@ -829,10 +840,7 @@ public void onFetchPage(@Nonnull final Downloader downloader) .getBytes(StandardCharsets.UTF_8); nextResponse = getJsonPostResponse(NEXT, body, localization); - // streamType can only have LIVE_STREAM, POST_LIVE_STREAM and VIDEO_STREAM values (see - // setStreamType()), so this block will be run only for POST_LIVE_STREAM and VIDEO_STREAM - // values if fetching of the ANDROID client is not forced - if ((!isAgeRestricted && streamType != StreamType.LIVE_STREAM) + if ((!isAgeRestricted && !isLive && !isPostLive()) || isAndroidClientFetchForced) { try { fetchAndroidMobileJsonPlayer(contentCountry, localization, videoId); @@ -842,7 +850,7 @@ public void onFetchPage(@Nonnull final Downloader downloader) } } - if ((!isAgeRestricted && streamType == StreamType.LIVE_STREAM) + if ((!isAgeRestricted && isLive) || isIosClientFetchForced) { try { fetchIosMobileJsonPlayer(contentCountry, localization, videoId); @@ -1194,195 +1202,123 @@ private JsonObject getVideoSecondaryInfoRenderer() throws ParsingException { } @Nonnull - private List getItags( + private , + I extends ItagFormat> List getItags( final String streamingDataKey, - final ItagItem.ItagType itagTypeWanted, - final java.util.function.Function streamBuilderHelper, - final String streamTypeExceptionMessage) throws ParsingException { + final I[] itagFormats, + final BiFunction, DeliveryData, T> streamBuilder, + final String streamTypeExceptionMessage + ) throws ParsingException { + assertPageFetched(); try { - final String videoId = getId(); - final List streamList = new ArrayList<>(); + if (html5StreamingData == null + && androidStreamingData == null + && iosStreamingData == null) { + return Collections.emptyList(); + } - java.util.stream.Stream.of( + final List> streamingDataAndCpnLoopList = Arrays.asList( // Use the androidStreamingData object first because there is no n param and no // signatureCiphers in streaming URLs of the Android client new Pair<>(androidStreamingData, androidCpn), new Pair<>(html5StreamingData, html5Cpn), // Use the iosStreamingData object in the last position because most of the - // available streams can be extracted with the Android and web clients and also - // because the iOS client is only enabled by default on livestreams + // available streams can be extracted with the Android and web clients and + // also because the iOS client is only enabled by default on livestreams new Pair<>(iosStreamingData, iosCpn) - ) - .flatMap(pair -> getStreamsFromStreamingDataKey(videoId, pair.getFirst(), - streamingDataKey, itagTypeWanted, pair.getSecond())) - .map(streamBuilderHelper) - .forEachOrdered(stream -> { - if (!Stream.containSimilarStream(stream, streamList)) { - streamList.add(stream); - } - }); + ); + + return streamingDataAndCpnLoopList.stream() + .flatMap(p -> buildItagInfoForItags( + p.getFirst(), + streamingDataKey, + itagFormats, + p.getSecond())) + .map(i -> streamBuilder.apply(i, buildDeliveryData(i))) + .collect(Collectors.toList()); - return streamList; } catch (final Exception e) { throw new ParsingException( "Could not get " + streamTypeExceptionMessage + " streams", e); } } - /** - * Get the stream builder helper which will be used to build {@link AudioStream}s in - * {@link #getItags(String, ItagItem.ItagType, java.util.function.Function, String)} - * - *

- * The {@code StreamBuilderHelper} will set the following attributes in the - * {@link AudioStream}s built: - *

    - *
  • the {@link ItagItem}'s id of the stream as its id;
  • - *
  • {@link ItagInfo#getContent()} and {@link ItagInfo#getIsUrl()} as its content and - * and as the value of {@code isUrl};
  • - *
  • the media format returned by the {@link ItagItem} as its media format;
  • - *
  • its average bitrate with the value returned by {@link - * ItagItem#getAverageBitrate()};
  • - *
  • the {@link ItagItem};
  • - *
  • the {@link DeliveryMethod#DASH DASH delivery method}, for OTF streams, live streams - * and ended streams.
  • - *
- *

- * - *

- * Note that the {@link ItagItem} comes from an {@link ItagInfo} instance. - *

- * - * @return a stream builder helper to build {@link AudioStream}s - */ @Nonnull - private java.util.function.Function getAudioStreamBuilderHelper() { - return (itagInfo) -> { - final ItagItem itagItem = itagInfo.getItagItem(); - final AudioStream.Builder builder = new AudioStream.Builder() - .setId(String.valueOf(itagItem.id)) - .setContent(itagInfo.getContent(), itagInfo.getIsUrl()) - .setMediaFormat(itagItem.getMediaFormat()) - .setAverageBitrate(itagItem.getAverageBitrate()) - .setItagItem(itagItem); - - if (streamType == StreamType.LIVE_STREAM - || streamType == StreamType.POST_LIVE_STREAM - || !itagInfo.getIsUrl()) { - // For YouTube videos on OTF streams and for all streams of post-live streams - // and live streams, only the DASH delivery method can be used. - builder.setDeliveryMethod(DeliveryMethod.DASH); - } + private > DeliveryData buildDeliveryData(final ItagInfo itagInfo) { + final ItagFormatDeliveryData iDeliveryData = itagInfo.getItagFormat().deliveryData(); + if (iDeliveryData instanceof ProgressiveHTTPItagFormatDeliveryData) { + return new SimpleProgressiveHTTPDeliveryDataImpl(itagInfo.getStreamUrl()); + } else if (iDeliveryData instanceof HLSItagFormatDeliveryData) { + return new SimpleHLSDeliveryDataImpl(itagInfo.getStreamUrl()); + } - return builder.build(); - }; - } + // DASH + // TODO + if ("FORMAT_STREAM_TYPE_OTF".equalsIgnoreCase(itagInfo.getType())) { + // OTF DASH MANIFEST + } else if (isPostLive()) { + // YoutubePostLiveStreamDvrDashManifestCreator + } + // YoutubeProgressiveDashManifestCreator - /** - * Get the stream builder helper which will be used to build {@link VideoStream}s in - * {@link #getItags(String, ItagItem.ItagType, java.util.function.Function, String)} - * - *

- * The {@code StreamBuilderHelper} will set the following attributes in the - * {@link VideoStream}s built: - *

    - *
  • the {@link ItagItem}'s id of the stream as its id;
  • - *
  • {@link ItagInfo#getContent()} and {@link ItagInfo#getIsUrl()} as its content and - * and as the value of {@code isUrl};
  • - *
  • the media format returned by the {@link ItagItem} as its media format;
  • - *
  • whether it is video-only with the {@code areStreamsVideoOnly} parameter
  • - *
  • the {@link ItagItem};
  • - *
  • the resolution, by trying to use, in this order: - *
      - *
    1. the height returned by the {@link ItagItem} + {@code p} + the frame rate if - * it is more than 30;
    2. - *
    3. the default resolution string from the {@link ItagItem};
    4. - *
    5. an empty string.
    6. - *
    - *
  • - *
  • the {@link DeliveryMethod#DASH DASH delivery method}, for OTF streams, live streams - * and ended streams.
  • - *
- * - *

- * Note that the {@link ItagItem} comes from an {@link ItagInfo} instance. - *

- * - * @param areStreamsVideoOnly whether the stream builder helper will set the video - * streams as video-only streams - * @return a stream builder helper to build {@link VideoStream}s - */ - @Nonnull - private java.util.function.Function getVideoStreamBuilderHelper( - final boolean areStreamsVideoOnly) { - return (itagInfo) -> { - final ItagItem itagItem = itagInfo.getItagItem(); - final VideoStream.Builder builder = new VideoStream.Builder() - .setId(String.valueOf(itagItem.id)) - .setContent(itagInfo.getContent(), itagInfo.getIsUrl()) - .setMediaFormat(itagItem.getMediaFormat()) - .setIsVideoOnly(areStreamsVideoOnly) - .setItagItem(itagItem); - - final String resolutionString = itagItem.getResolutionString(); - builder.setResolution(resolutionString != null ? resolutionString - : ""); - - if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) { - // For YouTube videos on OTF streams and for all streams of post-live streams - // and live streams, only the DASH delivery method can be used. - builder.setDeliveryMethod(DeliveryMethod.DASH); - } - return builder.build(); - }; + return null; } @Nonnull - private java.util.stream.Stream getStreamsFromStreamingDataKey( - final String videoId, + private > Stream> buildItagInfoForItags( final JsonObject streamingData, final String streamingDataKey, - @Nonnull final ItagItem.ItagType itagTypeWanted, + final I[] itagFormats, @Nonnull final String contentPlaybackNonce) { if (streamingData == null || !streamingData.has(streamingDataKey)) { - return java.util.stream.Stream.empty(); + return Stream.of(); } - return streamingData.getArray(streamingDataKey).stream() + final String videoId; + try { + videoId = getId(); + } catch (final ParsingException ignored) { + return Stream.of(); + } + return streamingData.getArray(streamingDataKey) + .stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) + .filter(formatData -> + ItagFormatRegistry.isSupported( + itagFormats, + formatData.getInt("itag"))) .map(formatData -> { try { - final ItagItem itagItem = ItagItem.getItag(formatData.getInt("itag")); - if (itagItem.itagType == itagTypeWanted) { - return buildAndAddItagInfoToList(videoId, formatData, itagItem, - itagItem.itagType, contentPlaybackNonce); - } - } catch (final IOException | ExtractionException ignored) { - // if the itag is not supported and getItag fails, we end up here + final I itagFormat = ItagFormatRegistry.getById( + itagFormats, + formatData.getInt("itag")); + + return buildItagInfo(videoId, formatData, itagFormat, contentPlaybackNonce); + } catch (final Exception ignored) { + return null; } - return null; }) .filter(Objects::nonNull); } - private ItagInfo buildAndAddItagInfoToList( + private > ItagInfo buildItagInfo( @Nonnull final String videoId, @Nonnull final JsonObject formatData, - @Nonnull final ItagItem itagItem, - @Nonnull final ItagItem.ItagType itagType, - @Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException { + @Nonnull final I itagFormat, + @Nonnull final String contentPlaybackNonce + ) throws ParsingException { + + // Build url String streamUrl; if (formatData.has("url")) { streamUrl = formatData.getString("url"); } else { // This url has an obfuscated signature - final String cipherString = formatData.has(CIPHER) + final Map cipher = Parser.compatParseMap(formatData.has(CIPHER) ? formatData.getString(CIPHER) - : formatData.getString(SIGNATURE_CIPHER); - final Map cipher = Parser.compatParseMap( - cipherString); + : formatData.getString(SIGNATURE_CIPHER)); streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "=" + deobfuscateSignature(cipher.get("s")); } @@ -1393,63 +1329,144 @@ private ItagInfo buildAndAddItagInfoToList( // Decrypt the n parameter if it is present streamUrl = tryDecryptUrl(streamUrl, videoId); - final JsonObject initRange = formatData.getObject("initRange"); - final JsonObject indexRange = formatData.getObject("indexRange"); + final ItagInfo itagInfo = new ItagInfo<>(itagFormat, streamUrl); + + if (itagFormat instanceof BaseAudioItagFormat) { + final Integer averageBitrate = getIntegerFromJson(formatData, "averageBitrate"); + if (averageBitrate != null) { + itagInfo.setAverageBitrate((int) Math.round(averageBitrate / 1000d)); + } + // YouTube returns the audio sample rate as a string + try { + itagInfo.setAudioSampleRate( + Integer.parseInt(formatData.getString("audioSampleRate"))); + } catch (final Exception ignore) { + // Ignore errors - leave default value + } + itagInfo.setAudioChannels(getIntegerFromJson(formatData, "audioChannels")); + } + if (itagFormat instanceof VideoItagFormat) { + itagInfo.setHeight(getIntegerFromJson(formatData, "height")); + itagInfo.setWidth(getIntegerFromJson(formatData, "width")); + itagInfo.setFps(getIntegerFromJson(formatData, "fps")); + } + + itagInfo.setBitRate(getIntegerFromJson(formatData, "bitRate")); + itagInfo.setQuality(formatData.getString("quality")); + final String mimeType = formatData.getString("mimeType", ""); - final String codec = mimeType.contains("codecs") - ? mimeType.split("\"")[1] : ""; - - itagItem.setBitrate(formatData.getInt("bitrate")); - itagItem.setWidth(formatData.getInt("width")); - itagItem.setHeight(formatData.getInt("height")); - itagItem.setInitStart(Integer.parseInt(initRange.getString("start", "-1"))); - itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", "-1"))); - itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", "-1"))); - itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", "-1"))); - itagItem.setQuality(formatData.getString("quality")); - itagItem.setCodec(codec); - - if (streamType == StreamType.LIVE_STREAM || streamType == StreamType.POST_LIVE_STREAM) { - itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec")); - } - - if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) { - itagItem.setFps(formatData.getInt("fps")); - } else if (itagType == ItagItem.ItagType.AUDIO) { - // YouTube return the audio sample rate as a string - itagItem.setSampleRate(Integer.parseInt(formatData.getString("audioSampleRate"))); - itagItem.setAudioChannels(formatData.getInt("audioChannels", - // Most audio streams have two audio channels, so use this value if the real - // count cannot be extracted - // Doing this prevents an exception when generating the - // AudioChannelConfiguration element of DASH manifests of audio streams in - // YoutubeDashManifestCreatorUtils - 2)); - } - - // YouTube return the content length and the approximate duration as strings - itagItem.setContentLength(Long.parseLong(formatData.getString("contentLength", - String.valueOf(CONTENT_LENGTH_UNKNOWN)))); - itagItem.setApproxDurationMs(Long.parseLong(formatData.getString("approxDurationMs", - String.valueOf(APPROX_DURATION_MS_UNKNOWN)))); - - final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem); - - if (streamType == StreamType.VIDEO_STREAM) { - itagInfo.setIsUrl(!formatData.getString("type", "") - .equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF")); - } else { - // We are currently not able to generate DASH manifests for running - // livestreams, so because of the requirements of StreamInfo - // objects, return these streams as DASH URL streams (even if they - // are not playable). - // Ended livestreams are returned as non URL streams - itagInfo.setIsUrl(streamType != StreamType.POST_LIVE_STREAM); + itagInfo.setCodec(mimeType.contains("codecs") + ? mimeType.split("\"")[1] + : null); + + itagInfo.setInitRange(ItagInfoRangeHelper.buildFrom(formatData.getObject("initRange"))); + itagInfo.setIndexRange(ItagInfoRangeHelper.buildFrom(formatData.getObject("indexRange"))); + + // YouTube returns the content length and the approximate duration as strings + try { + itagInfo.setContentLength( + Long.parseLong(formatData.getString("contentLength"))); + } catch (final Exception ignored) { + // Ignore errors - leave default value } + try { + itagInfo.setApproxDurationMs( + Long.parseLong(formatData.getString("approxDurationMs"))); + } catch (final Exception ignored) { + // Ignore errors - leave default value + } + + itagInfo.setType(formatData.getString("type")); + itagInfo.setTargetDurationSec(getIntegerFromJson(formatData, "targetDurationSec")); return itagInfo; } + // TODO + private static Integer getIntegerFromJson(final JsonObject jsonObject, final String key) { + return (Integer) jsonObject.getNumber(key); + } + +// private ItagInfo buildItagInfo( +// @Nonnull final String videoId, +// @Nonnull final JsonObject formatData, +// @Nonnull final ItagItem itagItem, +// @Nonnull final ItagItem.ItagType itagType, +// @Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException { +// String streamUrl; +// if (formatData.has("url")) { +// streamUrl = formatData.getString("url"); +// } else { +// // This url has an obfuscated signature +// final String cipherString = formatData.has(CIPHER) +// ? formatData.getString(CIPHER) +// : formatData.getString(SIGNATURE_CIPHER); +// final Map cipher = Parser.compatParseMap(cipherString); +// streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "=" +// + deobfuscateSignature(cipher.get("s")); +// } +// +// // Add the content playback nonce to the stream URL +// streamUrl += "&" + CPN + "=" + contentPlaybackNonce; +// +// // Decrypt the n parameter if it is present +// streamUrl = tryDecryptUrl(streamUrl, videoId); +// +// final JsonObject initRange = formatData.getObject("initRange"); +// final JsonObject indexRange = formatData.getObject("indexRange"); +// final String mimeType = formatData.getString("mimeType", EMPTY_STRING); +// final String codec = mimeType.contains("codecs") +// ? mimeType.split("\"")[1] +// : EMPTY_STRING; +// +// itagItem.setBitrate(formatData.getInt("bitrate")); +// itagItem.setWidth(formatData.getInt("width")); +// itagItem.setHeight(formatData.getInt("height")); +// itagItem.setInitStart(Integer.parseInt(initRange.getString("start", "-1"))); +// itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", "-1"))); +// itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", "-1"))); +// itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", "-1"))); +// itagItem.setQuality(formatData.getString("quality")); +// itagItem.setCodec(codec); +// +// if (isLive() || isPostLive()) { +// itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec")); +// } +// +// if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) { +// itagItem.setFps(formatData.getInt("fps")); +// } +// if (itagType == ItagItem.ItagType.AUDIO) { +// // YouTube returns the audio sample rate as a string +// itagItem.setSampleRate(Integer.parseInt(formatData.getString("audioSampleRate"))); +// itagItem.setAudioChannels(formatData.getInt("audioChannels")); +// } +// +// // YouTube return the content length and the approximate duration as strings +// itagItem.setContentLength(Long.parseLong(formatData.getString( +// "contentLength", +// String.valueOf(CONTENT_LENGTH_UNKNOWN)))); +// itagItem.setApproxDurationMs(Long.parseLong(formatData.getString( +// "approxDurationMs", +// String.valueOf(APPROX_DURATION_MS_UNKNOWN)))); +// +// final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem); +// +// if (streamType == StreamType.VIDEO_STREAM) { +// itagInfo.setIsUrl(!formatData.getString("type", "") +// .equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF")); +// } else { +// // We are currently not able to generate DASH manifests for running +// // livestreams, so because of the requirements of StreamInfo +// // objects, return these streams as DASH URL streams (even if they +// // are not playable). +// // Ended livestreams are returned as non URL streams +// itagInfo.setIsUrl(streamType != StreamType.POST_LIVE_STREAM); +// } +// +// return itagInfo; +// } + @Nonnull @Override public List getFrames() throws ExtractionException { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java index da36291f06..ae66b627e2 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java @@ -117,14 +117,14 @@ private ItagFormatRegistry() { // No impl } - public static boolean isSupported(final int id) { - return Stream.of(VIDEO_AUDIO_FORMATS, AUDIO_FORMATS, VIDEO_FORMATS) + public static boolean isSupported(final ItagFormat[] itagFormats, final int id) { + return Stream.of(itagFormats) .flatMap(Stream::of) .anyMatch(itagFormat -> itagFormat.id() == id); } - public static ItagFormat getById(final int id) { - return Stream.of(VIDEO_AUDIO_FORMATS, AUDIO_FORMATS, VIDEO_FORMATS) + public static > I getById(final I[] itagFormats, final int id) { + return Stream.of(itagFormats) .flatMap(Stream::of) .filter(itagFormat -> itagFormat.id() == id) .findFirst() diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java index 1eda56723c..5e8b7ed1a8 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java @@ -10,10 +10,10 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -public class ItagInfo { +public class ItagInfo> { @Nonnull - private final ItagFormat itagFormat; + private final I itagFormat; // TODO: Maybe generate the streamUrl on-demand and not always instantly? @Nonnull @@ -21,8 +21,11 @@ public class ItagInfo { // region Audio + /** + * The average bitrate in KBit/s + */ @Nullable - private Integer averageBitRate; + private Integer averageBitrate; @Nullable private Integer audioSampleRate; @@ -78,7 +81,7 @@ public class ItagInfo { public ItagInfo( - @Nonnull final ItagFormat itagFormat, + @Nonnull final I itagFormat, @Nonnull final String streamUrl) { this.itagFormat = itagFormat; this.streamUrl = streamUrl; @@ -87,7 +90,7 @@ public ItagInfo( // region Getters + Setters @Nonnull - public ItagFormat getItagFormat() { + public I getItagFormat() { return itagFormat; } @@ -97,12 +100,12 @@ public String getStreamUrl() { } @Nullable - public Integer getAverageBitRate() { - return averageBitRate; + public Integer getAverageBitrate() { + return averageBitrate; } - public void setAverageBitRate(@Nullable final Integer averageBitRate) { - this.averageBitRate = averageBitRate; + public void setAverageBitrate(@Nullable final Integer averageBitrate) { + this.averageBitrate = averageBitrate; } @Nullable @@ -238,12 +241,12 @@ public void setTargetDurationSec(@Nullable final Integer targetDurationSec) { * @return averageBitRate in KBit/s or -1 */ public int getCombinedAverageBitrate() { - if (averageBitRate != null) { - return (int) Math.round(averageBitRate / 1000d); + if (averageBitrate != null) { + return averageBitrate; } if (itagFormat instanceof BaseAudioItagFormat) { - return ((BaseAudioItagFormat)itagFormat).averageBitrate(); + return ((BaseAudioItagFormat) itagFormat).averageBitrate(); } return -1; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Privacy.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Privacy.java new file mode 100644 index 0000000000..e02dd327b7 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Privacy.java @@ -0,0 +1,9 @@ +package org.schabi.newpipe.extractor.stream; + +public enum Privacy { + PUBLIC, + UNLISTED, + PRIVATE, + INTERNAL, + OTHER +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java index 12dc554833..0d2d8d1d76 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java @@ -344,14 +344,19 @@ public List getSubtitles() throws IOException, ExtractionExcepti } /** - * Get the {@link StreamType}. + * This will return whenever there are only audio streams available. * - * @return the type of the stream + * @return true when audio only otherwise false */ - @Deprecated // TODO - kill - public abstract StreamType getStreamType() throws ParsingException; + public boolean isAudioOnly() { + return false; + } - // TODO - Implement + /** + * This will return whenever the current stream is live. + * + * @return true when live otherwise false + */ public boolean isLive() { return false; } @@ -366,8 +371,8 @@ public boolean isLive() { * @return a list of InfoItems showing the related videos/streams */ @Nullable - public InfoItemsCollector - getRelatedItems() throws IOException, ExtractionException { + public InfoItemsCollector getRelatedItems() + throws IOException, ExtractionException { return null; } @@ -382,9 +387,8 @@ public StreamInfoItemsCollector getRelatedStreams() throws IOException, Extracti final InfoItemsCollector collector = getRelatedItems(); if (collector instanceof StreamInfoItemsCollector) { return (StreamInfoItemsCollector) collector; - } else { - return null; } + return null; } /** @@ -429,33 +433,33 @@ protected long getTimestampSeconds(final String regexPattern) throws ParsingExce return -2; } - if (!timestamp.isEmpty()) { + if (timestamp.isEmpty()) { + return 0; + } + + try { + String secondsString = ""; + String minutesString = ""; + String hoursString = ""; try { - String secondsString = ""; - String minutesString = ""; - String hoursString = ""; - try { - secondsString = Parser.matchGroup1("(\\d+)s", timestamp); - minutesString = Parser.matchGroup1("(\\d+)m", timestamp); - hoursString = Parser.matchGroup1("(\\d+)h", timestamp); - } catch (final Exception e) { - // it could be that time is given in another method - if (secondsString.isEmpty() && minutesString.isEmpty()) { - // if nothing was obtained, treat as unlabelled seconds - secondsString = Parser.matchGroup1("t=(\\d+)", timestamp); - } + secondsString = Parser.matchGroup1("(\\d+)s", timestamp); + minutesString = Parser.matchGroup1("(\\d+)m", timestamp); + hoursString = Parser.matchGroup1("(\\d+)h", timestamp); + } catch (final Exception e) { + // it could be that time is given in another method + if (secondsString.isEmpty() && minutesString.isEmpty()) { + // if nothing was obtained, treat as unlabelled seconds + secondsString = Parser.matchGroup1("t=(\\d+)", timestamp); } + } - final int seconds = secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString); - final int minutes = minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString); - final int hours = hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString); + final int seconds = secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString); + final int minutes = minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString); + final int hours = hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString); - return seconds + (60L * minutes) + (3600L * hours); - } catch (final ParsingException e) { - throw new ParsingException("Could not get timestamp.", e); - } - } else { - return 0; + return seconds + (60L * minutes) + (3600L * hours); + } catch (final ParsingException e) { + throw new ParsingException("Could not get timestamp.", e); } } @@ -568,12 +572,4 @@ public List getStreamSegments() throws ParsingException { public List getMetaInfo() throws ParsingException { return Collections.emptyList(); } - - public enum Privacy { - PUBLIC, - UNLISTED, - PRIVATE, - INTERNAL, - OTHER - } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index 5bb615b4b9..9734dda49d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -51,7 +51,6 @@ */ public class StreamInfo extends Info { - private StreamType streamType; private String thumbnailUrl = ""; private String textualUploadDate; private DateWrapper uploadDate; @@ -73,14 +72,17 @@ public class StreamInfo extends Info { private String subChannelUrl = ""; private String subChannelAvatarUrl = ""; + private List videoStreams = new ArrayList<>(); + private List audioStreams = new ArrayList<>(); + private List videoOnlyStreams = new ArrayList<>(); + @Nonnull private String dashMpdUrl = ""; @Nonnull private String hlsMasterPlaylistUrl = ""; - private List videoStreams = new ArrayList<>(); - private List audioStreams = new ArrayList<>(); - private List videoOnlyStreams = new ArrayList<>(); + private boolean audioOnly = false; + private boolean live = false; private List relatedItems = new ArrayList<>(); @@ -88,7 +90,7 @@ public class StreamInfo extends Info { private List subtitles = new ArrayList<>(); private String host = ""; - private StreamExtractor.Privacy privacy; + private Privacy privacy; private String category = ""; private String licence = ""; private String supportInfo = ""; @@ -100,12 +102,10 @@ public class StreamInfo extends Info { public StreamInfo(final int serviceId, final String url, final String originalUrl, - final StreamType streamType, final String id, final String name, final int ageLimit) { super(serviceId, id, url, originalUrl, name); - this.streamType = streamType; this.ageLimit = ageLimit; } @@ -114,19 +114,6 @@ public StreamInfo(final int serviceId, */ private List previewFrames = Collections.emptyList(); - /** - * Get the stream type - * - * @return the stream type - */ - public StreamType getStreamType() { - return streamType; - } - - public void setStreamType(final StreamType streamType) { - this.streamType = streamType; - } - /** * Get the thumbnail url * @@ -328,6 +315,22 @@ public void setHlsMasterPlaylistUrl(@Nonnull final String hlsMasterPlaylistUrl) this.hlsMasterPlaylistUrl = Objects.requireNonNull(hlsMasterPlaylistUrl); } + public boolean isAudioOnly() { + return audioOnly; + } + + public void setAudioOnly(final boolean audioOnly) { + this.audioOnly = audioOnly; + } + + public boolean isLive() { + return live; + } + + public void setLive(final boolean live) { + this.live = live; + } + public List getRelatedItems() { return relatedItems; } @@ -361,11 +364,11 @@ public void setHost(final String host) { this.host = host; } - public StreamExtractor.Privacy getPrivacy() { + public Privacy getPrivacy() { return this.privacy; } - public void setPrivacy(final StreamExtractor.Privacy privacy) { + public void setPrivacy(final Privacy privacy) { this.privacy = privacy; } @@ -479,23 +482,32 @@ private static StreamInfo extractImportantData(@Nonnull final StreamExtractor ex extractor.getServiceId(); // Check if a exception is thrown final String url = extractor.getUrl(); extractor.getOriginalUrl(); // Check if a exception is thrown - final StreamType streamType = extractor.getStreamType(); final String id = extractor.getId(); final String name = extractor.getName(); final int ageLimit = extractor.getAgeLimit(); // Suppress always-non-null warning as here we double-check it really is not null //noinspection ConstantConditions - if (streamType == StreamType.NONE - || isNullOrEmpty(url) + if (isNullOrEmpty(url) || isNullOrEmpty(id) || name == null /* but it can be empty of course */ || ageLimit == -1) { - throw new ExtractionException("Some important stream information was not given."); + throw new ExtractionException("Some important stream information was not given"); } - return new StreamInfo(extractor.getServiceId(), url, extractor.getOriginalUrl(), - streamType, id, name, ageLimit); + final StreamInfo streamInfo = new StreamInfo( + extractor.getServiceId(), + url, + extractor.getOriginalUrl(), + id, + name, + ageLimit); + + // These methods don't throw checked exceptions but the data is essential. + streamInfo.setLive(extractor.isLive()); + streamInfo.setAudioOnly(extractor.isAudioOnly()); + + return streamInfo; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java deleted file mode 100644 index 98d8f8c06d..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.schabi.newpipe.extractor.stream; - -// TODO: Remove this useless class -/** - * An enum representing the stream type of a {@link StreamInfo} extracted by a {@link - * StreamExtractor}. - */ -public enum StreamType { - - /** - * Placeholder to check if the stream type was checked or not. It doesn't make sense to use this - * enum constant outside of the extractor as it will never be returned by an {@link - * org.schabi.newpipe.extractor.Extractor} and is only used internally. - */ - // TODO: useless - NONE, - - /** - * A normal video stream, usually with audio. Note that the {@link StreamInfo} can also - * provide audio-only {@link AudioStream}s in addition to video or video-only {@link - * VideoStream}s. - */ - // TODO: Default - VIDEO_STREAM, - - /** - * An audio-only stream. There should be no {@link VideoStream}s available! In order to prevent - * unexpected behaviors, when {@link StreamExtractor}s return this stream type, they should - * ensure that no video stream is returned in {@link StreamExtractor#getVideoStreams()} and - * {@link StreamExtractor#getVideoOnlyStreams()}. - */ - // TODO: Can be replaced by checking #getVideoStreams or #getVideoOnlyStreams - AUDIO_STREAM, - - /** - * A video live stream, usually with audio. Note that the {@link StreamInfo} can also - * provide audio-only {@link AudioStream}s in addition to video or video-only {@link - * VideoStream}s. - */ - // TODO: Can be replaced by flag isLive - LIVE_STREAM, - - /** - * An audio-only live stream. There should be no {@link VideoStream}s available! In order to - * prevent unexpected behaviors, when {@link StreamExtractor}s return this stream type, they - * should ensure that no video stream is returned in {@link StreamExtractor#getVideoStreams()} - * and {@link StreamExtractor#getVideoOnlyStreams()}. - */ - // TODO: Can be replaced by flag isLive - AUDIO_LIVE_STREAM, - - /** - * A video live stream that has just ended but has not yet been encoded into a normal video - * stream. Note that the {@link StreamInfo} can also provide audio-only {@link - * AudioStream}s in addition to video or video-only {@link VideoStream}s. - * - *

- * Note that most of the content of an ended live video (or audio) may be extracted as {@link - * #VIDEO_STREAM regular video contents} (or {@link #AUDIO_STREAM regular audio contents}) - * later, because the service may encode them again later as normal video/audio streams. That's - * the case on YouTube, for example. - *

- */ - // TODO: Useless (only used internally inside YT) - POST_LIVE_STREAM, - - /** - * An audio live stream that has just ended but has not yet been encoded into a normal audio - * stream. There should be no {@link VideoStream}s available! In order to prevent unexpected - * behaviors, when {@link StreamExtractor}s return this stream type, they should ensure that no - * video stream is returned in {@link StreamExtractor#getVideoStreams()} and - * {@link StreamExtractor#getVideoOnlyStreams()}. - * - *

- * Note that most of ended live audio streams extracted with this value are processed as - * {@link #AUDIO_STREAM regular audio streams} later, because the service may encode them - * again later. - *

- */ - // TODO: Useless (only used internally inside YT) - POST_LIVE_AUDIO_STREAM -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java index 64707cd3a1..c3e3ace294 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Parser.java @@ -93,13 +93,16 @@ public static boolean isMatch(@Nonnull final Pattern pattern, final String input } @Nonnull - public static Map compatParseMap(@Nonnull final String input) - throws UnsupportedEncodingException { + public static Map compatParseMap(@Nonnull final String input) { final Map map = new HashMap<>(); for (final String arg : input.split("&")) { final String[] splitArg = arg.split("="); if (splitArg.length > 1) { - map.put(splitArg[0], URLDecoder.decode(splitArg[1], UTF_8)); + try { + map.put(splitArg[0], URLDecoder.decode(splitArg[1], UTF_8)); + } catch (final UnsupportedEncodingException ignored) { + // No UTF-8 = Not possible + } } else { map.put(splitArg[0], ""); } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java index d9b4e6cde2..bbff34e63c 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java @@ -1,5 +1,17 @@ package org.schabi.newpipe.extractor.services; +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.extractor.ExtractorAsserts.assertEqualsOrderIndependent; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreaterOrEqual; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsSecureUrl; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsValidUrl; +import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestListOfItems; +import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT; + import org.junit.jupiter.api.Test; import org.schabi.newpipe.extractor.ExtractorAsserts; import org.schabi.newpipe.extractor.InfoItemsCollector; @@ -9,32 +21,20 @@ import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Frameset; +import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamType; -import org.schabi.newpipe.extractor.stream.SubtitlesStream; -import org.schabi.newpipe.extractor.stream.VideoStream; -import javax.annotation.Nullable; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.net.MalformedURLException; import java.net.URL; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; -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.extractor.ExtractorAsserts.assertGreaterOrEqual; -import static org.schabi.newpipe.extractor.ExtractorAsserts.assertEqualsOrderIndependent; -import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsSecureUrl; -import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsValidUrl; -import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestListOfItems; -import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT; +import javax.annotation.Nullable; /** * Test for {@link StreamExtractor} @@ -67,7 +67,7 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest expectedMetaInfo() throws MalformedURLException { return Collections.emptyList(); } // default: no metadata info available - @Test - @Override - public void testStreamType() throws Exception { - assertEquals(expectedStreamType(), extractor().getStreamType()); - } - @Test @Override public void testUploaderName() throws Exception { diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java index b220c3d190..596367c4ad 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java @@ -43,6 +43,7 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeTestsUtils; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.Description; +import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamSegment; import org.schabi.newpipe.extractor.stream.StreamType; @@ -442,7 +443,7 @@ public static void setUp() throws Exception { @Test void testGetUnlisted() { - assertEquals(StreamExtractor.Privacy.UNLISTED, extractor.getPrivacy()); + assertEquals(Privacy.UNLISTED, extractor.getPrivacy()); } } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java index 691ec59167..4f7225e4ae 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java @@ -1,7 +1,7 @@ package org.schabi.newpipe.extractor.services.youtube.stream; import static org.schabi.newpipe.extractor.ServiceList.YouTube; -import static org.schabi.newpipe.extractor.stream.StreamExtractor.Privacy.UNLISTED; +import static org.schabi.newpipe.extractor.stream.Privacy.UNLISTED; import org.junit.jupiter.api.BeforeAll; import org.schabi.newpipe.downloader.DownloaderFactory; @@ -9,6 +9,7 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; import org.schabi.newpipe.extractor.services.youtube.YoutubeTestsUtils; +import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamType; @@ -52,7 +53,7 @@ public static void setUp() throws Exception { @Nullable @Override public String expectedTextualUploadDate() { return "2017-09-22"; } @Override public long expectedLikeCountAtLeast() { return 110; } @Override public long expectedDislikeCountAtLeast() { return -1; } - @Override public StreamExtractor.Privacy expectedPrivacy() { return UNLISTED; } + @Override public Privacy expectedPrivacy() { return UNLISTED; } @Override public String expectedLicence() { return "YouTube licence"; } @Override public String expectedCategory() { return "Gaming"; } @Override public List expectedTags() { return Arrays.asList("dark souls", "hooked", "praise the casual"); } From 32e85d3f4f27bfe9627328914e08e85e720f899e Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 10 Jun 2022 22:59:45 +0200 Subject: [PATCH 12/43] More refactoring... --- .../extractors/YoutubeStreamExtractor.java | 100 ++---------------- .../newpipe/extractor/utils/JsonUtils.java | 5 + 2 files changed, 13 insertions(+), 92 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java index a45153b552..5136e075c4 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java @@ -35,6 +35,7 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareAndroidMobileJsonBuilder; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareIosMobileJsonBuilder; +import static org.schabi.newpipe.extractor.utils.JsonUtils.getNullableInteger; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import com.grack.nanojson.JsonArray; @@ -1332,7 +1333,7 @@ private > ItagInfo buildItagInfo( final ItagInfo itagInfo = new ItagInfo<>(itagFormat, streamUrl); if (itagFormat instanceof BaseAudioItagFormat) { - final Integer averageBitrate = getIntegerFromJson(formatData, "averageBitrate"); + final Integer averageBitrate = getNullableInteger(formatData, "averageBitrate"); if (averageBitrate != null) { itagInfo.setAverageBitrate((int) Math.round(averageBitrate / 1000d)); } @@ -1343,15 +1344,15 @@ private > ItagInfo buildItagInfo( } catch (final Exception ignore) { // Ignore errors - leave default value } - itagInfo.setAudioChannels(getIntegerFromJson(formatData, "audioChannels")); + itagInfo.setAudioChannels(getNullableInteger(formatData, "audioChannels")); } if (itagFormat instanceof VideoItagFormat) { - itagInfo.setHeight(getIntegerFromJson(formatData, "height")); - itagInfo.setWidth(getIntegerFromJson(formatData, "width")); - itagInfo.setFps(getIntegerFromJson(formatData, "fps")); + itagInfo.setHeight(getNullableInteger(formatData, "height")); + itagInfo.setWidth(getNullableInteger(formatData, "width")); + itagInfo.setFps(getNullableInteger(formatData, "fps")); } - itagInfo.setBitRate(getIntegerFromJson(formatData, "bitRate")); + itagInfo.setBitRate(getNullableInteger(formatData, "bitRate")); itagInfo.setQuality(formatData.getString("quality")); final String mimeType = formatData.getString("mimeType", ""); @@ -1377,96 +1378,11 @@ private > ItagInfo buildItagInfo( } itagInfo.setType(formatData.getString("type")); - itagInfo.setTargetDurationSec(getIntegerFromJson(formatData, "targetDurationSec")); + itagInfo.setTargetDurationSec(getNullableInteger(formatData, "targetDurationSec")); return itagInfo; } - // TODO - private static Integer getIntegerFromJson(final JsonObject jsonObject, final String key) { - return (Integer) jsonObject.getNumber(key); - } - -// private ItagInfo buildItagInfo( -// @Nonnull final String videoId, -// @Nonnull final JsonObject formatData, -// @Nonnull final ItagItem itagItem, -// @Nonnull final ItagItem.ItagType itagType, -// @Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException { -// String streamUrl; -// if (formatData.has("url")) { -// streamUrl = formatData.getString("url"); -// } else { -// // This url has an obfuscated signature -// final String cipherString = formatData.has(CIPHER) -// ? formatData.getString(CIPHER) -// : formatData.getString(SIGNATURE_CIPHER); -// final Map cipher = Parser.compatParseMap(cipherString); -// streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "=" -// + deobfuscateSignature(cipher.get("s")); -// } -// -// // Add the content playback nonce to the stream URL -// streamUrl += "&" + CPN + "=" + contentPlaybackNonce; -// -// // Decrypt the n parameter if it is present -// streamUrl = tryDecryptUrl(streamUrl, videoId); -// -// final JsonObject initRange = formatData.getObject("initRange"); -// final JsonObject indexRange = formatData.getObject("indexRange"); -// final String mimeType = formatData.getString("mimeType", EMPTY_STRING); -// final String codec = mimeType.contains("codecs") -// ? mimeType.split("\"")[1] -// : EMPTY_STRING; -// -// itagItem.setBitrate(formatData.getInt("bitrate")); -// itagItem.setWidth(formatData.getInt("width")); -// itagItem.setHeight(formatData.getInt("height")); -// itagItem.setInitStart(Integer.parseInt(initRange.getString("start", "-1"))); -// itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", "-1"))); -// itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", "-1"))); -// itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", "-1"))); -// itagItem.setQuality(formatData.getString("quality")); -// itagItem.setCodec(codec); -// -// if (isLive() || isPostLive()) { -// itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec")); -// } -// -// if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) { -// itagItem.setFps(formatData.getInt("fps")); -// } -// if (itagType == ItagItem.ItagType.AUDIO) { -// // YouTube returns the audio sample rate as a string -// itagItem.setSampleRate(Integer.parseInt(formatData.getString("audioSampleRate"))); -// itagItem.setAudioChannels(formatData.getInt("audioChannels")); -// } -// -// // YouTube return the content length and the approximate duration as strings -// itagItem.setContentLength(Long.parseLong(formatData.getString( -// "contentLength", -// String.valueOf(CONTENT_LENGTH_UNKNOWN)))); -// itagItem.setApproxDurationMs(Long.parseLong(formatData.getString( -// "approxDurationMs", -// String.valueOf(APPROX_DURATION_MS_UNKNOWN)))); -// -// final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem); -// -// if (streamType == StreamType.VIDEO_STREAM) { -// itagInfo.setIsUrl(!formatData.getString("type", "") -// .equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF")); -// } else { -// // We are currently not able to generate DASH manifests for running -// // livestreams, so because of the requirements of StreamInfo -// // objects, return these streams as DASH URL streams (even if they -// // are not playable). -// // Ended livestreams are returned as non URL streams -// itagInfo.setIsUrl(streamType != StreamType.POST_LIVE_STREAM); -// } -// -// return itagInfo; -// } - @Nonnull @Override public List getFrames() throws ExtractionException { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/JsonUtils.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/JsonUtils.java index 850eba778e..2002fc1b06 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/JsonUtils.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/JsonUtils.java @@ -161,4 +161,9 @@ public static List getStringListFromJsonArray(@Nonnull final JsonArray a .map(String.class::cast) .collect(Collectors.toList()); } + + public static Integer getNullableInteger(@Nonnull final JsonObject jsonObject, + @Nonnull final String key) { + return (Integer) jsonObject.getNumber(key); + } } From a2df180d443bed457bfae81b420357d53e205b94 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 10 Jun 2022 23:25:54 +0200 Subject: [PATCH 13/43] Fix StreamInfoItem --- .../BandcampRadioInfoItemExtractor.java | 12 +++---- ...BandcampSearchStreamInfoItemExtractor.java | 6 +--- .../BandcampStreamInfoItemExtractor.java | 7 ++-- .../MediaCCCLiveStreamKioskExtractor.java | 21 ++++++------ .../MediaCCCRecentKioskExtractor.java | 6 ---- .../MediaCCCStreamInfoItemExtractor.java | 7 +--- .../PeertubeStreamInfoItemExtractor.java | 6 ++-- .../SoundcloudStreamInfoItemExtractor.java | 11 +++---- .../YoutubeFeedInfoItemExtractor.java | 11 ++----- .../YoutubeStreamInfoItemExtractor.java | 33 ++++++++++--------- .../newpipe/extractor/stream/StreamInfo.java | 30 +++++++---------- .../extractor/stream/StreamInfoItem.java | 17 +++++++--- .../stream/StreamInfoItemExtractor.java | 18 +++++++--- .../stream/StreamInfoItemsCollector.java | 6 +++- 14 files changed, 91 insertions(+), 100 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioInfoItemExtractor.java index be6ba0b7ad..9992d985c0 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioInfoItemExtractor.java @@ -2,17 +2,17 @@ package org.schabi.newpipe.extractor.services.bandcamp.extractors; +import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL; +import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl; + import com.grack.nanojson.JsonObject; + 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 javax.annotation.Nullable; -import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL; -import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl; - public class BandcampRadioInfoItemExtractor implements StreamInfoItemExtractor { private final JsonObject show; @@ -58,8 +58,8 @@ public String getThumbnailUrl() { } @Override - public StreamType getStreamType() { - return StreamType.AUDIO_STREAM; + public boolean isAudioOnly() { + return true; } @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java index 5e585e7e00..ef384496fc 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java @@ -22,11 +22,7 @@ public BandcampSearchStreamInfoItemExtractor(final Element searchResult, public String getUploaderName() { final String subhead = resultInfo.getElementsByClass("subhead").text(); final String[] splitBy = subhead.split("by "); - if (splitBy.length > 1) { - return splitBy[1]; - } else { - return splitBy[0]; - } + return splitBy.length > 1 ? splitBy[1] : splitBy[0]; } @Nullable diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampStreamInfoItemExtractor.java index fee96fcd97..07ca6d7ed9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampStreamInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampStreamInfoItemExtractor.java @@ -3,7 +3,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 javax.annotation.Nullable; @@ -13,13 +12,13 @@ public abstract class BandcampStreamInfoItemExtractor implements StreamInfoItemExtractor { private final String uploaderUrl; - public BandcampStreamInfoItemExtractor(final String uploaderUrl) { + protected BandcampStreamInfoItemExtractor(final String uploaderUrl) { this.uploaderUrl = uploaderUrl; } @Override - public StreamType getStreamType() { - return StreamType.AUDIO_STREAM; + public boolean isAudioOnly() { + return true; } @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java index 2b311fb352..69b78dc791 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java @@ -1,10 +1,10 @@ package org.schabi.newpipe.extractor.services.media_ccc.extractors; import com.grack.nanojson.JsonObject; + 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 javax.annotation.Nullable; @@ -38,15 +38,16 @@ public String getThumbnailUrl() throws ParsingException { } @Override - public StreamType getStreamType() throws ParsingException { - boolean isVideo = false; - for (final Object stream : roomInfo.getArray("streams")) { - if ("video".equals(((JsonObject) stream).getString("type"))) { - isVideo = true; - break; - } - } - return isVideo ? StreamType.LIVE_STREAM : StreamType.AUDIO_LIVE_STREAM; + public boolean isLive() { + return true; + } + + @Override + public boolean isAudioOnly() { + return roomInfo.getArray("streams").stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .noneMatch(s -> "video".equalsIgnoreCase(s.getString("type"))); } @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCRecentKioskExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCRecentKioskExtractor.java index 1e18f8da13..386d6c2ce2 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCRecentKioskExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCRecentKioskExtractor.java @@ -6,7 +6,6 @@ import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferenceLinkHandlerFactory; import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -36,11 +35,6 @@ public String getThumbnailUrl() throws ParsingException { return event.getString("thumb_url"); } - @Override - public StreamType getStreamType() throws ParsingException { - return StreamType.VIDEO_STREAM; - } - @Override public boolean isAd() { return false; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/infoItems/MediaCCCStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/infoItems/MediaCCCStreamInfoItemExtractor.java index 92f0894b9f..293d825f33 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/infoItems/MediaCCCStreamInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/infoItems/MediaCCCStreamInfoItemExtractor.java @@ -1,11 +1,11 @@ package org.schabi.newpipe.extractor.services.media_ccc.extractors.infoItems; import com.grack.nanojson.JsonObject; + import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import javax.annotation.Nullable; @@ -16,11 +16,6 @@ public MediaCCCStreamInfoItemExtractor(final JsonObject event) { this.event = event; } - @Override - public StreamType getStreamType() { - return StreamType.VIDEO_STREAM; - } - @Override public boolean isAd() { return false; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamInfoItemExtractor.java index b9c7f4b13e..9cc6afeb80 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamInfoItemExtractor.java @@ -1,12 +1,12 @@ package org.schabi.newpipe.extractor.services.peertube.extractors; import com.grack.nanojson.JsonObject; + import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.utils.JsonUtils; import javax.annotation.Nullable; @@ -93,8 +93,8 @@ public DateWrapper getUploadDate() throws ParsingException { } @Override - public StreamType getStreamType() { - return item.getBoolean("isLive") ? StreamType.LIVE_STREAM : StreamType.VIDEO_STREAM; + public boolean isLive() { + return item.getBoolean("isLive"); } @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java index 3d265e4e41..8e9e5878bb 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java @@ -8,7 +8,6 @@ import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import javax.annotation.Nullable; @@ -20,6 +19,11 @@ public SoundcloudStreamInfoItemExtractor(final JsonObject itemObject) { this.itemObject = itemObject; } + @Override + public boolean isAudioOnly() { + return true; + } + @Override public String getUrl() { return replaceHttpWithHttps(itemObject.getString("permalink_url")); @@ -80,11 +84,6 @@ public String getThumbnailUrl() { return artworkUrl.replace("large.jpg", "crop.jpg"); } - @Override - public StreamType getStreamType() { - return StreamType.AUDIO_STREAM; - } - @Override public boolean isAd() { return false; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedInfoItemExtractor.java index a79586e8f4..ba8f4f8d07 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedInfoItemExtractor.java @@ -4,12 +4,12 @@ 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 javax.annotation.Nullable; import java.time.OffsetDateTime; import java.time.format.DateTimeParseException; +import javax.annotation.Nullable; + public class YoutubeFeedInfoItemExtractor implements StreamInfoItemExtractor { private final Element entryElement; @@ -17,13 +17,6 @@ public YoutubeFeedInfoItemExtractor(final Element entryElement) { this.entryElement = entryElement; } - @Override - public StreamType getStreamType() { - // It is not possible to determine the stream type using the feed endpoint. - // All entries are considered a video stream. - return StreamType.VIDEO_STREAM; - } - @Override public boolean isAd() { return false; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java index d731729519..239b254a63 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java @@ -14,7 +14,6 @@ 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.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.Utils; @@ -46,7 +45,7 @@ public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor { private final JsonObject videoInfo; private final TimeAgoParser timeAgoParser; - private StreamType cachedStreamType; + private Boolean cachedIsLive; /** * Creates an extractor of StreamInfoItems from a YouTube page. @@ -61,19 +60,23 @@ public YoutubeStreamInfoItemExtractor(final JsonObject videoInfoItem, } @Override - public StreamType getStreamType() { - if (cachedStreamType != null) { - return cachedStreamType; + public boolean isLive() { + if (cachedIsLive != null) { + return cachedIsLive; } + cachedIsLive = determineIfIsLive(); + return cachedIsLive; + } + + private boolean determineIfIsLive() { final JsonArray badges = videoInfo.getArray("badges"); for (final Object badge : badges) { final JsonObject badgeRenderer = ((JsonObject) badge).getObject("metadataBadgeRenderer"); - if (badgeRenderer.getString("style", "").equals("BADGE_STYLE_TYPE_LIVE_NOW") - || badgeRenderer.getString("label", "").equals("LIVE NOW")) { - cachedStreamType = StreamType.LIVE_STREAM; - return cachedStreamType; + if ("BADGE_STYLE_TYPE_LIVE_NOW".equals(badgeRenderer.getString("style")) + || "LIVE NOW".equals(badgeRenderer.getString("label"))) { + return true; } } @@ -82,13 +85,11 @@ public StreamType getStreamType() { .getObject("thumbnailOverlayTimeStatusRenderer") .getString("style", ""); if (style.equalsIgnoreCase("LIVE")) { - cachedStreamType = StreamType.LIVE_STREAM; - return cachedStreamType; + return true; } } - cachedStreamType = StreamType.VIDEO_STREAM; - return cachedStreamType; + return false; } @Override @@ -118,7 +119,7 @@ public String getName() throws ParsingException { @Override public long getDuration() throws ParsingException { - if (getStreamType() == StreamType.LIVE_STREAM || isPremiere()) { + if (isLive() || isPremiere()) { return -1; } @@ -212,7 +213,7 @@ public boolean isUploaderVerified() throws ParsingException { @Nullable @Override public String getTextualUploadDate() throws ParsingException { - if (getStreamType().equals(StreamType.LIVE_STREAM)) { + if (isLive()) { return null; } @@ -232,7 +233,7 @@ public String getTextualUploadDate() throws ParsingException { @Nullable @Override public DateWrapper getUploadDate() throws ParsingException { - if (getStreamType().equals(StreamType.LIVE_STREAM)) { + if (isLive()) { return null; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index 9734dda49d..14511946a7 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -81,8 +81,8 @@ public class StreamInfo extends Info { @Nonnull private String hlsMasterPlaylistUrl = ""; - private boolean audioOnly = false; - private boolean live = false; + private final boolean audioOnly; + private final boolean live; private List relatedItems = new ArrayList<>(); @@ -104,9 +104,13 @@ public StreamInfo(final int serviceId, final String originalUrl, final String id, final String name, - final int ageLimit) { + final int ageLimit, + final boolean audioOnly, + final boolean live) { super(serviceId, id, url, originalUrl, name); this.ageLimit = ageLimit; + this.audioOnly = audioOnly; + this.live = live; } /** @@ -319,18 +323,10 @@ public boolean isAudioOnly() { return audioOnly; } - public void setAudioOnly(final boolean audioOnly) { - this.audioOnly = audioOnly; - } - public boolean isLive() { return live; } - public void setLive(final boolean live) { - this.live = live; - } - public List getRelatedItems() { return relatedItems; } @@ -495,19 +491,15 @@ private static StreamInfo extractImportantData(@Nonnull final StreamExtractor ex throw new ExtractionException("Some important stream information was not given"); } - final StreamInfo streamInfo = new StreamInfo( + return new StreamInfo( extractor.getServiceId(), url, extractor.getOriginalUrl(), id, name, - ageLimit); - - // These methods don't throw checked exceptions but the data is essential. - streamInfo.setLive(extractor.isLive()); - streamInfo.setAudioOnly(extractor.isAudioOnly()); - - return streamInfo; + ageLimit, + extractor.isAudioOnly(), + extractor.isLive()); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java index 72e1454cad..996a47938e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java @@ -29,7 +29,8 @@ * Info object for previews of unopened videos, eg search results, related videos */ public class StreamInfoItem extends InfoItem { - private final StreamType streamType; + private final boolean audioOnly; + private final boolean live; private String uploaderName; private String shortDescription; @@ -46,13 +47,19 @@ public class StreamInfoItem extends InfoItem { public StreamInfoItem(final int serviceId, final String url, final String name, - final StreamType streamType) { + final boolean audioOnly, + final boolean live) { super(InfoType.STREAM, serviceId, url, name); - this.streamType = streamType; + this.audioOnly = audioOnly; + this.live = live; } - public StreamType getStreamType() { - return streamType; + public boolean isAudioOnly() { + return audioOnly; + } + + public boolean isLive() { + return live; } public String getUploaderName() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemExtractor.java index 131719d8c3..ea06e6345c 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemExtractor.java @@ -30,12 +30,22 @@ public interface StreamInfoItemExtractor extends InfoItemExtractor { /** - * Get the stream type + * This will return whenever there are only audio streams available. * - * @return the stream type - * @throws ParsingException thrown if there is an error in the extraction + * @return true when audio only otherwise false */ - StreamType getStreamType() throws ParsingException; + default boolean isAudioOnly() { + return false; + } + + /** + * This will return whenever the current stream is live. + * + * @return true when live otherwise false + */ + default boolean isLive() { + return false; + } /** * Check if the stream is an ad. diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemsCollector.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemsCollector.java index 231a929e58..d2d1b527a7 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemsCollector.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemsCollector.java @@ -45,7 +45,11 @@ public StreamInfoItem extract(final StreamInfoItemExtractor extractor) throws Pa } final StreamInfoItem resultItem = new StreamInfoItem( - getServiceId(), extractor.getUrl(), extractor.getName(), extractor.getStreamType()); + getServiceId(), + extractor.getUrl(), + extractor.getName(), + extractor.isAudioOnly(), + extractor.isLive()); // optional information try { From 474c3cd7388e899132dfeb137a5b266f9037f6a3 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 10 Jun 2022 23:36:16 +0200 Subject: [PATCH 14/43] Fix more compile problems --- .../BandcampRadioStreamExtractor.java | 29 ++++++++++--------- .../extractor/stream/StreamInfoItem.java | 3 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java index 03a9c2222b..dcc2815274 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java @@ -11,7 +11,6 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Element; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; @@ -21,9 +20,12 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.StreamSegment; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; +import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; import java.io.IOException; import java.util.ArrayList; @@ -120,24 +122,23 @@ public long getLength() { @Override public List getAudioStreams() { - final List audioStreams = new ArrayList<>(); final JsonObject streams = showInfo.getObject("audio_stream"); + final List audioStreams = new ArrayList<>(); if (streams.has(MP3_128)) { - audioStreams.add(new AudioStream.Builder() - .setId(MP3_128) - .setContent(streams.getString(MP3_128), true) - .setMediaFormat(MediaFormat.MP3) - .setAverageBitrate(128) - .build()); + audioStreams.add(new SimpleAudioStreamImpl( + AudioFormatRegistry.MP3, + new SimpleProgressiveHTTPDeliveryDataImpl(streams.getString(MP3_128)), + 128 + )); } if (streams.has(OPUS_LO)) { - audioStreams.add(new AudioStream.Builder() - .setId(OPUS_LO) - .setContent(streams.getString(OPUS_LO), true) - .setMediaFormat(MediaFormat.OPUS) - .setAverageBitrate(100).build()); + audioStreams.add(new SimpleAudioStreamImpl( + AudioFormatRegistry.OPUS, + new SimpleProgressiveHTTPDeliveryDataImpl(streams.getString(OPUS_LO)), + 100 + )); } return audioStreams; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java index 996a47938e..b88a275068 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java @@ -140,7 +140,8 @@ public void setUploaderVerified(final boolean uploaderVerified) { @Override public String toString() { return "StreamInfoItem{" - + "streamType=" + streamType + + "audioOnly=" + audioOnly + + ", live='" + live + '\'' + ", uploaderName='" + uploaderName + '\'' + ", textualUploadDate='" + textualUploadDate + '\'' + ", viewCount=" + viewCount From cf77bf5974008d65a528b8b2346354d3cd7686ee Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 12 Jun 2022 17:04:58 +0200 Subject: [PATCH 15/43] Use better name for meethod --- .../youtube/extractors/YoutubeStreamExtractor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java index 5136e075c4..75e6037584 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java @@ -597,7 +597,7 @@ private static String getManifestUrl( @Override public List getAudioStreams() throws ExtractionException { - return getItags( + return buildStrems( ADAPTIVE_FORMATS, ItagFormatRegistry.AUDIO_FORMATS, (itagInfo, deliveryData) -> new SimpleAudioStreamImpl( @@ -609,7 +609,7 @@ public List getAudioStreams() throws ExtractionException { @Override public List getVideoStreams() throws ExtractionException { - return getItags(FORMATS, + return buildStrems(FORMATS, ItagFormatRegistry.VIDEO_AUDIO_FORMATS, (itagInfo, deliveryData) -> new SimpleVideoAudioStreamImpl( itagInfo.getItagFormat().mediaFormat(), @@ -621,7 +621,7 @@ public List getVideoStreams() throws ExtractionException { @Override public List getVideoOnlyStreams() throws ExtractionException { - return getItags( + return buildStrems( ADAPTIVE_FORMATS, ItagFormatRegistry.VIDEO_FORMATS, (itagInfo, deliveryData) -> new SimpleVideoStreamImpl( @@ -1204,7 +1204,7 @@ private JsonObject getVideoSecondaryInfoRenderer() throws ParsingException { @Nonnull private , - I extends ItagFormat> List getItags( + I extends ItagFormat> List buildStrems( final String streamingDataKey, final I[] itagFormats, final BiFunction, DeliveryData, T> streamBuilder, From 862ed8762db8de3aeb5c0edba8872a4d2ad0aeb7 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Mon, 13 Jun 2022 22:15:16 +0200 Subject: [PATCH 16/43] Added validation for ``MediaFormat``s --- .../streamdata/format/AbstractMediaFormat.java | 17 +++++++++++------ .../streamdata/format/AudioMediaFormat.java | 8 +++++--- .../streamdata/format/MediaFormat.java | 5 +++++ .../streamdata/format/SubtitleMediaFormat.java | 8 +++++--- .../format/VideoAudioMediaFormat.java | 8 +++++--- 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java index adb770b120..4420f66518 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java @@ -2,6 +2,8 @@ import java.util.Objects; +import javax.annotation.Nonnull; + public abstract class AbstractMediaFormat implements MediaFormat { private final int id; private final String name; @@ -10,14 +12,14 @@ public abstract class AbstractMediaFormat implements MediaFormat { protected AbstractMediaFormat( final int id, - final String name, - final String suffix, - final String mimeType + @Nonnull final String name, + @Nonnull final String suffix, + @Nonnull final String mimeType ) { this.id = id; - this.name = name; - this.suffix = suffix; - this.mimeType = mimeType; + this.name = Objects.requireNonNull(name); + this.suffix = Objects.requireNonNull(suffix); + this.mimeType = Objects.requireNonNull(mimeType); } @Override @@ -25,16 +27,19 @@ public int id() { return id; } + @Nonnull @Override public String name() { return name; } + @Nonnull @Override public String suffix() { return suffix; } + @Nonnull @Override public String mimeType() { return mimeType; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java index 8da6af9feb..bce4fd7f14 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AudioMediaFormat.java @@ -1,11 +1,13 @@ package org.schabi.newpipe.extractor.streamdata.format; +import javax.annotation.Nonnull; + public class AudioMediaFormat extends AbstractMediaFormat { public AudioMediaFormat( final int id, - final String name, - final String suffix, - final String mimeType + @Nonnull final String name, + @Nonnull final String suffix, + @Nonnull final String mimeType ) { super(id, name, suffix, mimeType); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/MediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/MediaFormat.java index bfea6ffd34..9448916890 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/MediaFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/MediaFormat.java @@ -1,12 +1,17 @@ package org.schabi.newpipe.extractor.streamdata.format; +import javax.annotation.Nonnull; + public interface MediaFormat { int id(); + @Nonnull String name(); + @Nonnull String suffix(); + @Nonnull String mimeType(); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/SubtitleMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/SubtitleMediaFormat.java index c860eb31dd..62c693d20d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/SubtitleMediaFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/SubtitleMediaFormat.java @@ -1,11 +1,13 @@ package org.schabi.newpipe.extractor.streamdata.format; +import javax.annotation.Nonnull; + public class SubtitleMediaFormat extends AbstractMediaFormat { public SubtitleMediaFormat( final int id, - final String name, - final String suffix, - final String mimeType + @Nonnull final String name, + @Nonnull final String suffix, + @Nonnull final String mimeType ) { super(id, name, suffix, mimeType); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/VideoAudioMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/VideoAudioMediaFormat.java index 4250fd39cb..8120b08558 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/VideoAudioMediaFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/VideoAudioMediaFormat.java @@ -1,11 +1,13 @@ package org.schabi.newpipe.extractor.streamdata.format; +import javax.annotation.Nonnull; + public class VideoAudioMediaFormat extends AbstractMediaFormat { public VideoAudioMediaFormat( final int id, - final String name, - final String suffix, - final String mimeType + @Nonnull final String name, + @Nonnull final String suffix, + @Nonnull final String mimeType ) { super(id, name, suffix, mimeType); } From 1d0a27cd60d901f32f2753b80dbe1ce6b1ab7f4c Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Mon, 13 Jun 2022 22:27:31 +0200 Subject: [PATCH 17/43] Abstracted YT Dash manifest creators and made them instantiable --- .../services/youtube/DeliveryType.java | 56 -- .../AbstractYoutubeDashManifestCreator.java | 650 +++++++++++++++ .../YoutubeOtfDashManifestCreator.java | 162 ++++ ...ePostLiveStreamDvrDashManifestCreator.java | 121 +++ ...YoutubeProgressiveDashManifestCreator.java | 159 ++++ .../CreationException.java | 63 -- .../YoutubeDashManifestCreatorsUtils.java | 756 ------------------ .../YoutubeOtfDashManifestCreator.java | 265 ------ ...ePostLiveStreamDvrDashManifestCreator.java | 217 ----- ...YoutubeProgressiveDashManifestCreator.java | 235 ------ .../youtube/itag/info/ItagInfoRange.java | 12 +- .../DashManifestCreationException.java | 56 ++ .../DashManifestCreator.java | 14 + .../DashManifestCreatorConstants.java | 21 + .../YoutubeDashManifestCreatorsTest.java | 61 +- 15 files changed, 1224 insertions(+), 1624 deletions(-) delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/CreationException.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubePostLiveStreamDvrDashManifestCreator.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreationException.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreatorConstants.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java deleted file mode 100644 index 5ce0010f8c..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube; - -/** - * Streaming format types used by YouTube in their streams. - * - *

- * It is different from {@link org.schabi.newpipe.extractor.stream.DeliveryMethod delivery methods}! - *

- */ -// TODO: Kill -public enum DeliveryType { - - /** - * YouTube's progressive delivery method, which works with HTTP range headers. - * (Note that official clients use the corresponding parameter instead.) - * - *

- * Initialization and index ranges are available to get metadata (the corresponding values - * are returned in the player response). - *

- */ - PROGRESSIVE, - - /** - * YouTube's OTF delivery method which uses a sequence parameter to get segments of - * streams. - * - *

- * The first sequence (which can be fetched with the {@code &sq=0} parameter) contains all the - * metadata needed to build the stream source (sidx boxes, segment length, segment count, - * duration, ...). - *

- * - *

- * Only used for videos; mostly those with a small amount of views, or ended livestreams - * which have just been re-encoded as normal videos. - *

- */ - OTF, - - /** - * YouTube's delivery method for livestreams which uses a sequence parameter to get - * segments of streams. - * - *

- * Each sequence (which can be fetched with the {@code &sq=0} parameter) contains its own - * metadata (sidx boxes, segment length, ...), which make no need of an initialization - * segment. - *

- * - *

- * Only used for livestreams (ended or running). - *

- */ - LIVE -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java new file mode 100644 index 0000000000..95a4fc906a --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java @@ -0,0 +1,650 @@ +package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator; + +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.addClientInfoHeaders; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.ADAPTATION_SET; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.AUDIO_CHANNEL_CONFIGURATION; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.MPD; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.PERIOD; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.REPRESENTATION; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.ROLE; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TEMPLATE; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TIMELINE; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.downloader.Downloader; +import org.schabi.newpipe.extractor.downloader.Response; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.services.youtube.itag.format.AudioItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat; +import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreator; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; +import org.w3c.dom.Attr; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +import javax.annotation.Nonnull; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +// TODO: Doc +public abstract class AbstractYoutubeDashManifestCreator implements DashManifestCreator { + + /** + * The redirect count limit that this class uses, which is the same limit as OkHttp. + */ + public static final int MAXIMUM_REDIRECT_COUNT = 20; + + /** + * URL parameter of the first sequence for live, post-live-DVR and OTF streams. + */ + public static final String SQ_0 = "&sq=0"; + + /** + * URL parameter of the first stream request made by official clients. + */ + public static final String RN_0 = "&rn=0"; + + /** + * URL parameter specific to web clients. When this param is added, if a redirection occurs, + * the server will not redirect clients to the redirect URL. Instead, it will provide this URL + * as the response body. + */ + public static final String ALR_YES = "&alr=yes"; + + protected final ItagInfo itagInfo; + protected final long durationSecondsFallback; + + protected Document document; + + protected AbstractYoutubeDashManifestCreator( + @Nonnull final ItagInfo itagInfo, + final long durationSecondsFallback) { + this.itagInfo = Objects.requireNonNull(itagInfo); + this.durationSecondsFallback = durationSecondsFallback; + } + + protected boolean isLiveDelivery() { + return false; + } + + // region generate manifest elements + + /** + * Generate a {@link Document} with common manifest creator elements added to it. + * + *

+ * Those are: + *

    + *
  • {@code MPD} (using {@link #generateDocumentAndMpdElement(long)});
  • + *
  • {@code Period} (using {@link #generatePeriodElement()});
  • + *
  • {@code AdaptationSet} (using {@link #generateAdaptationSetElement()});
  • + *
  • {@code Role} (using {@link #generateRoleElement()});
  • + *
  • {@code Representation} (using {@link #generateRepresentationElement()});
  • + *
  • and, for audio streams, {@code AudioChannelConfiguration} (using + * {@link #generateAudioChannelConfigurationElement()}).
  • + *
+ *

+ * + * @param streamDurationMs the duration of the stream, in milliseconds + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateDocumentAndCommonElements(final long streamDurationMs) { + generateDocumentAndMpdElement(streamDurationMs); + + generatePeriodElement(); + generateAdaptationSetElement(); + generateRoleElement(); + generateRepresentationElement(); + if (itagInfo.getItagFormat() instanceof AudioItagFormat) { + generateAudioChannelConfigurationElement(); + } + } + + /** + * Create a {@link Document} instance and generate the {@code } element of the manifest. + * + *

+ * The generated {@code } element looks like the manifest returned into the player + * response of videos: + *

+ * + *

+ * {@code } + * (where {@code $duration$} represents the duration in seconds (a number with 3 digits after + * the decimal point)). + *

+ * + * @param durationMs the duration of the stream, in milliseconds + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateDocumentAndMpdElement(final long durationMs) { + try { + newDocument(); + + final Element mpdElement = createElement(MPD); + document.appendChild(mpdElement); + + appendNewAttrWithValue( + mpdElement, "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + + appendNewAttrWithValue(mpdElement, "xmlns", "urn:mpeg:DASH:schema:MPD:2011"); + + appendNewAttrWithValue( + mpdElement, + "xsi:schemaLocation", + "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"); + + appendNewAttrWithValue(mpdElement, "minBufferTime", "PT1.500S"); + + appendNewAttrWithValue( + mpdElement, "profiles", "urn:mpeg:dash:profile:full:2011"); + + appendNewAttrWithValue(mpdElement, "type", "static"); + + final String durationSeconds = + String.format(Locale.ENGLISH, "%.3f", durationMs / 1000.0); + appendNewAttrWithValue( + mpdElement, "mediaPresentationDuration", "PT" + durationSeconds + "S"); + } catch (final Exception e) { + throw new DashManifestCreationException( + "Could not generate the DASH manifest or append the MPD document to it", e); + } + } + + /** + * Generate the {@code } element, appended as a child of the {@code } element. + * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateDocumentAndMpdElement(long)}. + *

+ * + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generatePeriodElement() { + try { + getFirstElementByName(MPD).appendChild(createElement(PERIOD)); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(PERIOD, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the {@code } + * element. + * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generatePeriodElement()}. + *

+ * + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateAdaptationSetElement() { + try { + final Element adaptationSetElement = createElement(ADAPTATION_SET); + + appendNewAttrWithValue(adaptationSetElement, "id", "0"); + + appendNewAttrWithValue( + adaptationSetElement, + "mimeType", + itagInfo.getItagFormat().mediaFormat().mimeType()); + + appendNewAttrWithValue(adaptationSetElement, "subsegmentAlignment", "true"); + + getFirstElementByName(PERIOD).appendChild(adaptationSetElement); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(ADAPTATION_SET, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the {@code } + * element. + * + *

+ * This element, with its attributes and values, is: + *

+ * + *

+ * {@code } + *

+ * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateAdaptationSetElement(Document)}). + *

+ * + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateRoleElement() { + try { + final Element roleElement = createElement(ROLE); + + appendNewAttrWithValue(roleElement, "schemeIdUri", "urn:mpeg:DASH:role:2011"); + + appendNewAttrWithValue(roleElement, "value", "main"); + + getFirstElementByName(ADAPTATION_SET).appendChild(roleElement); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(ROLE, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateAdaptationSetElement()}). + *

+ * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateRepresentationElement() { + try { + final Element representationElement = createElement(REPRESENTATION); + + appendNewAttrWithValue( + representationElement, "id", itagInfo.getItagFormat().id()); + + final String codec = itagInfo.getCodec(); + if (isNullOrEmpty(codec)) { + throw DashManifestCreationException.couldNotAddElement(ADAPTATION_SET, + "the codec value of the ItagItem is null or empty"); + } + + appendNewAttrWithValue( + representationElement, "codecs", codec); + + appendNewAttrWithValue( + representationElement, "startWithSAP", "1"); + + appendNewAttrWithValue( + representationElement, "maxPlayoutRate", "1"); + + final Integer bitrate = itagInfo.getBitRate(); + if (bitrate == null || bitrate <= 0) { + throw DashManifestCreationException.couldNotAddElement( + REPRESENTATION, + "invalid bitrate=" + bitrate); + } + + appendNewAttrWithValue( + representationElement, "bandwidth", bitrate); + + if (itagInfo.getItagFormat() instanceof VideoItagFormat) { + + final VideoQualityData videoQualityData = itagInfo.getCombinedVideoQualityData(); + + if (videoQualityData.height() <= 0 && videoQualityData.width() <= 0) { + throw DashManifestCreationException.couldNotAddElement( + REPRESENTATION, + "both width and height are <= 0"); + } + + if (videoQualityData.width() > 0) { + appendNewAttrWithValue( + representationElement, "width", videoQualityData.width()); + } + + appendNewAttrWithValue( + representationElement, "height", videoQualityData.height()); + + if (videoQualityData.fps() > 0) { + appendNewAttrWithValue( + representationElement, "height", videoQualityData.fps()); + } + } + + if (itagInfo.getItagFormat() instanceof AudioItagFormat + && itagInfo.getAudioSampleRate() != null + && itagInfo.getAudioSampleRate() > 0) { + + appendNewAttrWithValue( + representationElement, + "audioSamplingRate", + itagInfo.getAudioSampleRate()); + } + + getFirstElementByName(ADAPTATION_SET).appendChild(representationElement); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(REPRESENTATION, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * This method is only used when generating DASH manifests of audio streams. + *

+ * + *

+ * It will produce the following element: + *
+ * {@code + * (where {@code audioChannelsValue} is get from the {@link ItagInfo} passed as the second + * parameter of this method) + *

+ * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateRepresentationElement()}). + *

+ * + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateAudioChannelConfigurationElement() { + try { + final Element audioChannelConfigElement = createElement(AUDIO_CHANNEL_CONFIGURATION); + + appendNewAttrWithValue( + audioChannelConfigElement, + "schemeIdUri", + "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"); + + final Integer audioChannels = itagInfo.getAudioChannels(); + if (audioChannels != null && audioChannels <= 0) { + throw new DashManifestCreationException( + "Invalid value for 'audioChannels'=" + audioChannels); + } + + appendNewAttrWithValue( + audioChannelConfigElement, "value", itagInfo.getAudioChannels()); + + getFirstElementByName(REPRESENTATION).appendChild(audioChannelConfigElement); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(AUDIO_CHANNEL_CONFIGURATION, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * This method is only used when generating DASH manifests from OTF and post-live-DVR streams. + *

+ * + *

+ * It will produce a {@code } element with the following attributes: + *

    + *
  • {@code startNumber}, which takes the value {@code 0} for post-live-DVR streams and + * {@code 1} for OTF streams;
  • + *
  • {@code timescale}, which is always {@code 1000};
  • + *
  • {@code media}, which is the base URL of the stream on which is appended + * {@code &sq=$Number$};
  • + *
  • {@code initialization} (only for OTF streams), which is the base URL of the stream + * on which is appended {@link #SQ_0}.
  • + *
+ *

+ * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateRepresentationElement()}). + *

+ * + * @param baseUrl the base URL of the OTF/post-live-DVR stream + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateSegmentTemplateElement(@Nonnull final String baseUrl) { + try { + final Element segmentTemplateElement = createElement(SEGMENT_TEMPLATE); + + // The first sequence of post DVR streams is the beginning of the video stream and not + // an initialization segment + appendNewAttrWithValue( + segmentTemplateElement, "startNumber", isLiveDelivery() ? "0" : "1"); + + appendNewAttrWithValue( + segmentTemplateElement, "timescale", "1000"); + + // Post-live-DVR/ended livestreams streams don't require an initialization sequence + if (!isLiveDelivery()) { + appendNewAttrWithValue( + segmentTemplateElement, "initialization", baseUrl + SQ_0); + } + + appendNewAttrWithValue( + segmentTemplateElement, "media", baseUrl + "&sq=$Number$"); + + getFirstElementByName(REPRESENTATION).appendChild(segmentTemplateElement); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(SEGMENT_TEMPLATE, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateSegmentTemplateElement(String)}. + *

+ * + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateSegmentTimelineElement() + throws DashManifestCreationException { + try { + final Element segmentTemplateElement = getFirstElementByName(SEGMENT_TEMPLATE); + final Element segmentTimelineElement = createElement(SEGMENT_TIMELINE); + + segmentTemplateElement.appendChild(segmentTimelineElement); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(SEGMENT_TIMELINE, e); + } + } + // endregion + + // region initResponse + + @Nonnull + protected Response getInitializationResponse(@Nonnull String baseStreamingUrl) { + final boolean isHtml5StreamingUrl = isWebStreamingUrl(baseStreamingUrl) + || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(baseStreamingUrl); + final boolean isAndroidStreamingUrl = isAndroidStreamingUrl(baseStreamingUrl); + final boolean isIosStreamingUrl = isIosStreamingUrl(baseStreamingUrl); + if (isHtml5StreamingUrl) { + baseStreamingUrl += ALR_YES; + } + baseStreamingUrl = appendBaseStreamingUrlParams(baseStreamingUrl); + + final Downloader downloader = NewPipe.getDownloader(); + if (isHtml5StreamingUrl) { + return getStreamingWebUrlWithoutRedirects(downloader, baseStreamingUrl); + } else if (isAndroidStreamingUrl || isIosStreamingUrl) { + try { + final Map> headers = new HashMap<>(); + headers.put("User-Agent", Collections.singletonList( + isAndroidStreamingUrl + ? getAndroidUserAgent(null) + : getIosUserAgent(null))); + final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8); + return downloader.post(baseStreamingUrl, headers, emptyBody); + } catch (final IOException | ExtractionException e) { + throw new DashManifestCreationException("Could not get the " + + (isIosStreamingUrl ? "IOS" : "ANDROID") + " streaming URL response", e); + } + } + + try { + return downloader.get(baseStreamingUrl); + } catch (final IOException | ExtractionException e) { + throw new DashManifestCreationException("Could not get the streaming URL response", e); + } + } + + @Nonnull + protected Response getStreamingWebUrlWithoutRedirects( + @Nonnull final Downloader downloader, + @Nonnull String streamingUrl) { + try { + final Map> headers = new HashMap<>(); + addClientInfoHeaders(headers); + + for (int r = 0; r < MAXIMUM_REDIRECT_COUNT; r++) { + final Response response = downloader.get(streamingUrl, headers); + + final int responseCode = response.responseCode(); + if (responseCode != 200) { + throw new DashManifestCreationException( + "Could not get the initialization URL: HTTP response code " + + responseCode); + } + + // A valid HTTP 1.0+ response should include a Content-Type header, so we can + // require that the response from video servers has this header. + final String responseMimeType = + Objects.requireNonNull( + response.getHeader("Content-Type"), + "Could not get the Content-Type header from the response headers"); + + // The response body is not the redirection URL + if (!responseMimeType.equals("text/plain")) { + return response; + } + + streamingUrl = response.responseBody(); + } + + throw new DashManifestCreationException("Too many redirects"); + + } catch (final IOException | ExtractionException e) { + throw new DashManifestCreationException( + "Could not get the streaming URL response of a HTML5 client", e); + } + } + + @Nonnull + protected String appendBaseStreamingUrlParams(@Nonnull final String baseStreamingUrl) { + return baseStreamingUrl + SQ_0 + RN_0; + } + + // endregion + + // region document util + + /** + * Generate a new {@link DocumentBuilder} secured from XXE attacks, on platforms which + * support setting {@link XMLConstants#ACCESS_EXTERNAL_DTD} and + * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link DocumentBuilderFactory} instances. + */ + protected void newDocument() throws ParserConfigurationException { + final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + try { + documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } catch (final Exception ignored) { + // Ignore exceptions as setting these attributes to secure XML generation is not + // supported by all platforms (like the Android implementation) + } + + final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + document = documentBuilder.newDocument(); + } + + protected Attr appendNewAttrWithValue( + final Element baseElement, + final String name, + final Object value + ) { + return appendNewAttrWithValue(baseElement, name, String.valueOf(value)); + } + + protected Attr appendNewAttrWithValue( + final Element baseElement, + final String name, + final String value + ) { + final Attr attr = document.createAttribute(name); + attr.setValue(value); + baseElement.setAttributeNode(attr); + + return attr; + } + + protected Element getFirstElementByName(final String name) { + return (Element) document.getElementsByTagName(name).item(0); + } + + protected Element createElement(final String name) { + return document.createElement(name); + } + + @SuppressWarnings("squid:S2755") // warning is still shown despite applied solution + protected String documentToXml() throws TransformerException { + /* + * Generate a new {@link TransformerFactory} secured from XXE attacks, on platforms which + * support setting {@link XMLConstants#ACCESS_EXTERNAL_DTD} and + * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link TransformerFactory} instances. + */ + final TransformerFactory transformerFactory = TransformerFactory.newInstance(); + try { + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } catch (final Exception ignored) { + // Ignore exceptions as setting these attributes to secure XML generation is not + // supported by all platforms (like the Android implementation) + } + + final Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); + + final StringWriter result = new StringWriter(); + transformer.transform(new DOMSource(document), new StreamResult(result)); + + return result.toString(); + } + + protected String documentToXmlSafe() { + try { + return documentToXml(); + } catch (final TransformerException e) { + throw new DashManifestCreationException("Failed to convert XML-document to string", e); + } + } + + // endregion +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java new file mode 100644 index 0000000000..ad4bc55931 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java @@ -0,0 +1,162 @@ +package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator; + +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TIMELINE; +import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; +import static org.schabi.newpipe.extractor.utils.Utils.removeNonDigitCharacters; + +import org.schabi.newpipe.extractor.downloader.Response; +import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException; +import org.w3c.dom.Element; + +import java.util.Arrays; +import java.util.stream.Stream; + +import javax.annotation.Nonnull; + +public class YoutubeOtfDashManifestCreator extends AbstractYoutubeDashManifestCreator { + + public YoutubeOtfDashManifestCreator(@Nonnull final ItagInfo itagInfo, + final long durationSecondsFallback) { + super(itagInfo, durationSecondsFallback); + } + + @Nonnull + @Override + public String generateManifest() { + // Try to avoid redirects when streaming the content by saving the last URL we get + // from video servers. + final Response response = getInitializationResponse(itagInfo.getStreamUrl()); + final String otfBaseStreamingUrl = response.latestUrl() + .replace(SQ_0, EMPTY_STRING) + .replace(RN_0, EMPTY_STRING) + .replace(ALR_YES, EMPTY_STRING); + + final int responseCode = response.responseCode(); + if (responseCode != 200) { + throw new DashManifestCreationException("Could not get the initialization URL: " + + "response code " + + responseCode); + } + + final String[] segmentDurations; + + try { + final String[] segmentsAndDurationsResponseSplit = response.responseBody() + // Get the lines with the durations and the following + .split("Segment-Durations-Ms: ")[1] + // Remove the other lines + .split("\n")[0] + // Get all durations and repetitions which are separated by a comma + .split(","); + final int lastIndex = segmentsAndDurationsResponseSplit.length - 1; + if (isBlank(segmentsAndDurationsResponseSplit[lastIndex])) { + segmentDurations = Arrays.copyOf(segmentsAndDurationsResponseSplit, lastIndex); + } else { + segmentDurations = segmentsAndDurationsResponseSplit; + } + } catch (final Exception e) { + throw new DashManifestCreationException("Could not get segment durations", e); + } + + long streamDurationMs; + try { + streamDurationMs = getStreamDuration(segmentDurations); + } catch (final DashManifestCreationException e) { + streamDurationMs = durationSecondsFallback * 1000; + } + + generateDocumentAndCommonElements(streamDurationMs); + generateSegmentTemplateElement(otfBaseStreamingUrl); + generateSegmentTimelineElement(); + generateSegmentElementsForOtfStreams(segmentDurations); + + return documentToXmlSafe(); + } + + /** + * Generate segment elements for OTF streams. + * + *

+ * By parsing by the first media sequence, we know how many durations and repetitions there are + * so we just have to loop into segment durations to generate the following elements for each + * duration repeated X times: + *

+ * + *

+ * {@code } + *

+ * + *

+ * If there is no repetition of the duration between two segments, the {@code r} attribute is + * not added to the {@code S} element, as it is not needed. + *

+ * + *

+ * These elements will be appended as children of the {@code } element, which + * needs to be generated before these elements with + * {@link #generateSegmentTimelineElement()}. + *

+ * + * @param segmentDurations the sequences "length" or "length(r=repeat_count" extracted with the + * regular expressions + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateSegmentElementsForOtfStreams(@Nonnull final String[] segmentDurations) { + try { + final Element segmentTimelineElement = getFirstElementByName(SEGMENT_TIMELINE); + streamAndSplitSegmentDurations(segmentDurations) + .map(segmentLengthRepeat -> { + final Element sElement = createElement("S"); + // There are repetitions of a segment duration in other segments + if (segmentLengthRepeat.length > 1) { + appendNewAttrWithValue(sElement, "r", Integer.parseInt( + removeNonDigitCharacters(segmentLengthRepeat[1]))); + } + + appendNewAttrWithValue( + sElement, "d", Integer.parseInt(segmentLengthRepeat[0])); + return sElement; + }) + .forEach(segmentTimelineElement::appendChild); + } catch (final Exception e) { + throw DashManifestCreationException.couldNotAddElement("segment (S)", e); + } + } + + /** + * Get the duration of an OTF stream. + * + *

+ * The duration of OTF streams is not returned into the player response and needs to be + * calculated by adding the duration of each segment. + *

+ * + * @param segmentDurations the segment duration object extracted from the initialization + * sequence of the stream + * @return the duration of the OTF stream, in milliseconds + * @throws DashManifestCreationException May throw a CreationException + */ + protected long getStreamDuration(@Nonnull final String[] segmentDurations) { + try { + return streamAndSplitSegmentDurations(segmentDurations) + .mapToLong(segmentLengthRepeat -> { + final long segmentLength = Integer.parseInt(segmentLengthRepeat[0]); + final long segmentRepeatCount = segmentLengthRepeat.length > 1 + ? Long.parseLong(removeNonDigitCharacters(segmentLengthRepeat[1])) + : 0; + return segmentLength + segmentRepeatCount * segmentLength; + }) + .sum(); + } catch (final NumberFormatException e) { + throw new DashManifestCreationException("Could not get stream length from sequences " + + "list", e); + } + } + + protected Stream streamAndSplitSegmentDurations(@Nonnull final String[] segmentDurations) { + return Stream.of(segmentDurations) + .map(segDuration -> segDuration.split("\\(r=")); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java new file mode 100644 index 0000000000..3c6bd1da8f --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java @@ -0,0 +1,121 @@ +package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator; + +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TIMELINE; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + +import org.schabi.newpipe.extractor.downloader.Response; +import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException; +import org.w3c.dom.Attr; +import org.w3c.dom.DOMException; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; + +public class YoutubePostLiveStreamDvrDashManifestCreator extends AbstractYoutubeDashManifestCreator { + + protected YoutubePostLiveStreamDvrDashManifestCreator(@Nonnull final ItagInfo itagInfo, + final long durationSecondsFallback) { + super(itagInfo, durationSecondsFallback); + } + + @Override + protected boolean isLiveDelivery() { + return true; + } + + @Nonnull + @Override + public String generateManifest() { + final Integer targetDurationSec = itagInfo.getTargetDurationSec(); + if (targetDurationSec == null || targetDurationSec <= 0) { + throw new DashManifestCreationException( + "Invalid value for 'targetDurationSec'=" + targetDurationSec); + } + + // Try to avoid redirects when streaming the content by saving the latest URL we get + // from video servers. + final Response response = getInitializationResponse(itagInfo.getStreamUrl()); + final String realPostLiveStreamDvrStreamingUrl = response.latestUrl() + .replace(SQ_0, "") + .replace(RN_0, "") + .replace(ALR_YES, ""); + + final int responseCode = response.responseCode(); + if (responseCode != 200) { + throw new DashManifestCreationException( + "Could not get the initialization sequence: response code " + responseCode); + } + + final String streamDurationMsString; + final String segmentCount; + try { + final Map> responseHeaders = response.responseHeaders(); + streamDurationMsString = responseHeaders.get("X-Head-Time-Millis").get(0); + segmentCount = responseHeaders.get("X-Head-Seqnum").get(0); + } catch (final IndexOutOfBoundsException e) { + throw new DashManifestCreationException( + "Could not get the value of the X-Head-Time-Millis or the X-Head-Seqnum header", + e); + } + + if (isNullOrEmpty(segmentCount)) { + throw new DashManifestCreationException("Could not get the number of segments"); + } + + long streamDurationMs; + try { + streamDurationMs = Long.parseLong(streamDurationMsString); + } catch (final NumberFormatException e) { + streamDurationMs = durationSecondsFallback * 1000; + } + + generateDocumentAndCommonElements(streamDurationMs); + + generateSegmentTemplateElement(realPostLiveStreamDvrStreamingUrl); + generateSegmentTimelineElement(); + generateSegmentElementForPostLiveDvrStreams(targetDurationSec, segmentCount); + + return documentToXmlSafe(); + } + + /** + * Generate the segment ({@code }) element. + * + *

+ * We don't know the exact duration of segments for post-live-DVR streams but an + * average instead (which is the {@code targetDurationSec} value), so we can use the following + * structure to generate the segment timeline for DASH manifests of ended livestreams: + *
+ * {@code } + *

+ * + * @param targetDurationSeconds the {@code targetDurationSec} value from YouTube player + * response's stream + * @param segmentCount the number of segments + * @throws DashManifestCreationException May throw a CreationException + */ + private void generateSegmentElementForPostLiveDvrStreams( + final int targetDurationSeconds, + @Nonnull final String segmentCount + ) { + try { + final Element sElement = document.createElement("S"); + + appendNewAttrWithValue(sElement, "d", targetDurationSeconds * 1000); + + final Attr rAttribute = document.createAttribute("r"); + rAttribute.setValue(segmentCount); + sElement.setAttributeNode(rAttribute); + + appendNewAttrWithValue(sElement, "r", segmentCount); + + getFirstElementByName(SEGMENT_TIMELINE).appendChild(sElement); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement("segment (S)", e); + } + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java new file mode 100644 index 0000000000..c368712a6e --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java @@ -0,0 +1,159 @@ +package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator; + +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.BASE_URL; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.INITIALIZATION; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.MPD; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.REPRESENTATION; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_BASE; + +import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException; +import org.w3c.dom.DOMException; +import org.w3c.dom.Element; + +import javax.annotation.Nonnull; + +public class YoutubeProgressiveDashManifestCreator extends AbstractYoutubeDashManifestCreator { + + public YoutubeProgressiveDashManifestCreator(@Nonnull final ItagInfo itagInfo, + final long durationSecondsFallback) { + super(itagInfo, durationSecondsFallback); + } + + @Nonnull + @Override + public String generateManifest() { + final long streamDurationMs; + if (itagInfo.getApproxDurationMs() != null) { + streamDurationMs = itagInfo.getApproxDurationMs(); + } else if (durationSecondsFallback > 0) { + streamDurationMs = durationSecondsFallback * 1000; + } else { + throw DashManifestCreationException.couldNotAddElement(MPD, "the duration of the " + + "stream could not be determined and durationSecondsFallback is <= 0"); + } + + generateDocumentAndCommonElements(streamDurationMs); + generateSegmentBaseElement(); + generateInitializationElement(); + + return documentToXmlSafe(); + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateRepresentationElement()}). + *

+ * + * @param baseUrl the base URL of the stream, which must not be null and will be set as the + * content of the {@code } element + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateBaseUrlElement(@Nonnull final String baseUrl) + throws DashManifestCreationException { + try { + final Element representationElement = getFirstElementByName(REPRESENTATION); + + appendNewAttrWithValue(representationElement, BASE_URL, baseUrl); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(BASE_URL, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * It generates the following element: + *
+ * {@code } + *
+ * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagInfo} passed + * as the second parameter) + *

+ * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateRepresentationElement()}), + * and the {@code BaseURL} element with {@link #generateBaseUrlElement(String)} + * should be generated too. + *

+ * + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateSegmentBaseElement() { + try { + final Element segmentBaseElement = createElement(SEGMENT_BASE); + + if (itagInfo.getIndexRange() == null + || itagInfo.getIndexRange().start() < 0 + || itagInfo.getIndexRange().end() < 0) { + throw DashManifestCreationException.couldNotAddElement(SEGMENT_BASE, + "invalid index-range: " + itagInfo.getIndexRange()); + } + + appendNewAttrWithValue( + segmentBaseElement, + "indexRange", + itagInfo.getIndexRange().start() + "-" + itagInfo.getIndexRange().end()); + + getFirstElementByName(REPRESENTATION).appendChild(segmentBaseElement); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(SEGMENT_BASE, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * It generates the following element: + *
+ * {@code } + *
+ * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagInfo} passed + * as the second parameter) + *

+ * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateSegmentBaseElement()}). + *

+ * + * @throws DashManifestCreationException May throw a CreationException + */ + protected void generateInitializationElement() { + try { + final Element initializationElement = createElement(INITIALIZATION); + + if (itagInfo.getInitRange() == null + || itagInfo.getInitRange().start() < 0 + || itagInfo.getInitRange().end() < 0) { + throw DashManifestCreationException.couldNotAddElement(SEGMENT_BASE, + "invalid (init)-range: " + itagInfo.getInitRange()); + } + + appendNewAttrWithValue( + initializationElement, + "range", + itagInfo.getInitRange().start() + "-" + itagInfo.getInitRange().end()); + + getFirstElementByName(SEGMENT_BASE).appendChild(initializationElement); + } catch (final DOMException e) { + throw DashManifestCreationException.couldNotAddElement(INITIALIZATION, e); + } + } + + + @Nonnull + @Override + protected String appendBaseStreamingUrlParams(@Nonnull final String baseStreamingUrl) { + return baseStreamingUrl + RN_0; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/CreationException.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/CreationException.java deleted file mode 100644 index 46f32664b1..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/CreationException.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; - -import javax.annotation.Nonnull; - -/** - * Exception that is thrown when a YouTube DASH manifest creator encounters a problem - * while creating a manifest. - */ -public final class CreationException extends RuntimeException { - - /** - * Create a new {@link CreationException} with a detail message. - * - * @param message the detail message to add in the exception - */ - public CreationException(final String message) { - super(message); - } - - /** - * Create a new {@link CreationException} with a detail message and a cause. - * @param message the detail message to add in the exception - * @param cause the exception cause of this {@link CreationException} - */ - public CreationException(final String message, final Exception cause) { - super(message, cause); - } - - // Methods to create exceptions easily without having to use big exception messages and to - // reduce duplication - - /** - * Create a new {@link CreationException} with a cause and the following detail message format: - *
- * {@code "Could not add " + element + " element", cause}, where {@code element} is an element - * of a DASH manifest. - * - * @param element the element which was not added to the DASH document - * @param cause the exception which prevented addition of the element to the DASH document - * @return a new {@link CreationException} - */ - @Nonnull - public static CreationException couldNotAddElement(final String element, - final Exception cause) { - return new CreationException("Could not add " + element + " element", cause); - } - - /** - * Create a new {@link CreationException} with a cause and the following detail message format: - *
- * {@code "Could not add " + element + " element: " + reason}, where {@code element} is an - * element of a DASH manifest and {@code reason} the reason why this element cannot be added to - * the DASH document. - * - * @param element the element which was not added to the DASH document - * @param reason the reason message of why the element has been not added to the DASH document - * @return a new {@link CreationException} - */ - @Nonnull - public static CreationException couldNotAddElement(final String element, final String reason) { - return new CreationException("Could not add " + element + " element: " + reason); - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java deleted file mode 100644 index 045e5dda49..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java +++ /dev/null @@ -1,756 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; - -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.downloader.Downloader; -import org.schabi.newpipe.extractor.downloader.Response; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.services.youtube.DeliveryType; -import org.schabi.newpipe.extractor.services.youtube.ItagItem; -import org.schabi.newpipe.extractor.utils.ManifestCreatorCache; -import org.w3c.dom.Attr; -import org.w3c.dom.DOMException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import javax.annotation.Nonnull; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; - -import java.io.IOException; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; - -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.addClientInfoHeaders; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl; -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; - -/** - * Utilities and constants for YouTube DASH manifest creators. - * - *

- * This class includes common methods of manifest creators and useful constants. - *

- * - *

- * Generation of DASH documents and their conversion as a string is done using external classes - * from {@link org.w3c.dom} and {@link javax.xml} packages. - *

- */ -public final class YoutubeDashManifestCreatorsUtils { - - private YoutubeDashManifestCreatorsUtils() { - } - - /** - * The redirect count limit that this class uses, which is the same limit as OkHttp. - */ - public static final int MAXIMUM_REDIRECT_COUNT = 20; - - /** - * URL parameter of the first sequence for live, post-live-DVR and OTF streams. - */ - public static final String SQ_0 = "&sq=0"; - - /** - * URL parameter of the first stream request made by official clients. - */ - public static final String RN_0 = "&rn=0"; - - /** - * URL parameter specific to web clients. When this param is added, if a redirection occurs, - * the server will not redirect clients to the redirect URL. Instead, it will provide this URL - * as the response body. - */ - public static final String ALR_YES = "&alr=yes"; - - // XML elements of DASH MPD manifests - // see https://www.brendanlong.com/the-structure-of-an-mpeg-dash-mpd.html - public static final String MPD = "MPD"; - public static final String PERIOD = "Period"; - public static final String ADAPTATION_SET = "AdaptationSet"; - public static final String ROLE = "Role"; - public static final String REPRESENTATION = "Representation"; - public static final String AUDIO_CHANNEL_CONFIGURATION = "AudioChannelConfiguration"; - public static final String SEGMENT_TEMPLATE = "SegmentTemplate"; - public static final String SEGMENT_TIMELINE = "SegmentTimeline"; - public static final String BASE_URL = "BaseURL"; - public static final String SEGMENT_BASE = "SegmentBase"; - public static final String INITIALIZATION = "Initialization"; - - /** - * Create an attribute with {@link Document#createAttribute(String)}, assign to it the provided - * name and value, then add it to the provided element using {@link - * Element#setAttributeNode(Attr)}. - * - * @param element element to which to add the created node - * @param doc document to use to create the attribute - * @param name name of the attribute - * @param value value of the attribute, will be set using {@link Attr#setValue(String)} - */ - public static void setAttribute(final Element element, - final Document doc, - final String name, - final String value) { - final Attr attr = doc.createAttribute(name); - attr.setValue(value); - element.setAttributeNode(attr); - } - - /** - * Generate a {@link Document} with common manifest creator elements added to it. - * - *

- * Those are: - *

    - *
  • {@code MPD} (using {@link #generateDocumentAndMpdElement(long)});
  • - *
  • {@code Period} (using {@link #generatePeriodElement(Document)});
  • - *
  • {@code AdaptationSet} (using {@link #generateAdaptationSetElement(Document, - * ItagItem)});
  • - *
  • {@code Role} (using {@link #generateRoleElement(Document)});
  • - *
  • {@code Representation} (using {@link #generateRepresentationElement(Document, - * ItagItem)});
  • - *
  • and, for audio streams, {@code AudioChannelConfiguration} (using - * {@link #generateAudioChannelConfigurationElement(Document, ItagItem)}).
  • - *
- *

- * - * @param itagItem the {@link ItagItem} associated to the stream, which must not be null - * @param streamDuration the duration of the stream, in milliseconds - * @return a {@link Document} with the common elements added in it - */ - @Nonnull - public static Document generateDocumentAndDoCommonElementsGeneration( - @Nonnull final ItagItem itagItem, - final long streamDuration) throws CreationException { - final Document doc = generateDocumentAndMpdElement(streamDuration); - - generatePeriodElement(doc); - generateAdaptationSetElement(doc, itagItem); - generateRoleElement(doc); - generateRepresentationElement(doc, itagItem); - if (itagItem.itagType == ItagItem.ItagType.AUDIO) { - generateAudioChannelConfigurationElement(doc, itagItem); - } - - return doc; - } - - /** - * Create a {@link Document} instance and generate the {@code } element of the manifest. - * - *

- * The generated {@code } element looks like the manifest returned into the player - * response of videos: - *

- * - *

- * {@code } - * (where {@code $duration$} represents the duration in seconds (a number with 3 digits after - * the decimal point)). - *

- * - * @param duration the duration of the stream, in milliseconds - * @return a {@link Document} instance which contains a {@code } element - */ - @Nonnull - public static Document generateDocumentAndMpdElement(final long duration) - throws CreationException { - try { - final Document doc = newDocument(); - - final Element mpdElement = doc.createElement(MPD); - doc.appendChild(mpdElement); - - setAttribute(mpdElement, doc, "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); - setAttribute(mpdElement, doc, "xmlns", "urn:mpeg:DASH:schema:MPD:2011"); - setAttribute(mpdElement, doc, "xsi:schemaLocation", - "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"); - setAttribute(mpdElement, doc, "minBufferTime", "PT1.500S"); - setAttribute(mpdElement, doc, "profiles", "urn:mpeg:dash:profile:full:2011"); - setAttribute(mpdElement, doc, "type", "static"); - setAttribute(mpdElement, doc, "mediaPresentationDuration", - String.format(Locale.ENGLISH, "PT%.3fS", duration / 1000.0)); - - return doc; - } catch (final Exception e) { - throw new CreationException( - "Could not generate the DASH manifest or append the MPD doc to it", e); - } - } - - /** - * Generate the {@code } element, appended as a child of the {@code } element. - * - *

- * The {@code } element needs to be generated before this element with - * {@link #generateDocumentAndMpdElement(long)}. - *

- * - * @param doc the {@link Document} on which the the {@code } element will be appended - */ - public static void generatePeriodElement(@Nonnull final Document doc) - throws CreationException { - try { - final Element mpdElement = (Element) doc.getElementsByTagName(MPD).item(0); - final Element periodElement = doc.createElement(PERIOD); - mpdElement.appendChild(periodElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(PERIOD, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the {@code } - * element. - * - *

- * The {@code } element needs to be generated before this element with - * {@link #generatePeriodElement(Document)}. - *

- * - * @param doc the {@link Document} on which the {@code } element will be appended - * @param itagItem the {@link ItagItem} corresponding to the stream, which must not be null - */ - public static void generateAdaptationSetElement(@Nonnull final Document doc, - @Nonnull final ItagItem itagItem) - throws CreationException { - try { - final Element periodElement = (Element) doc.getElementsByTagName(PERIOD) - .item(0); - final Element adaptationSetElement = doc.createElement(ADAPTATION_SET); - - setAttribute(adaptationSetElement, doc, "id", "0"); - - final MediaFormat mediaFormat = itagItem.getMediaFormat(); - if (mediaFormat == null || isNullOrEmpty(mediaFormat.getMimeType())) { - throw CreationException.couldNotAddElement(ADAPTATION_SET, - "the MediaFormat or its mime type is null or empty"); - } - - setAttribute(adaptationSetElement, doc, "mimeType", mediaFormat.getMimeType()); - setAttribute(adaptationSetElement, doc, "subsegmentAlignment", "true"); - - periodElement.appendChild(adaptationSetElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(ADAPTATION_SET, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the {@code } - * element. - * - *

- * This element, with its attributes and values, is: - *

- * - *

- * {@code } - *

- * - *

- * The {@code } element needs to be generated before this element with - * {@link #generateAdaptationSetElement(Document, ItagItem)}). - *

- * - * @param doc the {@link Document} on which the the {@code } element will be appended - */ - public static void generateRoleElement(@Nonnull final Document doc) - throws CreationException { - try { - final Element adaptationSetElement = (Element) doc.getElementsByTagName( - ADAPTATION_SET).item(0); - final Element roleElement = doc.createElement(ROLE); - - setAttribute(roleElement, doc, "schemeIdUri", "urn:mpeg:DASH:role:2011"); - setAttribute(roleElement, doc, "value", "main"); - - adaptationSetElement.appendChild(roleElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(ROLE, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

- * The {@code } element needs to be generated before this element with - * {@link #generateAdaptationSetElement(Document, ItagItem)}). - *

- * - * @param doc the {@link Document} on which the the {@code } element will be - * appended - * @param itagItem the {@link ItagItem} to use, which must not be null - */ - public static void generateRepresentationElement(@Nonnull final Document doc, - @Nonnull final ItagItem itagItem) - throws CreationException { - try { - final Element adaptationSetElement = (Element) doc.getElementsByTagName( - ADAPTATION_SET).item(0); - final Element representationElement = doc.createElement(REPRESENTATION); - - final int id = itagItem.id; - if (id <= 0) { - throw CreationException.couldNotAddElement(REPRESENTATION, - "the id of the ItagItem is <= 0"); - } - setAttribute(representationElement, doc, "id", String.valueOf(id)); - - final String codec = itagItem.getCodec(); - if (isNullOrEmpty(codec)) { - throw CreationException.couldNotAddElement(ADAPTATION_SET, - "the codec value of the ItagItem is null or empty"); - } - setAttribute(representationElement, doc, "codecs", codec); - setAttribute(representationElement, doc, "startWithSAP", "1"); - setAttribute(representationElement, doc, "maxPlayoutRate", "1"); - - final int bitrate = itagItem.getBitrate(); - if (bitrate <= 0) { - throw CreationException.couldNotAddElement(REPRESENTATION, - "the bitrate of the ItagItem is <= 0"); - } - setAttribute(representationElement, doc, "bandwidth", String.valueOf(bitrate)); - - if (itagItem.itagType == ItagItem.ItagType.VIDEO - || itagItem.itagType == ItagItem.ItagType.VIDEO_ONLY) { - final int height = itagItem.getHeight(); - final int width = itagItem.getWidth(); - if (height <= 0 && width <= 0) { - throw CreationException.couldNotAddElement(REPRESENTATION, - "both width and height of the ItagItem are <= 0"); - } - - if (width > 0) { - setAttribute(representationElement, doc, "width", String.valueOf(width)); - } - setAttribute(representationElement, doc, "height", - String.valueOf(itagItem.getHeight())); - - final int fps = itagItem.getFps(); - if (fps > 0) { - setAttribute(representationElement, doc, "frameRate", String.valueOf(fps)); - } - } - - if (itagItem.itagType == ItagItem.ItagType.AUDIO && itagItem.getSampleRate() > 0) { - final Attr audioSamplingRateAttribute = doc.createAttribute( - "audioSamplingRate"); - audioSamplingRateAttribute.setValue(String.valueOf(itagItem.getSampleRate())); - } - - adaptationSetElement.appendChild(representationElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(REPRESENTATION, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

- * This method is only used when generating DASH manifests of audio streams. - *

- * - *

- * It will produce the following element: - *
- * {@code - * (where {@code audioChannelsValue} is get from the {@link ItagItem} passed as the second - * parameter of this method) - *

- * - *

- * The {@code } element needs to be generated before this element with - * {@link #generateRepresentationElement(Document, ItagItem)}). - *

- * - * @param doc the {@link Document} on which the {@code } element will - * be appended - * @param itagItem the {@link ItagItem} to use, which must not be null - */ - public static void generateAudioChannelConfigurationElement( - @Nonnull final Document doc, - @Nonnull final ItagItem itagItem) throws CreationException { - try { - final Element representationElement = (Element) doc.getElementsByTagName( - REPRESENTATION).item(0); - final Element audioChannelConfigurationElement = doc.createElement( - AUDIO_CHANNEL_CONFIGURATION); - - setAttribute(audioChannelConfigurationElement, doc, "schemeIdUri", - "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"); - - if (itagItem.getAudioChannels() <= 0) { - throw new CreationException("the number of audioChannels in the ItagItem is <= 0: " - + itagItem.getAudioChannels()); - } - setAttribute(audioChannelConfigurationElement, doc, "value", - String.valueOf(itagItem.getAudioChannels())); - - representationElement.appendChild(audioChannelConfigurationElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(AUDIO_CHANNEL_CONFIGURATION, e); - } - } - - /** - * Convert a DASH manifest {@link Document doc} to a string and cache it. - * - * @param originalBaseStreamingUrl the original base URL of the stream - * @param doc the doc to be converted - * @param manifestCreatorCache the {@link ManifestCreatorCache} on which store the string - * generated - * @return the DASH manifest {@link Document doc} converted to a string - */ - public static String buildAndCacheResult( - @Nonnull final String originalBaseStreamingUrl, - @Nonnull final Document doc, - @Nonnull final ManifestCreatorCache manifestCreatorCache) - throws CreationException { - - try { - final String documentXml = documentToXml(doc); - manifestCreatorCache.put(originalBaseStreamingUrl, documentXml); - return documentXml; - } catch (final Exception e) { - throw new CreationException( - "Could not convert the DASH manifest generated to a string", e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

- * This method is only used when generating DASH manifests from OTF and post-live-DVR streams. - *

- * - *

- * It will produce a {@code } element with the following attributes: - *

    - *
  • {@code startNumber}, which takes the value {@code 0} for post-live-DVR streams and - * {@code 1} for OTF streams;
  • - *
  • {@code timescale}, which is always {@code 1000};
  • - *
  • {@code media}, which is the base URL of the stream on which is appended - * {@code &sq=$Number$};
  • - *
  • {@code initialization} (only for OTF streams), which is the base URL of the stream - * on which is appended {@link #SQ_0}.
  • - *
- *

- * - *

- * The {@code } element needs to be generated before this element with - * {@link #generateRepresentationElement(Document, ItagItem)}). - *

- * - * @param doc the {@link Document} on which the {@code } element will - * be appended - * @param baseUrl the base URL of the OTF/post-live-DVR stream - * @param deliveryType the stream {@link DeliveryType delivery type}, which must be either - * {@link DeliveryType#OTF OTF} or {@link DeliveryType#LIVE LIVE} - */ - public static void generateSegmentTemplateElement(@Nonnull final Document doc, - @Nonnull final String baseUrl, - final DeliveryType deliveryType) - throws CreationException { - if (deliveryType != DeliveryType.OTF && deliveryType != DeliveryType.LIVE) { - throw CreationException.couldNotAddElement(SEGMENT_TEMPLATE, "invalid delivery type: " - + deliveryType); - } - - try { - final Element representationElement = (Element) doc.getElementsByTagName( - REPRESENTATION).item(0); - final Element segmentTemplateElement = doc.createElement(SEGMENT_TEMPLATE); - - // The first sequence of post DVR streams is the beginning of the video stream and not - // an initialization segment - setAttribute(segmentTemplateElement, doc, "startNumber", - deliveryType == DeliveryType.LIVE ? "0" : "1"); - setAttribute(segmentTemplateElement, doc, "timescale", "1000"); - - // Post-live-DVR/ended livestreams streams don't require an initialization sequence - if (deliveryType != DeliveryType.LIVE) { - setAttribute(segmentTemplateElement, doc, "initialization", baseUrl + SQ_0); - } - - setAttribute(segmentTemplateElement, doc, "media", baseUrl + "&sq=$Number$"); - - representationElement.appendChild(segmentTemplateElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(SEGMENT_TEMPLATE, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

- * The {@code } element needs to be generated before this element with - * {@link #generateSegmentTemplateElement(Document, String, DeliveryType)}. - *

- * - * @param doc the {@link Document} on which the the {@code } element will be - * appended - */ - public static void generateSegmentTimelineElement(@Nonnull final Document doc) - throws CreationException { - try { - final Element segmentTemplateElement = (Element) doc.getElementsByTagName( - SEGMENT_TEMPLATE).item(0); - final Element segmentTimelineElement = doc.createElement(SEGMENT_TIMELINE); - - segmentTemplateElement.appendChild(segmentTimelineElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(SEGMENT_TIMELINE, e); - } - } - - /** - * Get the "initialization" {@link Response response} of a stream. - * - *

This method fetches, for OTF streams and for post-live-DVR streams: - *

    - *
  • the base URL of the stream, to which are appended {@link #SQ_0} and - * {@link #RN_0} parameters, with a {@code GET} request for streaming URLs from HTML5 - * clients and a {@code POST} request for the ones from the {@code ANDROID} and the - * {@code IOS} clients;
  • - *
  • for streaming URLs from HTML5 clients, the {@link #ALR_YES} param is also added. - *
  • - *
- *

- * - * @param baseStreamingUrl the base URL of the stream, which must not be null - * @param itagItem the {@link ItagItem} of stream, which must not be null - * @param deliveryType the {@link DeliveryType} of the stream - * @return the "initialization" response, without redirections on the network on which the - * request(s) is/are made - */ - @SuppressWarnings("checkstyle:FinalParameters") - @Nonnull - public static Response getInitializationResponse(@Nonnull String baseStreamingUrl, - @Nonnull final ItagItem itagItem, - final DeliveryType deliveryType) - throws CreationException { - final boolean isHtml5StreamingUrl = isWebStreamingUrl(baseStreamingUrl) - || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(baseStreamingUrl); - final boolean isAndroidStreamingUrl = isAndroidStreamingUrl(baseStreamingUrl); - final boolean isIosStreamingUrl = isIosStreamingUrl(baseStreamingUrl); - if (isHtml5StreamingUrl) { - baseStreamingUrl += ALR_YES; - } - baseStreamingUrl = appendRnSqParamsIfNeeded(baseStreamingUrl, deliveryType); - - final Downloader downloader = NewPipe.getDownloader(); - if (isHtml5StreamingUrl) { - final String mimeTypeExpected = itagItem.getMediaFormat().getMimeType(); - if (!isNullOrEmpty(mimeTypeExpected)) { - return getStreamingWebUrlWithoutRedirects(downloader, baseStreamingUrl, - mimeTypeExpected); - } - } else if (isAndroidStreamingUrl || isIosStreamingUrl) { - try { - final Map> headers = Collections.singletonMap("User-Agent", - Collections.singletonList(isAndroidStreamingUrl - ? getAndroidUserAgent(null) : getIosUserAgent(null))); - final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8); - return downloader.post(baseStreamingUrl, headers, emptyBody); - } catch (final IOException | ExtractionException e) { - throw new CreationException("Could not get the " - + (isIosStreamingUrl ? "ANDROID" : "IOS") + " streaming URL response", e); - } - } - - try { - return downloader.get(baseStreamingUrl); - } catch (final IOException | ExtractionException e) { - throw new CreationException("Could not get the streaming URL response", e); - } - } - - /** - * Generate a new {@link DocumentBuilder} secured from XXE attacks, on platforms which - * support setting {@link XMLConstants#ACCESS_EXTERNAL_DTD} and - * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link DocumentBuilderFactory} instances. - * - * @return an instance of {@link Document} secured against XXE attacks on supported platforms, - * that should then be convertible to an XML string without security problems - */ - private static Document newDocument() throws ParserConfigurationException { - final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - try { - documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - } catch (final Exception ignored) { - // Ignore exceptions as setting these attributes to secure XML generation is not - // supported by all platforms (like the Android implementation) - } - - return documentBuilderFactory.newDocumentBuilder().newDocument(); - } - - /** - * Generate a new {@link TransformerFactory} secured from XXE attacks, on platforms which - * support setting {@link XMLConstants#ACCESS_EXTERNAL_DTD} and - * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link TransformerFactory} instances. - * - * @param doc the doc to convert, which must have been created using {@link #newDocument()} to - * properly prevent XXE attacks - * @return the doc converted to an XML string, making sure there can't be XXE attacks - */ - // Sonar warning is suppressed because it is still shown even if we apply its solution - @SuppressWarnings("squid:S2755") - private static String documentToXml(@Nonnull final Document doc) - throws TransformerException { - - final TransformerFactory transformerFactory = TransformerFactory.newInstance(); - try { - transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - } catch (final Exception ignored) { - // Ignore exceptions as setting these attributes to secure XML generation is not - // supported by all platforms (like the Android implementation) - } - - final Transformer transformer = transformerFactory.newTransformer(); - transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); - transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); - - final StringWriter result = new StringWriter(); - transformer.transform(new DOMSource(doc), new StreamResult(result)); - - return result.toString(); - } - - /** - * Append {@link #SQ_0} for post-live-DVR and OTF streams and {@link #RN_0} to all streams. - * - * @param baseStreamingUrl the base streaming URL to which the parameter(s) are being appended - * @param deliveryType the {@link DeliveryType} of the stream - * @return the base streaming URL to which the param(s) are appended, depending on the - * {@link DeliveryType} of the stream - */ - @Nonnull - private static String appendRnSqParamsIfNeeded(@Nonnull final String baseStreamingUrl, - @Nonnull final DeliveryType deliveryType) { - return baseStreamingUrl + (deliveryType == DeliveryType.PROGRESSIVE ? "" : SQ_0) + RN_0; - } - - /** - * Get a URL on which no redirection between playback hosts should be present on the network - * and/or IP used to fetch the streaming URL, for HTML5 clients. - * - *

This method will follow redirects which works in the following way: - *

    - *
  1. the {@link #ALR_YES} param is appended to all streaming URLs
  2. - *
  3. if no redirection occurs, the video server will return the streaming data;
  4. - *
  5. if a redirection occurs, the server will respond with HTTP status code 200 and a - * {@code text/plain} mime type. The redirection URL is the response body;
  6. - *
  7. the redirection URL is requested and the steps above from step 2 are repeated, - * until too many redirects are reached of course (the maximum number of redirects is - * {@link #MAXIMUM_REDIRECT_COUNT the same as OkHttp}).
  8. - *
- *

- * - *

- * For non-HTML5 clients, redirections are managed in the standard way in - * {@link #getInitializationResponse(String, ItagItem, DeliveryType)}. - *

- * - * @param downloader the {@link Downloader} instance to be used - * @param streamingUrl the streaming URL which we are trying to get a streaming URL - * without any redirection on the network and/or IP used - * @param responseMimeTypeExpected the response mime type expected from Google video servers - * @return the {@link Response} of the stream, which should have no redirections - */ - @SuppressWarnings("checkstyle:FinalParameters") - @Nonnull - private static Response getStreamingWebUrlWithoutRedirects( - @Nonnull final Downloader downloader, - @Nonnull String streamingUrl, - @Nonnull final String responseMimeTypeExpected) - throws CreationException { - try { - final Map> headers = new HashMap<>(); - addClientInfoHeaders(headers); - - String responseMimeType = ""; - - int redirectsCount = 0; - while (!responseMimeType.equals(responseMimeTypeExpected) - && redirectsCount < MAXIMUM_REDIRECT_COUNT) { - final Response response = downloader.get(streamingUrl, headers); - - final int responseCode = response.responseCode(); - if (responseCode != 200) { - throw new CreationException( - "Could not get the initialization URL: HTTP response code " - + responseCode); - } - - // A valid HTTP 1.0+ response should include a Content-Type header, so we can - // require that the response from video servers has this header. - responseMimeType = Objects.requireNonNull(response.getHeader("Content-Type"), - "Could not get the Content-Type header from the response headers"); - - // The response body is the redirection URL - if (responseMimeType.equals("text/plain")) { - streamingUrl = response.responseBody(); - redirectsCount++; - } else { - return response; - } - } - - if (redirectsCount >= MAXIMUM_REDIRECT_COUNT) { - throw new CreationException( - "Too many redirects when trying to get the the streaming URL response of a " - + "HTML5 client"); - } - - // This should never be reached, but is required because we don't want to return null - // here - throw new CreationException( - "Could not get the streaming URL response of a HTML5 client: unreachable code " - + "reached!"); - } catch (final IOException | ExtractionException e) { - throw new CreationException( - "Could not get the streaming URL response of a HTML5 client", e); - } - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java deleted file mode 100644 index 46e84df1db..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java +++ /dev/null @@ -1,265 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; - -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.ALR_YES; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.RN_0; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_TIMELINE; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SQ_0; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.buildAndCacheResult; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateDocumentAndDoCommonElementsGeneration; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateSegmentTemplateElement; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateSegmentTimelineElement; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.getInitializationResponse; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.setAttribute; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; - -import org.schabi.newpipe.extractor.downloader.Response; -import org.schabi.newpipe.extractor.services.youtube.DeliveryType; -import org.schabi.newpipe.extractor.services.youtube.ItagItem; -import org.schabi.newpipe.extractor.utils.ManifestCreatorCache; -import org.schabi.newpipe.extractor.utils.Utils; -import org.w3c.dom.DOMException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import java.util.Arrays; -import java.util.Objects; - -import javax.annotation.Nonnull; - -/** - * Class which generates DASH manifests of YouTube {@link DeliveryType#OTF OTF streams}. - */ -public final class YoutubeOtfDashManifestCreator { - - /** - * Cache of DASH manifests generated for OTF streams. - */ - private static final ManifestCreatorCache OTF_STREAMS_CACHE - = new ManifestCreatorCache<>(); - - private YoutubeOtfDashManifestCreator() { - } - - /** - * Create DASH manifests from a YouTube OTF stream. - * - *

- * OTF streams are YouTube-DASH specific streams which work with sequences and without the need - * to get a manifest (even if one is provided, it is not used by official clients). - *

- * - *

- * They can be found only on videos; mostly those with a small amount of views, or ended - * livestreams which have just been re-encoded as normal videos. - *

- * - *

This method needs: - *

    - *
  • the base URL of the stream (which, if you try to access to it, returns HTTP - * status code 404 after redirects, and if the URL is valid);
  • - *
  • an {@link ItagItem}, which needs to contain the following information: - *
      - *
    • its type (see {@link ItagItem.ItagType}), to identify if the content is - * an audio or a video stream;
    • - *
    • its bitrate;
    • - *
    • its mime type;
    • - *
    • its codec(s);
    • - *
    • for an audio stream: its audio channels;
    • - *
    • for a video stream: its width and height.
    • - *
    - *
  • - *
  • the duration of the video, which will be used if the duration could not be - * parsed from the first sequence of the stream.
  • - *
- *

- * - *

In order to generate the DASH manifest, this method will: - *

    - *
  • request the first sequence of the stream (the base URL on which the first - * sequence parameter is appended (see {@link YoutubeDashManifestCreatorsUtils#SQ_0})) - * with a {@code POST} or {@code GET} request (depending of the client on which the - * streaming URL comes from is a mobile one ({@code POST}) or not ({@code GET}));
  • - *
  • follow its redirection(s), if any;
  • - *
  • save the last URL, remove the first sequence parameter;
  • - *
  • use the information provided in the {@link ItagItem} to generate all - * elements of the DASH manifest.
  • - *
- *

- * - *

- * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used - * as the stream duration. - *

- * - * @param otfBaseStreamingUrl the base URL of the OTF stream, which must not be null - * @param itagItem the {@link ItagItem} corresponding to the stream, which - * must not be null - * @param durationSecondsFallback the duration of the video, which will be used if the duration - * could not be extracted from the first sequence - * @return the manifest generated into a string - */ - @Nonnull - public static String fromOtfStreamingUrl( - @Nonnull final String otfBaseStreamingUrl, - @Nonnull final ItagItem itagItem, - final long durationSecondsFallback) throws CreationException { - if (OTF_STREAMS_CACHE.containsKey(otfBaseStreamingUrl)) { - return Objects.requireNonNull(OTF_STREAMS_CACHE.get(otfBaseStreamingUrl)).getSecond(); - } - - String realOtfBaseStreamingUrl = otfBaseStreamingUrl; - // Try to avoid redirects when streaming the content by saving the last URL we get - // from video servers. - final Response response = getInitializationResponse(realOtfBaseStreamingUrl, - itagItem, DeliveryType.OTF); - realOtfBaseStreamingUrl = response.latestUrl().replace(SQ_0, "") - .replace(RN_0, "").replace(ALR_YES, ""); - - final int responseCode = response.responseCode(); - if (responseCode != 200) { - throw new CreationException("Could not get the initialization URL: response code " - + responseCode); - } - - final String[] segmentDuration; - - try { - final String[] segmentsAndDurationsResponseSplit = response.responseBody() - // Get the lines with the durations and the following - .split("Segment-Durations-Ms: ")[1] - // Remove the other lines - .split("\n")[0] - // Get all durations and repetitions which are separated by a comma - .split(","); - final int lastIndex = segmentsAndDurationsResponseSplit.length - 1; - if (isBlank(segmentsAndDurationsResponseSplit[lastIndex])) { - segmentDuration = Arrays.copyOf(segmentsAndDurationsResponseSplit, lastIndex); - } else { - segmentDuration = segmentsAndDurationsResponseSplit; - } - } catch (final Exception e) { - throw new CreationException("Could not get segment durations", e); - } - - long streamDuration; - try { - streamDuration = getStreamDuration(segmentDuration); - } catch (final CreationException e) { - streamDuration = durationSecondsFallback * 1000; - } - - final Document doc = generateDocumentAndDoCommonElementsGeneration(itagItem, - streamDuration); - - generateSegmentTemplateElement(doc, realOtfBaseStreamingUrl, DeliveryType.OTF); - generateSegmentTimelineElement(doc); - generateSegmentElementsForOtfStreams(segmentDuration, doc); - - return buildAndCacheResult(otfBaseStreamingUrl, doc, OTF_STREAMS_CACHE); - } - - /** - * @return the cache of DASH manifests generated for OTF streams - */ - @Nonnull - public static ManifestCreatorCache getCache() { - return OTF_STREAMS_CACHE; - } - - /** - * Generate segment elements for OTF streams. - * - *

- * By parsing by the first media sequence, we know how many durations and repetitions there are - * so we just have to loop into segment durations to generate the following elements for each - * duration repeated X times: - *

- * - *

- * {@code } - *

- * - *

- * If there is no repetition of the duration between two segments, the {@code r} attribute is - * not added to the {@code S} element, as it is not needed. - *

- * - *

- * These elements will be appended as children of the {@code } element, which - * needs to be generated before these elements with - * {@link YoutubeDashManifestCreatorsUtils#generateSegmentTimelineElement(Document)}. - *

- * - * @param segmentDurations the sequences "length" or "length(r=repeat_count" extracted with the - * regular expressions - * @param doc the {@link Document} on which the {@code } elements will be - * appended - */ - private static void generateSegmentElementsForOtfStreams( - @Nonnull final String[] segmentDurations, - @Nonnull final Document doc) throws CreationException { - try { - final Element segmentTimelineElement = (Element) doc.getElementsByTagName( - SEGMENT_TIMELINE).item(0); - - for (final String segmentDuration : segmentDurations) { - final Element sElement = doc.createElement("S"); - - final String[] segmentLengthRepeat = segmentDuration.split("\\(r="); - // make sure segmentLengthRepeat[0], which is the length, is convertible to int - Integer.parseInt(segmentLengthRepeat[0]); - - // There are repetitions of a segment duration in other segments - if (segmentLengthRepeat.length > 1) { - final int segmentRepeatCount = Integer.parseInt( - Utils.removeNonDigitCharacters(segmentLengthRepeat[1])); - setAttribute(sElement, doc, "r", String.valueOf(segmentRepeatCount)); - } - setAttribute(sElement, doc, "d", segmentLengthRepeat[0]); - - segmentTimelineElement.appendChild(sElement); - } - - } catch (final DOMException | IllegalStateException | IndexOutOfBoundsException - | NumberFormatException e) { - throw CreationException.couldNotAddElement("segment (S)", e); - } - } - - /** - * Get the duration of an OTF stream. - * - *

- * The duration of OTF streams is not returned into the player response and needs to be - * calculated by adding the duration of each segment. - *

- * - * @param segmentDuration the segment duration object extracted from the initialization - * sequence of the stream - * @return the duration of the OTF stream, in milliseconds - */ - private static long getStreamDuration(@Nonnull final String[] segmentDuration) - throws CreationException { - try { - long streamLengthMs = 0; - - for (final String segDuration : segmentDuration) { - final String[] segmentLengthRepeat = segDuration.split("\\(r="); - long segmentRepeatCount = 0; - - // There are repetitions of a segment duration in other segments - if (segmentLengthRepeat.length > 1) { - segmentRepeatCount = Long.parseLong(Utils.removeNonDigitCharacters( - segmentLengthRepeat[1])); - } - - final long segmentLength = Integer.parseInt(segmentLengthRepeat[0]); - streamLengthMs += segmentLength + segmentRepeatCount * segmentLength; - } - - return streamLengthMs; - } catch (final NumberFormatException e) { - throw new CreationException("Could not get stream length from sequences list", e); - } - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubePostLiveStreamDvrDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubePostLiveStreamDvrDashManifestCreator.java deleted file mode 100644 index 3a5a7dd23d..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubePostLiveStreamDvrDashManifestCreator.java +++ /dev/null @@ -1,217 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; - -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.ALR_YES; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.RN_0; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_TIMELINE; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SQ_0; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.buildAndCacheResult; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateDocumentAndDoCommonElementsGeneration; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateSegmentTemplateElement; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateSegmentTimelineElement; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.getInitializationResponse; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.setAttribute; -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; - -import org.schabi.newpipe.extractor.downloader.Response; -import org.schabi.newpipe.extractor.services.youtube.DeliveryType; -import org.schabi.newpipe.extractor.services.youtube.ItagItem; -import org.schabi.newpipe.extractor.utils.ManifestCreatorCache; -import org.w3c.dom.DOMException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import javax.annotation.Nonnull; - -/** - * Class which generates DASH manifests of YouTube post-live DVR streams (which use the - * {@link DeliveryType#LIVE LIVE delivery type}). - */ -public final class YoutubePostLiveStreamDvrDashManifestCreator { - - /** - * Cache of DASH manifests generated for post-live-DVR streams. - */ - private static final ManifestCreatorCache POST_LIVE_DVR_STREAMS_CACHE - = new ManifestCreatorCache<>(); - - private YoutubePostLiveStreamDvrDashManifestCreator() { - } - - /** - * Create DASH manifests from a YouTube post-live-DVR stream/ended livestream. - * - *

- * Post-live-DVR streams/ended livestreams are one of the YouTube DASH specific streams which - * works with sequences and without the need to get a manifest (even if one is provided but not - * used by main clients (and is not complete for big ended livestreams because it doesn't - * return the full stream)). - *

- * - *

- * They can be found only on livestreams which have ended very recently (a few hours, most of - * the time) - *

- * - *

This method needs: - *

    - *
  • the base URL of the stream (which, if you try to access to it, returns HTTP - * status code 404 after redirects, and if the URL is valid);
  • - *
  • an {@link ItagItem}, which needs to contain the following information: - *
      - *
    • its type (see {@link ItagItem.ItagType}), to identify if the content is - * an audio or a video stream;
    • - *
    • its bitrate;
    • - *
    • its mime type;
    • - *
    • its codec(s);
    • - *
    • for an audio stream: its audio channels;
    • - *
    • for a video stream: its width and height.
    • - *
    - *
  • - *
  • the duration of the video, which will be used if the duration could not be - * parsed from the first sequence of the stream.
  • - *
- *

- * - *

In order to generate the DASH manifest, this method will: - *

    - *
  • request the first sequence of the stream (the base URL on which the first - * sequence parameter is appended (see {@link YoutubeDashManifestCreatorsUtils#SQ_0})) - * with a {@code POST} or {@code GET} request (depending of the client on which the - * streaming URL comes from is a mobile one ({@code POST}) or not ({@code GET}));
  • - *
  • follow its redirection(s), if any;
  • - *
  • save the last URL, remove the first sequence parameters;
  • - *
  • use the information provided in the {@link ItagItem} to generate all elements - * of the DASH manifest.
  • - *
- *

- * - *

- * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used - * as the stream duration. - *

- * - * @param postLiveStreamDvrStreamingUrl the base URL of the post-live-DVR stream/ended - * livestream, which must not be null - * @param itagItem the {@link ItagItem} corresponding to the stream, which - * must not be null - * @param targetDurationSec the target duration of each sequence, in seconds (this - * value is returned with the {@code targetDurationSec} - * field for each stream in YouTube's player response) - * @param durationSecondsFallback the duration of the ended livestream, which will be - * used if the duration could not be extracted from the - * first sequence - * @return the manifest generated into a string - */ - @Nonnull - public static String fromPostLiveStreamDvrStreamingUrl( - @Nonnull final String postLiveStreamDvrStreamingUrl, - @Nonnull final ItagItem itagItem, - final int targetDurationSec, - final long durationSecondsFallback) throws CreationException { - if (POST_LIVE_DVR_STREAMS_CACHE.containsKey(postLiveStreamDvrStreamingUrl)) { - return Objects.requireNonNull( - POST_LIVE_DVR_STREAMS_CACHE.get(postLiveStreamDvrStreamingUrl)).getSecond(); - } - - String realPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl; - final String streamDurationString; - final String segmentCount; - - if (targetDurationSec <= 0) { - throw new CreationException("targetDurationSec value is <= 0: " + targetDurationSec); - } - - try { - // Try to avoid redirects when streaming the content by saving the latest URL we get - // from video servers. - final Response response = getInitializationResponse(realPostLiveStreamDvrStreamingUrl, - itagItem, DeliveryType.LIVE); - realPostLiveStreamDvrStreamingUrl = response.latestUrl().replace(SQ_0, "") - .replace(RN_0, "").replace(ALR_YES, ""); - - final int responseCode = response.responseCode(); - if (responseCode != 200) { - throw new CreationException( - "Could not get the initialization sequence: response code " + responseCode); - } - - final Map> responseHeaders = response.responseHeaders(); - streamDurationString = responseHeaders.get("X-Head-Time-Millis").get(0); - segmentCount = responseHeaders.get("X-Head-Seqnum").get(0); - } catch (final IndexOutOfBoundsException e) { - throw new CreationException( - "Could not get the value of the X-Head-Time-Millis or the X-Head-Seqnum header", - e); - } - - if (isNullOrEmpty(segmentCount)) { - throw new CreationException("Could not get the number of segments"); - } - - long streamDuration; - try { - streamDuration = Long.parseLong(streamDurationString); - } catch (final NumberFormatException e) { - streamDuration = durationSecondsFallback; - } - - final Document doc = generateDocumentAndDoCommonElementsGeneration(itagItem, - streamDuration); - - generateSegmentTemplateElement(doc, realPostLiveStreamDvrStreamingUrl, - DeliveryType.LIVE); - generateSegmentTimelineElement(doc); - generateSegmentElementForPostLiveDvrStreams(doc, targetDurationSec, segmentCount); - - return buildAndCacheResult(postLiveStreamDvrStreamingUrl, doc, - POST_LIVE_DVR_STREAMS_CACHE); - } - - /** - * @return the cache of DASH manifests generated for post-live-DVR streams - */ - @Nonnull - public static ManifestCreatorCache getCache() { - return POST_LIVE_DVR_STREAMS_CACHE; - } - - /** - * Generate the segment ({@code }) element. - * - *

- * We don't know the exact duration of segments for post-live-DVR streams but an - * average instead (which is the {@code targetDurationSec} value), so we can use the following - * structure to generate the segment timeline for DASH manifests of ended livestreams: - *
- * {@code } - *

- * - * @param doc the {@link Document} on which the {@code } element will - * be appended - * @param targetDurationSeconds the {@code targetDurationSec} value from YouTube player - * response's stream - * @param segmentCount the number of segments, extracted by {@link - * #fromPostLiveStreamDvrStreamingUrl(String, ItagItem, int, long)} - */ - private static void generateSegmentElementForPostLiveDvrStreams( - @Nonnull final Document doc, - final int targetDurationSeconds, - @Nonnull final String segmentCount) throws CreationException { - try { - final Element segmentTimelineElement = (Element) doc.getElementsByTagName( - SEGMENT_TIMELINE).item(0); - final Element sElement = doc.createElement("S"); - - setAttribute(sElement, doc, "d", String.valueOf(targetDurationSeconds * 1000)); - setAttribute(sElement, doc, "r", segmentCount); - - segmentTimelineElement.appendChild(sElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement("segment (S)", e); - } - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java deleted file mode 100644 index 0f69895bba..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java +++ /dev/null @@ -1,235 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; - -import org.schabi.newpipe.extractor.services.youtube.DeliveryType; -import org.schabi.newpipe.extractor.services.youtube.ItagItem; -import org.schabi.newpipe.extractor.utils.ManifestCreatorCache; -import org.w3c.dom.DOMException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import javax.annotation.Nonnull; -import java.util.Objects; - -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.BASE_URL; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.INITIALIZATION; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.MPD; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.REPRESENTATION; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_BASE; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.buildAndCacheResult; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateDocumentAndDoCommonElementsGeneration; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.setAttribute; - -/** - * Class which generates DASH manifests of {@link DeliveryType#PROGRESSIVE YouTube progressive} - * streams. - */ -public final class YoutubeProgressiveDashManifestCreator { - - /** - * Cache of DASH manifests generated for progressive streams. - */ - private static final ManifestCreatorCache PROGRESSIVE_STREAMS_CACHE - = new ManifestCreatorCache<>(); - - private YoutubeProgressiveDashManifestCreator() { - } - - /** - * Create DASH manifests from a YouTube progressive stream. - * - *

- * Progressive streams are YouTube DASH streams which work with range requests and without the - * need to get a manifest. - *

- * - *

- * They can be found on all videos, and for all streams for most of videos which come from a - * YouTube partner, and on videos with a large number of views. - *

- * - *

This method needs: - *

    - *
  • the base URL of the stream (which, if you try to access to it, returns the whole - * stream, after redirects, and if the URL is valid);
  • - *
  • an {@link ItagItem}, which needs to contain the following information: - *
      - *
    • its type (see {@link ItagItem.ItagType}), to identify if the content is - * an audio or a video stream;
    • - *
    • its bitrate;
    • - *
    • its mime type;
    • - *
    • its codec(s);
    • - *
    • for an audio stream: its audio channels;
    • - *
    • for a video stream: its width and height.
    • - *
    - *
  • - *
  • the duration of the video (parameter {@code durationSecondsFallback}), which - * will be used as the stream duration if the duration could not be parsed from the - * {@link ItagItem}.
  • - *
- *

- * - * @param progressiveStreamingBaseUrl the base URL of the progressive stream, which must not be - * null - * @param itagItem the {@link ItagItem} corresponding to the stream, which - * must not be null - * @param durationSecondsFallback the duration of the progressive stream which will be used - * if the duration could not be extracted from the - * {@link ItagItem} - * @return the manifest generated into a string - */ - @Nonnull - public static String fromProgressiveStreamingUrl( - @Nonnull final String progressiveStreamingBaseUrl, - @Nonnull final ItagItem itagItem, - final long durationSecondsFallback) throws CreationException { - if (PROGRESSIVE_STREAMS_CACHE.containsKey(progressiveStreamingBaseUrl)) { - return Objects.requireNonNull( - PROGRESSIVE_STREAMS_CACHE.get(progressiveStreamingBaseUrl)).getSecond(); - } - - final long itagItemDuration = itagItem.getApproxDurationMs(); - final long streamDuration; - if (itagItemDuration != -1) { - streamDuration = itagItemDuration; - } else { - if (durationSecondsFallback > 0) { - streamDuration = durationSecondsFallback * 1000; - } else { - throw CreationException.couldNotAddElement(MPD, "the duration of the stream " - + "could not be determined and durationSecondsFallback is <= 0"); - } - } - - final Document doc = generateDocumentAndDoCommonElementsGeneration(itagItem, - streamDuration); - - generateBaseUrlElement(doc, progressiveStreamingBaseUrl); - generateSegmentBaseElement(doc, itagItem); - generateInitializationElement(doc, itagItem); - - return buildAndCacheResult(progressiveStreamingBaseUrl, doc, - PROGRESSIVE_STREAMS_CACHE); - } - - /** - * @return the cache of DASH manifests generated for progressive streams - */ - @Nonnull - public static ManifestCreatorCache getCache() { - return PROGRESSIVE_STREAMS_CACHE; - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

- * The {@code } element needs to be generated before this element with - * {@link YoutubeDashManifestCreatorsUtils#generateRepresentationElement(Document, ItagItem)}). - *

- * - * @param doc the {@link Document} on which the {@code } element will be appended - * @param baseUrl the base URL of the stream, which must not be null and will be set as the - * content of the {@code } element - */ - private static void generateBaseUrlElement(@Nonnull final Document doc, - @Nonnull final String baseUrl) - throws CreationException { - try { - final Element representationElement = (Element) doc.getElementsByTagName( - REPRESENTATION).item(0); - final Element baseURLElement = doc.createElement(BASE_URL); - baseURLElement.setTextContent(baseUrl); - representationElement.appendChild(baseURLElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(BASE_URL, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

- * It generates the following element: - *
- * {@code } - *
- * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed - * as the second parameter) - *

- * - *

- * The {@code } element needs to be generated before this element with - * {@link YoutubeDashManifestCreatorsUtils#generateRepresentationElement(Document, ItagItem)}), - * and the {@code BaseURL} element with {@link #generateBaseUrlElement(Document, String)} - * should be generated too. - *

- * - * @param doc the {@link Document} on which the {@code } element will be appended - * @param itagItem the {@link ItagItem} to use, which must not be null - */ - private static void generateSegmentBaseElement(@Nonnull final Document doc, - @Nonnull final ItagItem itagItem) - throws CreationException { - try { - final Element representationElement = (Element) doc.getElementsByTagName( - REPRESENTATION).item(0); - final Element segmentBaseElement = doc.createElement(SEGMENT_BASE); - - final String range = itagItem.getIndexStart() + "-" + itagItem.getIndexEnd(); - if (itagItem.getIndexStart() < 0 || itagItem.getIndexEnd() < 0) { - throw CreationException.couldNotAddElement(SEGMENT_BASE, - "ItagItem's indexStart or " + "indexEnd are < 0: " + range); - } - setAttribute(segmentBaseElement, doc, "indexRange", range); - - representationElement.appendChild(segmentBaseElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(SEGMENT_BASE, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

- * It generates the following element: - *
- * {@code } - *
- * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed - * as the second parameter) - *

- * - *

- * The {@code } element needs to be generated before this element with - * {@link #generateSegmentBaseElement(Document, ItagItem)}). - *

- * - * @param doc the {@link Document} on which the {@code } element will be - * appended - * @param itagItem the {@link ItagItem} to use, which must not be null - */ - private static void generateInitializationElement(@Nonnull final Document doc, - @Nonnull final ItagItem itagItem) - throws CreationException { - try { - final Element segmentBaseElement = (Element) doc.getElementsByTagName( - SEGMENT_BASE).item(0); - final Element initializationElement = doc.createElement(INITIALIZATION); - - final String range = itagItem.getInitStart() + "-" + itagItem.getInitEnd(); - if (itagItem.getInitStart() < 0 || itagItem.getInitEnd() < 0) { - throw CreationException.couldNotAddElement(INITIALIZATION, - "ItagItem's initStart and/or " + "initEnd are/is < 0: " + range); - } - setAttribute(initializationElement, doc, "range", range); - - segmentBaseElement.appendChild(initializationElement); - } catch (final DOMException e) { - throw CreationException.couldNotAddElement(INITIALIZATION, e); - } - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java index 2e38f0b115..cf3bd2f146 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java @@ -9,11 +9,19 @@ public ItagInfoRange(final int start, final int end) { this.end = end; } - public int getStart() { + public int start() { return start; } - public int getEnd() { + public int end() { return end; } + + @Override + public String toString() { + return "ItagInfoRange{" + + "start=" + start + + ", end=" + end + + '}'; + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreationException.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreationException.java new file mode 100644 index 0000000000..b2aa4927c1 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreationException.java @@ -0,0 +1,56 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator; + +import javax.annotation.Nonnull; + +/** + * Exception that is thrown when a DASH manifest creator encounters a problem + * while creating a manifest. + */ +public class DashManifestCreationException extends RuntimeException { + + public DashManifestCreationException(final String message) { + super(message); + } + + public DashManifestCreationException(final String message, final Exception cause) { + super(message, cause); + } + + // Methods to create exceptions easily without having to use big exception messages and to + // reduce duplication + + /** + * Create a new {@link DashManifestCreationException} with a cause and the following detail + * message format: + *
+ * {@code "Could not add " + element + " element", cause}, where {@code element} is an element + * of a DASH manifest. + * + * @param element the element which was not added to the DASH document + * @param cause the exception which prevented addition of the element to the DASH document + * @return a new {@link DashManifestCreationException} + */ + @Nonnull + public static DashManifestCreationException couldNotAddElement(final String element, + final Exception cause) { + return new DashManifestCreationException("Could not add " + element + " element", cause); + } + + /** + * Create a new {@link DashManifestCreationException} with a cause and the following detail + * message format: + *
+ * {@code "Could not add " + element + " element: " + reason}, where {@code element} is an + * element of a DASH manifest and {@code reason} the reason why this element cannot be added to + * the DASH document. + * + * @param element the element which was not added to the DASH document + * @param reason the reason message of why the element has been not added to the DASH document + * @return a new {@link DashManifestCreationException} + */ + @Nonnull + public static DashManifestCreationException couldNotAddElement(final String element, + final String reason) { + return new DashManifestCreationException("Could not add " + element + " element: " + reason); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java new file mode 100644 index 0000000000..66cd2d8607 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java @@ -0,0 +1,14 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator; + +import javax.annotation.Nonnull; + +public interface DashManifestCreator { + + /** + * Generates the DASH manifest. + * @return The dash manifest as string. + * @throws DashManifestCreationException May throw a CreationException + */ + @Nonnull + String generateManifest(); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreatorConstants.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreatorConstants.java new file mode 100644 index 0000000000..10d74239c0 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreatorConstants.java @@ -0,0 +1,21 @@ +package org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator; + +public final class DashManifestCreatorConstants { + private DashManifestCreatorConstants() { + // No impl! + } + + // XML elements of DASH MPD manifests + // see https://www.brendanlong.com/the-structure-of-an-mpeg-dash-mpd.html + public static final String MPD = "MPD"; + public static final String PERIOD = "Period"; + public static final String ADAPTATION_SET = "AdaptationSet"; + public static final String ROLE = "Role"; + public static final String REPRESENTATION = "Representation"; + public static final String AUDIO_CHANNEL_CONFIGURATION = "AudioChannelConfiguration"; + public static final String SEGMENT_TEMPLATE = "SegmentTemplate"; + public static final String SEGMENT_TIMELINE = "SegmentTimeline"; + public static final String BASE_URL = "BaseURL"; + public static final String SEGMENT_BASE = "SegmentBase"; + public static final String INITIALIZATION = "Initialization"; +} diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java index 0d276f9013..9a396a7f93 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java @@ -1,23 +1,44 @@ 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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreater; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreaterOrEqual; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsValidUrl; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertNotBlank; +import static org.schabi.newpipe.extractor.ServiceList.YouTube; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.ADAPTATION_SET; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.AUDIO_CHANNEL_CONFIGURATION; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.BASE_URL; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.INITIALIZATION; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.MPD; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.PERIOD; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.REPRESENTATION; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.ROLE; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.SEGMENT_BASE; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.SEGMENT_TEMPLATE; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.SEGMENT_TIMELINE; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.schabi.newpipe.downloader.DownloaderTestImpl; import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException; -import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator; -import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeOtfDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeProgressiveDashManifestCreator; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; -import javax.annotation.Nonnull; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import java.io.StringReader; import java.util.List; import java.util.Random; @@ -25,29 +46,9 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreater; -import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreaterOrEqual; -import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsValidUrl; -import static org.schabi.newpipe.extractor.ExtractorAsserts.assertNotBlank; -import static org.schabi.newpipe.extractor.ServiceList.YouTube; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.ADAPTATION_SET; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.AUDIO_CHANNEL_CONFIGURATION; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.BASE_URL; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.INITIALIZATION; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.MPD; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.PERIOD; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.REPRESENTATION; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.ROLE; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_BASE; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_TEMPLATE; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_TIMELINE; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; +import javax.annotation.Nonnull; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; /** * Test for YouTube DASH manifest creators. @@ -112,7 +113,7 @@ void testProgressiveStreams() throws Exception { assertProgressiveStreams(extractor.getAudioStreams()); // we are not able to generate DASH manifests of video formats with audio - assertThrows(CreationException.class, + assertThrows(DashManifestCreationException.class, () -> assertProgressiveStreams(extractor.getVideoStreams())); } From 411b6c130dd144ad37a4501616385559e040df62 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Tue, 14 Jun 2022 21:29:28 +0200 Subject: [PATCH 18/43] YT: Use new DASHManifestCreators --- ...ePostLiveStreamDvrDashManifestCreator.java | 4 +-- .../extractors/YoutubeStreamExtractor.java | 30 ++++++++++++++----- .../delivery/DASHManifestDeliveryData.java | 14 +++------ .../SimpleDASHManifestDeliveryDataImpl.java | 25 ++++++++++++---- 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java index 3c6bd1da8f..a58a5021a9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java @@ -17,8 +17,8 @@ public class YoutubePostLiveStreamDvrDashManifestCreator extends AbstractYoutubeDashManifestCreator { - protected YoutubePostLiveStreamDvrDashManifestCreator(@Nonnull final ItagInfo itagInfo, - final long durationSecondsFallback) { + public YoutubePostLiveStreamDvrDashManifestCreator(@Nonnull final ItagInfo itagInfo, + final long durationSecondsFallback) { super(itagInfo, durationSecondsFallback); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java index 75e6037584..bae738397d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java @@ -66,6 +66,9 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptExtractor; import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.services.youtube.YoutubeThrottlingDecrypter; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeOtfDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubePostLiveStreamDvrDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeProgressiveDashManifestCreator; import org.schabi.newpipe.extractor.services.youtube.itag.delivery.HLSItagFormatDeliveryData; import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData; import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ProgressiveHTTPItagFormatDeliveryData; @@ -82,6 +85,8 @@ import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamSegment; import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreator; +import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleDASHManifestDeliveryDataImpl; import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl; import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl; import org.schabi.newpipe.extractor.streamdata.format.registry.SubtitleFormatRegistry; @@ -1254,16 +1259,27 @@ private > DeliveryData buildDeliveryData(final ItagInfo< } // DASH - // TODO - if ("FORMAT_STREAM_TYPE_OTF".equalsIgnoreCase(itagInfo.getType())) { - // OTF DASH MANIFEST - } else if (isPostLive()) { - // YoutubePostLiveStreamDvrDashManifestCreator + // Duration in seconds used as fallback inside the dashManifestCreators + long durationInSec; + try { + durationInSec = getLength(); + } catch (final ParsingException e) { + durationInSec = -1; } - // YoutubeProgressiveDashManifestCreator + return new SimpleDASHManifestDeliveryDataImpl( + getDashManifestCreatorConstructor(itagInfo).apply(itagInfo, durationInSec)); + } - return null; + private BiFunction, Long, DashManifestCreator> getDashManifestCreatorConstructor( + final ItagInfo itagInfo + ) { + if ("FORMAT_STREAM_TYPE_OTF".equalsIgnoreCase(itagInfo.getType())) { + return YoutubeOtfDashManifestCreator::new; + } else if (isPostLive()) { + return YoutubePostLiveStreamDvrDashManifestCreator::new; + } + return YoutubeProgressiveDashManifestCreator::new; } @Nonnull diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java index ce2452a732..914c6c1d8c 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java @@ -1,18 +1,12 @@ package org.schabi.newpipe.extractor.streamdata.delivery; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreator; + import javax.annotation.Nonnull; public interface DASHManifestDeliveryData extends DASHDeliveryData { - /** - * Returns the base url for the DashManifest. - * - * @return - */ - // TODO: Check removal @Nonnull - default String getBaseUrl() { - return ""; - } + DashManifestCreator getDashManifestCreator(); - String getManifestAsString(); + String getCachedDashManifestAsString(); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java index 767ce3a044..0102e7d59b 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java @@ -1,9 +1,9 @@ package org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl; import org.schabi.newpipe.extractor.streamdata.delivery.DASHManifestDeliveryData; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreator; import java.util.Objects; -import java.util.function.Supplier; import javax.annotation.Nonnull; @@ -21,14 +21,27 @@ public class SimpleDASHManifestDeliveryDataImpl extends AbstractDeliveryDataImpl implements DASHManifestDeliveryData { @Nonnull - private final Supplier dashManifestBuilder; + private final DashManifestCreator dashManifestCreator; - public SimpleDASHManifestDeliveryDataImpl(@Nonnull final Supplier dashManifestBuilder) { - this.dashManifestBuilder = Objects.requireNonNull(dashManifestBuilder); + private String cachedDashManifest; + + public SimpleDASHManifestDeliveryDataImpl( + @Nonnull final DashManifestCreator dashManifestCreator + ) { + this.dashManifestCreator = Objects.requireNonNull(dashManifestCreator); + } + + @Override + @Nonnull + public DashManifestCreator getDashManifestCreator() { + return dashManifestCreator; } @Override - public String getManifestAsString() { - return dashManifestBuilder.get(); + public String getCachedDashManifestAsString() { + if (cachedDashManifest != null) { + cachedDashManifest = getDashManifestCreator().generateManifest(); + } + return cachedDashManifest; } } \ No newline at end of file From 2ef215a75f59932d68fa4df918e905bbc94f7f42 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Tue, 14 Jun 2022 21:47:00 +0200 Subject: [PATCH 19/43] Fixed checkstyle problems --- .../AbstractYoutubeDashManifestCreator.java | 23 +++++++++------- .../YoutubeOtfDashManifestCreator.java | 13 +++++----- ...ePostLiveStreamDvrDashManifestCreator.java | 3 ++- ...YoutubeProgressiveDashManifestCreator.java | 4 +-- ...ProgressiveHTTPItagFormatDeliveryData.java | 3 ++- .../format/registry/ItagFormatRegistry.java | 4 +++ .../youtube/itag/info/ItagInfoRange.java | 8 +++--- .../newpipe/extractor/stream/StreamInfo.java | 5 ++-- .../DashManifestCreationException.java | 5 ++-- .../SimpleDASHManifestDeliveryDataImpl.java | 2 +- .../format/AbstractMediaFormat.java | 8 ++++-- .../streamdata/stream/AudioStream.java | 2 +- .../extractor/streamdata/stream/Stream.java | 2 +- .../stream/quality/VideoQualityData.java | 2 +- .../simpleimpl/SimpleAudioStreamImpl.java | 5 ++-- .../stream/util/NewPipeStreamCollectors.java | 26 +++++++++---------- .../stream/util/NewPipeStreamUtil.java | 2 +- 17 files changed, 66 insertions(+), 51 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java index 95a4fc906a..3e4102befa 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java @@ -478,20 +478,23 @@ protected void generateSegmentTimelineElement() // region initResponse + @SuppressWarnings("checkstyle:FinalParameters") @Nonnull - protected Response getInitializationResponse(@Nonnull String baseStreamingUrl) { + protected Response getInitializationResponse(@Nonnull final String baseStreamingUrl) { final boolean isHtml5StreamingUrl = isWebStreamingUrl(baseStreamingUrl) || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(baseStreamingUrl); final boolean isAndroidStreamingUrl = isAndroidStreamingUrl(baseStreamingUrl); final boolean isIosStreamingUrl = isIosStreamingUrl(baseStreamingUrl); + + String streamingUrl = baseStreamingUrl; if (isHtml5StreamingUrl) { - baseStreamingUrl += ALR_YES; + streamingUrl += ALR_YES; } - baseStreamingUrl = appendBaseStreamingUrlParams(baseStreamingUrl); + streamingUrl = appendBaseStreamingUrlParams(streamingUrl); final Downloader downloader = NewPipe.getDownloader(); if (isHtml5StreamingUrl) { - return getStreamingWebUrlWithoutRedirects(downloader, baseStreamingUrl); + return getStreamingWebUrlWithoutRedirects(downloader, streamingUrl); } else if (isAndroidStreamingUrl || isIosStreamingUrl) { try { final Map> headers = new HashMap<>(); @@ -500,7 +503,7 @@ protected Response getInitializationResponse(@Nonnull String baseStreamingUrl) { ? getAndroidUserAgent(null) : getIosUserAgent(null))); final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8); - return downloader.post(baseStreamingUrl, headers, emptyBody); + return downloader.post(streamingUrl, headers, emptyBody); } catch (final IOException | ExtractionException e) { throw new DashManifestCreationException("Could not get the " + (isIosStreamingUrl ? "IOS" : "ANDROID") + " streaming URL response", e); @@ -508,7 +511,7 @@ protected Response getInitializationResponse(@Nonnull String baseStreamingUrl) { } try { - return downloader.get(baseStreamingUrl); + return downloader.get(streamingUrl); } catch (final IOException | ExtractionException e) { throw new DashManifestCreationException("Could not get the streaming URL response", e); } @@ -517,13 +520,15 @@ protected Response getInitializationResponse(@Nonnull String baseStreamingUrl) { @Nonnull protected Response getStreamingWebUrlWithoutRedirects( @Nonnull final Downloader downloader, - @Nonnull String streamingUrl) { + @Nonnull final String streamingUrl) { try { final Map> headers = new HashMap<>(); addClientInfoHeaders(headers); + String currentStreamingUrl = streamingUrl; + for (int r = 0; r < MAXIMUM_REDIRECT_COUNT; r++) { - final Response response = downloader.get(streamingUrl, headers); + final Response response = downloader.get(currentStreamingUrl, headers); final int responseCode = response.responseCode(); if (responseCode != 200) { @@ -544,7 +549,7 @@ protected Response getStreamingWebUrlWithoutRedirects( return response; } - streamingUrl = response.responseBody(); + currentStreamingUrl = response.responseBody(); } throw new DashManifestCreationException("Too many redirects"); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java index ad4bc55931..a29f48a639 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java @@ -35,9 +35,8 @@ public String generateManifest() { final int responseCode = response.responseCode(); if (responseCode != 200) { - throw new DashManifestCreationException("Could not get the initialization URL: " + - "response code " - + responseCode); + throw new DashManifestCreationException("Could not get the initialization URL: " + + "response code " + responseCode); } final String[] segmentDurations; @@ -150,13 +149,13 @@ protected long getStreamDuration(@Nonnull final String[] segmentDurations) { }) .sum(); } catch (final NumberFormatException e) { - throw new DashManifestCreationException("Could not get stream length from sequences " + - "list", e); + throw new DashManifestCreationException( + "Could not get stream length from sequences list", e); } } - protected Stream streamAndSplitSegmentDurations(@Nonnull final String[] segmentDurations) { - return Stream.of(segmentDurations) + protected Stream streamAndSplitSegmentDurations(@Nonnull final String[] durations) { + return Stream.of(durations) .map(segDuration -> segDuration.split("\\(r=")); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java index a58a5021a9..1b7c769cf2 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java @@ -15,7 +15,8 @@ import javax.annotation.Nonnull; -public class YoutubePostLiveStreamDvrDashManifestCreator extends AbstractYoutubeDashManifestCreator { +public class YoutubePostLiveStreamDvrDashManifestCreator + extends AbstractYoutubeDashManifestCreator { public YoutubePostLiveStreamDvrDashManifestCreator(@Nonnull final ItagInfo itagInfo, final long durationSecondsFallback) { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java index c368712a6e..705c0ee235 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java @@ -29,8 +29,8 @@ public String generateManifest() { } else if (durationSecondsFallback > 0) { streamDurationMs = durationSecondsFallback * 1000; } else { - throw DashManifestCreationException.couldNotAddElement(MPD, "the duration of the " + - "stream could not be determined and durationSecondsFallback is <= 0"); + throw DashManifestCreationException.couldNotAddElement(MPD, + "unable to determine duration and fallback is invalid"); } generateDocumentAndCommonElements(streamDurationMs); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java index c01473bd3f..db805f874a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java @@ -2,6 +2,7 @@ import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ProgressiveHTTPItagFormatDeliveryData; -public class SimpleProgressiveHTTPItagFormatDeliveryData implements ProgressiveHTTPItagFormatDeliveryData { +public class SimpleProgressiveHTTPItagFormatDeliveryData + implements ProgressiveHTTPItagFormatDeliveryData { // Just a marker for now } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java index ae66b627e2..e69dc49887 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java @@ -20,7 +20,9 @@ import java.util.stream.Stream; +// CHECKSTYLE:OFF - Link is too long // https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1195 +// CHECKSTYLE:ON public final class ItagFormatRegistry { public static final VideoAudioItagFormat[] VIDEO_AUDIO_FORMATS = new VideoAudioItagFormat[]{ @@ -35,7 +37,9 @@ public final class ItagFormatRegistry { new SimpleVideoAudioItagFormat(35, MPEG_4, fromHeightWidth(480, 854), 128), // Itag 36 is no longer used because the height is unstable and it's not returned by YT + // CHECKSTYLE:OFF - Link is too long // see also: https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1204 + // CHECKSTYLE:ON new SimpleVideoAudioItagFormat(37, MPEG_4, fromHeightWidth(1080, 1920), 192), new SimpleVideoAudioItagFormat(38, MPEG_4, fromHeightWidth(3072, 4092), 192), diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java index cf3bd2f146..6b5916985b 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java @@ -19,9 +19,9 @@ public int end() { @Override public String toString() { - return "ItagInfoRange{" + - "start=" + start + - ", end=" + end + - '}'; + return "ItagInfoRange{" + + "start=" + start + + ", end=" + end + + '}'; } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index 14511946a7..5b5710945a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -99,6 +99,7 @@ public class StreamInfo extends Info { private List streamSegments = new ArrayList<>(); private List metaInfo = new ArrayList<>(); + @SuppressWarnings("checkstyle:ParameterNumber") public StreamInfo(final int serviceId, final String url, final String originalUrl, @@ -554,8 +555,8 @@ private static void extractStreams(final StreamInfo streamInfo, && streamInfo.getDashMpdUrl().trim().isEmpty() && streamInfo.getHlsMasterPlaylistUrl().trim().isEmpty() ) { - throw new StreamExtractException("Could not get any required streaming-data. " + - "See error variable to get further details."); + throw new StreamExtractException("Could not get any required streaming-data. " + + "See error variable to get further details."); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreationException.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreationException.java index b2aa4927c1..4c60e7fc73 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreationException.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreationException.java @@ -7,7 +7,7 @@ * while creating a manifest. */ public class DashManifestCreationException extends RuntimeException { - + public DashManifestCreationException(final String message) { super(message); } @@ -51,6 +51,7 @@ public static DashManifestCreationException couldNotAddElement(final String elem @Nonnull public static DashManifestCreationException couldNotAddElement(final String element, final String reason) { - return new DashManifestCreationException("Could not add " + element + " element: " + reason); + return new DashManifestCreationException( + "Could not add " + element + " element: " + reason); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java index 0102e7d59b..391634399e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java @@ -44,4 +44,4 @@ public String getCachedDashManifestAsString() { } return cachedDashManifest; } -} \ No newline at end of file +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java index 4420f66518..68f91e02c5 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/AbstractMediaFormat.java @@ -47,8 +47,12 @@ public String mimeType() { @Override public boolean equals(final Object o) { - if (this == o) return true; - if (!(o instanceof AbstractMediaFormat)) return false; + if (this == o) { + return true; + } + if (!(o instanceof AbstractMediaFormat)) { + return false; + } final AbstractMediaFormat that = (AbstractMediaFormat) o; return id() == that.id() && Objects.equals(name(), that.name()) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java index b7290cb278..e3bb2a252e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java @@ -10,7 +10,7 @@ * Represents a audio (only) stream. */ public interface AudioStream extends Stream, BaseAudioStream { - + @Override default boolean equalsStream(@Nullable final Stream other) { if (!(other instanceof AudioStream)) { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java index 2346e982e1..3eccbe9b3e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java @@ -21,5 +21,5 @@ public interface Stream { // TODO: May also have to check deliverydata - boolean equalsStream(@Nullable final Stream other); + boolean equalsStream(@Nullable Stream other); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java index 02022a2301..86bef7ab1f 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/quality/VideoQualityData.java @@ -1,7 +1,7 @@ package org.schabi.newpipe.extractor.streamdata.stream.quality; public class VideoQualityData { - public static int UNKNOWN = -1; + public static final int UNKNOWN = -1; private final int height; private final int width; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java index d301c38c28..c87cd39d90 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleAudioStreamImpl.java @@ -6,7 +6,8 @@ import javax.annotation.Nonnull; -public class SimpleAudioStreamImpl extends AbstractStreamImpl implements AudioStream { +public class SimpleAudioStreamImpl extends AbstractStreamImpl + implements AudioStream { private final int averageBitrate; public SimpleAudioStreamImpl( @@ -24,7 +25,7 @@ public SimpleAudioStreamImpl( ) { this(mediaFormat, deliveryData, UNKNOWN_AVG_BITRATE); } - + @Override public int averageBitrate() { return averageBitrate; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java index f9d1d03f04..bdbf84163e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java @@ -17,23 +17,23 @@ private NewPipeStreamCollectors() { // No impl } - public static - Collector> toDistinctList() { + public static > + Collector> toDistinctList() { return deduplicateEqualStreams(x -> x); } - public static - Collector> toDistinctStream() { + public static > + Collector> toDistinctStream() { return deduplicateEqualStreams(List::stream); } - public static - Collector deduplicateEqualStreams(final Function, R> finisher) { + public static , R> + Collector deduplicateEqualStreams(final Function, R> finisher) { return new CollectorImpl<>( (Supplier>) ArrayList::new, List::add, (left, right) -> { - for(final T rightElement : right) { + for (final T rightElement : right) { if (NewPipeStreamUtil.containSimilarStream(rightElement, left)) { left.add(rightElement); } @@ -44,15 +44,11 @@ private NewPipeStreamCollectors() { CH_ID); } - /** - * Copied from {@link java.util.stream.Collectors} - */ + // region COPIED FROM java.util.stream.Collectors + static final Set CH_ID = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); - /** - * Copied from {@link java.util.stream.Collectors} - */ static class CollectorImpl implements Collector { private final Supplier supplier; private final BiConsumer accumulator; @@ -63,7 +59,7 @@ static class CollectorImpl implements Collector { CollectorImpl(final Supplier supplier, final BiConsumer accumulator, final BinaryOperator combiner, - final Function finisher, + final Function finisher, final Set characteristics) { this.supplier = supplier; this.accumulator = accumulator; @@ -97,4 +93,6 @@ public Set characteristics() { return characteristics; } } + + //endregion } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java index 78a62a9454..76fbc34cc0 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java @@ -28,7 +28,7 @@ public static boolean containSimilarStream(final Stream stream, public List removeEqualStreams(final Collection streams) { final List returnList = new ArrayList<>(); - for(final T stream : streams) { + for (final T stream : streams) { if (!containSimilarStream(stream, returnList)) { returnList.add(stream); } From 736366920b3e81cfa478d79717ce4d1f8bef57d9 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Tue, 14 Jun 2022 22:24:42 +0200 Subject: [PATCH 20/43] Fixed API --- .../streamdata/delivery/ProgressiveHTTPDeliveryData.java | 2 +- .../schabi/newpipe/extractor/streamdata/stream/VideoStream.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java index 3817d47071..7591af4624 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java @@ -1,5 +1,5 @@ package org.schabi.newpipe.extractor.streamdata.delivery; -public interface ProgressiveHTTPDeliveryData extends DeliveryData { +public interface ProgressiveHTTPDeliveryData extends UrlBasedDeliveryData { // Nothing to implement additionally } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java index e55e4df754..5ad3b1f3f1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java @@ -5,12 +5,14 @@ import java.util.Objects; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * Represents a video (only) stream. */ public interface VideoStream extends Stream { + @Nonnull VideoQualityData videoQualityData(); @Override From 9da8dd8bd4fbe9d3124b9be93462f1e1d6ae6a99 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Tue, 14 Jun 2022 22:24:50 +0200 Subject: [PATCH 21/43] Started fixing tests... --- .../services/BaseStreamExtractorTest.java | 3 +- .../services/DefaultStreamExtractorTest.java | 119 ++++++++++-------- .../BandcampRadioStreamExtractorTest.java | 15 ++- .../bandcamp/BandcampStreamExtractorTest.java | 6 +- .../MediaCCCStreamExtractorTest.java | 10 +- .../peertube/PeertubeStreamExtractorTest.java | 3 - .../SoundcloudStreamExtractorTest.java | 12 +- ...utubeStreamExtractorAgeRestrictedTest.java | 1 - ...utubeStreamExtractorControversialTest.java | 1 - .../YoutubeStreamExtractorDefaultTest.java | 6 - .../YoutubeStreamExtractorLivestreamTest.java | 2 +- .../YoutubeStreamExtractorRelatedMixTest.java | 1 - .../YoutubeStreamExtractorUnlistedTest.java | 1 - 13 files changed, 89 insertions(+), 91 deletions(-) diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseStreamExtractorTest.java index da418660a1..c058a21fb3 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseStreamExtractorTest.java @@ -22,7 +22,8 @@ public interface BaseStreamExtractorTest extends BaseExtractorTest { void testAgeLimit() throws Exception; void testErrorMessage() throws Exception; void testAudioStreams() throws Exception; - void testVideoStreams() throws Exception; + void testVideoOnlyStreams() throws Exception; + void testVideoAudioStreams() throws Exception; void testSubtitles() throws Exception; void testGetDashMpdUrl() throws Exception; void testFrames() throws Exception; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java index bbff34e63c..fbbc337b30 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java @@ -15,15 +15,20 @@ import org.junit.jupiter.api.Test; import org.schabi.newpipe.extractor.ExtractorAsserts; import org.schabi.newpipe.extractor.InfoItemsCollector; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MetaInfo; import org.schabi.newpipe.extractor.localization.DateWrapper; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Frameset; import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.extractor.streamdata.delivery.DASHManifestDeliveryData; +import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; +import org.schabi.newpipe.extractor.streamdata.delivery.UrlBasedDeliveryData; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoStream; import java.net.MalformedURLException; import java.net.URL; @@ -42,7 +47,8 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest implements BaseStreamExtractorTest { - public abstract StreamType expectedStreamType(); + public boolean expectedIsLive() { return false; } + public boolean expectedIsAudioOnly() { return false; } public abstract String expectedUploaderName(); public abstract String expectedUploaderUrl(); public boolean expectedUploaderVerified() { return false; } @@ -61,8 +67,9 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest videoStreams = extractor().getVideoStreams(); + public void testVideoOnlyStreams() throws Exception { final List videoOnlyStreams = extractor().getVideoOnlyStreams(); - assertNotNull(videoStreams); assertNotNull(videoOnlyStreams); - videoStreams.addAll(videoOnlyStreams); - if (expectedHasVideoStreams()) { - assertFalse(videoStreams.isEmpty()); + if (expectedHasVideoOnlyStreams()) { + assertFalse(videoOnlyStreams.isEmpty()); - for (final VideoStream stream : videoStreams) { - if (stream.isUrl()) { - assertIsSecureUrl(stream.getContent()); - } - final StreamType streamType = extractor().getStreamType(); - // On some video streams, the resolution can be empty and the format be unknown, - // especially on livestreams (like streams with HLS master playlists) - if (streamType != StreamType.LIVE_STREAM - && streamType != StreamType.AUDIO_LIVE_STREAM) { - assertFalse(stream.getResolution().isEmpty()); - final int formatId = stream.getFormatId(); - // see MediaFormat: video stream formats range from 0 to 0x100 - assertTrue(0 <= formatId && formatId < 0x100, - "Format id does not fit a video stream: " + formatId); - } + for (final VideoStream stream : videoOnlyStreams) { + assertNotNull(stream.mediaFormat()); + assertNotNull(stream.videoQualityData()); + checkDeliveryData(stream.deliveryData()); + } + } else { + assertTrue(videoOnlyStreams.isEmpty()); + } + } + + @Test + @Override + public void testVideoAudioStreams() throws Exception { + final List videoAudioStreams = extractor().getVideoStreams(); + assertNotNull(videoAudioStreams); + + if (expectedHasVideoAndAudioStreams()) { + assertFalse(videoAudioStreams.isEmpty()); + + for (final VideoAudioStream stream : videoAudioStreams) { + assertNotNull(stream.mediaFormat()); + assertNotNull(stream.videoQualityData()); + checkDeliveryData(stream.deliveryData()); } } else { - assertTrue(videoStreams.isEmpty()); + assertTrue(videoAudioStreams.isEmpty()); } } @@ -295,17 +307,8 @@ public void testAudioStreams() throws Exception { assertFalse(audioStreams.isEmpty()); for (final AudioStream stream : audioStreams) { - if (stream.isUrl()) { - assertIsSecureUrl(stream.getContent()); - } - - // The media format can be unknown on some audio streams - if (stream.getFormat() != null) { - final int formatId = stream.getFormat().id; - // see MediaFormat: audio stream formats range from 0x100 to 0x1000 - assertTrue(0x100 <= formatId && formatId < 0x1000, - "Format id does not fit an audio stream: " + formatId); - } + assertNotNull(stream.mediaFormat()); + checkDeliveryData(stream.deliveryData()); } } else { assertTrue(audioStreams.isEmpty()); @@ -315,32 +318,29 @@ public void testAudioStreams() throws Exception { @Test @Override public void testSubtitles() throws Exception { - final List subtitles = extractor().getSubtitlesDefault(); + final List subtitles = extractor().getSubtitles(); assertNotNull(subtitles); if (expectedHasSubtitles()) { assertFalse(subtitles.isEmpty()); - for (final SubtitlesStream stream : subtitles) { - if (stream.isUrl()) { - assertIsSecureUrl(stream.getContent()); - } - - final int formatId = stream.getFormatId(); - // see MediaFormat: video stream formats range from 0x1000 to 0x10000 - assertTrue(0x1000 <= formatId && formatId < 0x10000, - "Format id does not fit a subtitles stream: " + formatId); + for (final SubtitleStream stream : subtitles) { + assertNotNull(stream.languageCode()); + assertNotNull(stream.mediaFormat()); + checkDeliveryData(stream.deliveryData()); } } else { assertTrue(subtitles.isEmpty()); + } + } - final MediaFormat[] formats = {MediaFormat.VTT, MediaFormat.TTML, MediaFormat.SRT, - MediaFormat.TRANSCRIPT1, MediaFormat.TRANSCRIPT2, MediaFormat.TRANSCRIPT3}; - for (final MediaFormat format : formats) { - final List formatSubtitles = extractor().getSubtitles(format); - assertNotNull(formatSubtitles); - assertTrue(formatSubtitles.isEmpty()); - } + private void checkDeliveryData(final DeliveryData deliveryData) { + if (deliveryData instanceof UrlBasedDeliveryData) { + assertIsSecureUrl(((UrlBasedDeliveryData) deliveryData).url()); + } else if (deliveryData instanceof DASHManifestDeliveryData) { + final DASHManifestDeliveryData dashManifestDD = + (DASHManifestDeliveryData) deliveryData; + assertNotNull(dashManifestDD.getDashManifestCreator()); } } @@ -455,6 +455,15 @@ public void testMetaInfo() throws Exception { assertTrue(urls.contains(expectedUrl)); } } + } + + @Test + public void testIsLive() throws Exception { + assertEquals(expectedIsLive(), extractor().isLive()); + } + @Test + public void testIsAudioOnly() throws Exception { + assertEquals(expectedIsAudioOnly(), extractor().isAudioOnly()); } } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampRadioStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampRadioStreamExtractorTest.java index 01ab77bfef..76976b02b5 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampRadioStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampRadioStreamExtractorTest.java @@ -1,5 +1,11 @@ package org.schabi.newpipe.extractor.services.bandcamp; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.schabi.newpipe.extractor.ServiceList.Bandcamp; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.schabi.newpipe.downloader.DownloaderTestImpl; @@ -11,7 +17,6 @@ import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import java.io.IOException; import java.util.Calendar; @@ -19,9 +24,6 @@ import java.util.List; import java.util.TimeZone; -import static org.junit.jupiter.api.Assertions.*; -import static org.schabi.newpipe.extractor.ServiceList.Bandcamp; - public class BandcampRadioStreamExtractorTest extends DefaultStreamExtractorTest { private static StreamExtractor extractor; @@ -47,11 +49,11 @@ public void testGettingCorrectStreamExtractor() throws ExtractionException { @Override public String expectedId() throws Exception { return "230"; } @Override public String expectedUrlContains() throws Exception { return URL; } @Override public String expectedOriginalUrlContains() throws Exception { return URL; } - @Override public boolean expectedHasVideoStreams() { return false; } + @Override public boolean expectedHasVideoAndAudioStreams() { return false; } @Override public boolean expectedHasSubtitles() { return false; } @Override public boolean expectedHasFrames() { return false; } @Override public boolean expectedHasRelatedItems() { return false; } - @Override public StreamType expectedStreamType() { return StreamType.AUDIO_STREAM; } + @Override public boolean expectedIsAudioOnly() { return true; } @Override public StreamingService expectedService() { return Bandcamp; } @Override public String expectedUploaderName() { return "Andrew Jervis"; } @Override public int expectedStreamSegmentsCount() { return 30; } @@ -80,6 +82,7 @@ public List expectedDescriptionContains() { @Override public String expectedUploadDate() { return "16 May 2017 00:00:00 GMT"; } @Override public String expectedTextualUploadDate() { return "16 May 2017 00:00:00 GMT"; } + @Override @Test public void testUploadDate() throws ParsingException { final Calendar expectedCalendar = Calendar.getInstance(); diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java index 3a582db287..8c707156b9 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java @@ -70,8 +70,8 @@ public String expectedOriginalUrlContains() { } @Override - public StreamType expectedStreamType() { - return StreamType.AUDIO_STREAM; + public boolean expectedIsAudioOnly() { + return true; } @Override @@ -120,7 +120,7 @@ public long expectedDislikeCountAtLeast() { } @Override - public boolean expectedHasVideoStreams() { + public boolean expectedHasVideoAndAudioStreams() { return false; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java index 458946ba3e..bd387d28d1 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java @@ -43,7 +43,6 @@ public static void setUp() throws Exception { @Override public String expectedId() { return ID; } @Override public String expectedUrlContains() { return URL; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "gpn18"; } @Override public String expectedUploaderUrl() { return "https://media.ccc.de/c/gpn18"; } @Override public List expectedDescriptionContains() { return Arrays.asList("SSH-Sessions", "\"Terminal Multiplexer\""); } @@ -76,8 +75,8 @@ public void testUploaderAvatarUrl() throws Exception { @Override @Test - public void testVideoStreams() throws Exception { - super.testVideoStreams(); + public void testVideoOnlyStreams() throws Exception { + super.testVideoOnlyStreams(); assertEquals(4, extractor.getVideoStreams().size()); } @@ -115,7 +114,6 @@ public static void setUp() throws Exception { } @Override public String expectedUrlContains() { return URL; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "36c3"; } @Override public String expectedUploaderUrl() { return "https://media.ccc.de/c/36c3"; } @Override public List expectedDescriptionContains() { return Arrays.asList("WhatsApp", "Signal"); } @@ -146,8 +144,8 @@ public void testUploaderAvatarUrl() throws Exception { @Override @Test - public void testVideoStreams() throws Exception { - super.testVideoStreams(); + public void testVideoOnlyStreams() throws Exception { + super.testVideoOnlyStreams(); assertEquals(8, extractor.getVideoStreams().size()); } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java index 63a7839fbf..a33089dc03 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java @@ -57,7 +57,6 @@ public void testGetLanguageInformation() throws ParsingException { @Override public String expectedUrlContains() { return INSTANCE + BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "Framasoft"; } @Override public String expectedUploaderUrl() { return "https://framatube.org/accounts/framasoft@framatube.org"; } @Override public String expectedSubChannelName() { return "A propos de PeerTube"; } @@ -118,7 +117,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return INSTANCE + BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "Marinauts"; } @Override public String expectedUploaderUrl() { return "https://tilvids.com/accounts/marinauts@tilvids.com"; } @Override public String expectedSubChannelName() { return "Main marinauts channel"; } @@ -163,7 +161,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return INSTANCE + BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "Résilience humaine"; } @Override public String expectedUploaderUrl() { return "https://nocensoring.net/accounts/gmt@nocensoring.net"; } @Override public String expectedSubChannelName() { return "SYSTEM FAILURE Quel à-venir ?"; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java index da2c0511dd..fb44ca3427 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java @@ -53,7 +53,7 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return UPLOADER + "/" + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.AUDIO_STREAM; } + @Override public boolean expectedIsAudioOnly() { return true; } @Override public String expectedUploaderName() { return "Jess Glynne"; } @Override public String expectedUploaderUrl() { return UPLOADER; } @Override public boolean expectedUploaderVerified() { return true; } @@ -67,7 +67,7 @@ public static void setUp() throws Exception { @Override public long expectedLikeCountAtLeast() { return -1; } @Override public long expectedDislikeCountAtLeast() { return -1; } @Override public boolean expectedHasAudioStreams() { return false; } - @Override public boolean expectedHasVideoStreams() { return false; } + @Override public boolean expectedHasVideoAndAudioStreams() { return false; } @Override public boolean expectedHasSubtitles() { return false; } @Override public boolean expectedHasFrames() { return false; } @Override public int expectedStreamSegmentsCount() { return 0; } @@ -116,7 +116,7 @@ public void testRelatedItems() throws Exception { @Override public String expectedUrlContains() { return UPLOADER + "/" + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.AUDIO_STREAM; } + @Override public boolean expectedIsAudioOnly() { return true; } @Override public String expectedUploaderName() { return "martinsolveig"; } @Override public String expectedUploaderUrl() { return UPLOADER; } @Override public boolean expectedUploaderVerified() { return true; } @@ -130,7 +130,7 @@ public void testRelatedItems() throws Exception { @Override public long expectedLikeCountAtLeast() { return -1; } @Override public long expectedDislikeCountAtLeast() { return -1; } @Override public boolean expectedHasAudioStreams() { return false; } - @Override public boolean expectedHasVideoStreams() { return false; } + @Override public boolean expectedHasVideoAndAudioStreams() { return false; } @Override public boolean expectedHasRelatedItems() { return true; } @Override public boolean expectedHasSubtitles() { return false; } @Override public boolean expectedHasFrames() { return false; } @@ -160,7 +160,7 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return UPLOADER + "/" + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.AUDIO_STREAM; } + @Override public boolean expectedIsAudioOnly() { return true; } @Override public String expectedUploaderName() { return "Creative Commons"; } @Override public String expectedUploaderUrl() { return UPLOADER; } @Override public List expectedDescriptionContains() { return Arrays.asList("Stigmergy is a mechanism of indirect coordination", @@ -172,7 +172,7 @@ public static void setUp() throws Exception { @Nullable @Override public String expectedTextualUploadDate() { return "2019-03-28 13:36:18"; } @Override public long expectedLikeCountAtLeast() { return -1; } @Override public long expectedDislikeCountAtLeast() { return -1; } - @Override public boolean expectedHasVideoStreams() { return false; } + @Override public boolean expectedHasVideoAndAudioStreams() { return false; } @Override public boolean expectedHasSubtitles() { return false; } @Override public boolean expectedHasFrames() { return false; } @Override public int expectedStreamSegmentsCount() { return 0; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorAgeRestrictedTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorAgeRestrictedTest.java index f72f598c9c..62315260e3 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorAgeRestrictedTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorAgeRestrictedTest.java @@ -38,7 +38,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return YoutubeStreamExtractorDefaultTest.BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "DAN TV"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCcQHIVL83g5BEQe2IJFb-6w"; } @Override public long expectedUploaderSubscriberCountAtLeast() { return 50; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorControversialTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorControversialTest.java index 085fce5be8..89c5b7f305 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorControversialTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorControversialTest.java @@ -42,7 +42,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return URL; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "Amazing Atheist"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCjNxszyFPasDdRoD9J6X-sw"; } @Override public long expectedUploaderSubscriberCountAtLeast() { return 900_000; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java index 596367c4ad..82bbd8ae28 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java @@ -134,7 +134,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "PewDiePie"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UC-lHJZR3Gqxm24_Vd_AJ5Yw"; } @Override public long expectedUploaderSubscriberCountAtLeast() { return 110_000_000; } @@ -177,7 +176,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return URL; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "Unbox Therapy"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCsTcErHg8oDvUnTzoqsYeNw"; } @Override public long expectedUploaderSubscriberCountAtLeast() { return 18_000_000; } @@ -230,7 +228,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "YouTuber PrinceOfFALLEN"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCQT2yul0lr6Ie9qNQNmw-sg"; } @Override public List expectedDescriptionContains() { return Arrays.asList("dislikes", "Alpha", "wrong"); } @@ -266,7 +263,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "tagesschau"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UC5NOEUbkLheQcaaRldYW5GA"; } @Override public long expectedUploaderSubscriberCountAtLeast() { return 1_000_000; } @@ -327,7 +323,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "maiLab"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCyHDQ5C6z1NDmJ4g6SerW8g"; } @Override public long expectedUploaderSubscriberCountAtLeast() { return 1_400_000; } @@ -395,7 +390,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "Dinge Erklärt – Kurzgesagt"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCwRH985XgMYXQ6NxXDo8npw"; } @Override public long expectedUploaderSubscriberCountAtLeast() { return 1_500_000; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java index 6b26a0d04e..7d5b0032b6 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java @@ -45,7 +45,7 @@ public void testUploaderName() throws Exception { @Override public String expectedUrlContains() { return YoutubeStreamExtractorDefaultTest.BASE_URL + ID; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.LIVE_STREAM; } + @Override public boolean expectedIsLive() { return true; } @Override public String expectedUploaderName() { return "Lofi Girl"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCSJ4gkVC6NrvII8umztf0Ow"; } @Override public long expectedUploaderSubscriberCountAtLeast() { return 9_800_000; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorRelatedMixTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorRelatedMixTest.java index cd7feb3527..a33faed314 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorRelatedMixTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorRelatedMixTest.java @@ -52,7 +52,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return URL; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "NoCopyrightSounds"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UC_aEa8K-EOJ3D6gOs7HcyNg"; } @Override public List expectedDescriptionContains() { diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java index 4f7225e4ae..5a5eb9d9e3 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java @@ -39,7 +39,6 @@ public static void setUp() throws Exception { @Override public String expectedUrlContains() { return URL; } @Override public String expectedOriginalUrlContains() { return URL; } - @Override public StreamType expectedStreamType() { return StreamType.VIDEO_STREAM; } @Override public String expectedUploaderName() { return "Hooked"; } @Override public String expectedUploaderUrl() { return "https://www.youtube.com/channel/UCPysfiuOv4VKBeXFFPhKXyw"; } @Override public long expectedUploaderSubscriberCountAtLeast() { return 24_300; } From 943b46c0816416d8d57f5d0867a05170bd10f128 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 16 Jun 2022 14:59:32 +0200 Subject: [PATCH 22/43] Fixed most tests --- .../services/BaseStreamExtractorTest.java | 1 - .../services/DefaultStreamExtractorTest.java | 6 +- .../bandcamp/BandcampStreamExtractorTest.java | 1 - .../services/media_ccc/MediaCCCOggTest.java | 4 +- .../MediaCCCStreamExtractorTest.java | 1 - .../peertube/PeertubeStreamExtractorTest.java | 1 - .../SoundcloudStreamExtractorTest.java | 42 ++-- .../YoutubeChannelLocalizationTest.java | 215 +++++++++--------- ...utubeStreamExtractorAgeRestrictedTest.java | 1 - ...utubeStreamExtractorControversialTest.java | 1 - .../YoutubeStreamExtractorDefaultTest.java | 1 - .../YoutubeStreamExtractorLivestreamTest.java | 1 - .../YoutubeStreamExtractorRelatedMixTest.java | 1 - .../YoutubeStreamExtractorUnlistedTest.java | 1 - 14 files changed, 130 insertions(+), 147 deletions(-) diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseStreamExtractorTest.java index c058a21fb3..5d94777c91 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/BaseStreamExtractorTest.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.extractor.services; public interface BaseStreamExtractorTest extends BaseExtractorTest { - void testStreamType() throws Exception; void testUploaderName() throws Exception; void testUploaderUrl() throws Exception; void testUploaderAvatarUrl() throws Exception; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java index fbbc337b30..5ca143aec2 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java @@ -21,7 +21,6 @@ import org.schabi.newpipe.extractor.stream.Frameset; import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.streamdata.delivery.DASHManifestDeliveryData; import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData; import org.schabi.newpipe.extractor.streamdata.delivery.UrlBasedDeliveryData; @@ -194,12 +193,13 @@ public void testViewCount() throws Exception { public void testUploadDate() throws Exception { final DateWrapper dateWrapper = extractor().getUploadDate(); - if (expectedUploadDate() == null) { + final String expectedUploadDate = expectedUploadDate(); + if (expectedUploadDate == null) { assertNull(dateWrapper); } else { assertNotNull(dateWrapper); - final LocalDateTime expectedDateTime = LocalDateTime.parse(expectedUploadDate(), + final LocalDateTime expectedDateTime = LocalDateTime.parse(expectedUploadDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); final LocalDateTime actualDateTime = dateWrapper.offsetDateTime().toLocalDateTime(); diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java index 8c707156b9..761f09e8a9 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java @@ -13,7 +13,6 @@ import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper; import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import java.io.IOException; import java.util.Collections; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCOggTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCOggTest.java index ed8d2ef16e..00032cc5be 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCOggTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCOggTest.java @@ -5,8 +5,8 @@ import org.schabi.newpipe.downloader.DownloaderTestImpl; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCStreamExtractor; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamExtractor; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.schabi.newpipe.extractor.ServiceList.MediaCCC; @@ -34,7 +34,7 @@ public void getAudioStreamsCount() throws Exception { @Test public void getAudioStreamsContainOgg() throws Exception { for (AudioStream stream : extractor.getAudioStreams()) { - assertEquals("OGG", stream.getFormat().toString()); + assertEquals("ogg", stream.mediaFormat().name()); } } } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java index bd387d28d1..c986c1f66f 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java @@ -8,7 +8,6 @@ import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCStreamExtractor; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import javax.annotation.Nullable; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java index a33089dc03..b4608259b6 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java @@ -10,7 +10,6 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import java.io.IOException; import java.util.Arrays; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java index fb44ca3427..3c7e02415f 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java @@ -1,20 +1,23 @@ package org.schabi.newpipe.extractor.services.soundcloud; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.schabi.newpipe.downloader.DownloaderTestImpl; import org.schabi.newpipe.extractor.ExtractorAsserts; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.extractor.streamdata.delivery.ProgressiveHTTPDeliveryData; +import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; import java.util.Arrays; import java.util.Collections; @@ -22,9 +25,6 @@ import javax.annotation.Nullable; -import static org.junit.jupiter.api.Assertions.*; -import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; - public class SoundcloudStreamExtractorTest { private static final String SOUNDCLOUD = "https://soundcloud.com/"; @@ -186,29 +186,33 @@ public static void setUp() throws Exception { @Test public void testAudioStreams() throws Exception { super.testAudioStreams(); + final List audioStreams = extractor.getAudioStreams(); assertEquals(2, audioStreams.size()); - audioStreams.forEach(audioStream -> { - final DeliveryMethod deliveryMethod = audioStream.getDeliveryMethod(); - final String mediaUrl = audioStream.getContent(); - if (audioStream.getFormat() == MediaFormat.OPUS) { + + for (final AudioStream audioStream : audioStreams) { + assertTrue(audioStream.deliveryData() instanceof ProgressiveHTTPDeliveryData, + "Wrong delivery method for mediaFormat=" + audioStream.mediaFormat() + + " , avgBR=" + audioStream.averageBitrate() + + " , deliverDataType=" + audioStream.deliveryData().getClass() + ); + + final ProgressiveHTTPDeliveryData deliveryData = + (ProgressiveHTTPDeliveryData) audioStream.deliveryData(); + + final String mediaUrl = deliveryData.url(); + if (audioStream.mediaFormat() == AudioFormatRegistry.OPUS) { // Assert that it's an OPUS 64 kbps media URL with a single range which comes // from an HLS SoundCloud CDN ExtractorAsserts.assertContains("-hls-opus-media.sndcdn.com", mediaUrl); ExtractorAsserts.assertContains(".64.opus", mediaUrl); - assertSame(DeliveryMethod.HLS, deliveryMethod, - "Wrong delivery method for stream " + audioStream.getId() + ": " - + deliveryMethod); - } else if (audioStream.getFormat() == MediaFormat.MP3) { + } else if (audioStream.mediaFormat() == AudioFormatRegistry.MP3) { // Assert that it's a MP3 128 kbps media URL which comes from a progressive // SoundCloud CDN ExtractorAsserts.assertContains("-media.sndcdn.com/bKOA7Pwbut93.128.mp3", mediaUrl); - assertSame(DeliveryMethod.PROGRESSIVE_HTTP, deliveryMethod, - "Wrong delivery method for stream " + audioStream.getId() + ": " - + deliveryMethod); } - }); + } } } } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelLocalizationTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelLocalizationTest.java index 533ad7ee94..5c01bc5452 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelLocalizationTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelLocalizationTest.java @@ -1,12 +1,18 @@ package org.schabi.newpipe.extractor.services.youtube; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestRelatedItems; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.schabi.newpipe.downloader.DownloaderFactory; -import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelExtractor; import org.schabi.newpipe.extractor.localization.DateWrapper; @@ -15,129 +21,112 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; -import java.util.LinkedHashMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Stream; /** * A class that tests multiple channels and ranges of "time ago". */ -public class YoutubeChannelLocalizationTest { - private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/channel/"; - private static final boolean DEBUG = false; - private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - - @Test - public void testAllSupportedLocalizations() throws Exception { +@Disabled("There is currently only one localization supported for YT") +class YoutubeChannelLocalizationTest { + private static final String RESOURCE_PATH = + DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/channel/"; + private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + private static final List CHANNEL_URLS = Arrays.asList( + "https://www.youtube.com/user/NBCNews", + "https://www.youtube.com/channel/UCcmpeVbSSQlZRvHfdC-CRwg/videos", + "https://www.youtube.com/channel/UC65afEgL62PGFWXY7n6CUbA", + "https://www.youtube.com/channel/UCEOXxzW2vU0P-0THehuIIeg"); + + private static final Map> REFERENCES = new HashMap<>(); + + + @BeforeAll + static void setUp() throws Exception { YoutubeTestsUtils.ensureStateless(); NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "localization")); - testLocalizationsFor("https://www.youtube.com/user/NBCNews"); - testLocalizationsFor("https://www.youtube.com/channel/UCcmpeVbSSQlZRvHfdC-CRwg/videos"); - testLocalizationsFor("https://www.youtube.com/channel/UC65afEgL62PGFWXY7n6CUbA"); - testLocalizationsFor("https://www.youtube.com/channel/UCEOXxzW2vU0P-0THehuIIeg"); + for (final String url : CHANNEL_URLS) { + REFERENCES.put(url, getItemsPage(url, Localization.DEFAULT)); + } } - private void testLocalizationsFor(final String channelUrl) throws Exception { - - final List supportedLocalizations = YouTube.getSupportedLocalizations(); - // final List supportedLocalizations = Arrays.asList(Localization.DEFAULT, new Localization("sr")); - final Map> results = new LinkedHashMap<>(); - - for (Localization currentLocalization : supportedLocalizations) { - if (DEBUG) System.out.println("Testing localization = " + currentLocalization); - - ListExtractor.InfoItemsPage itemsPage; - try { - final ChannelExtractor extractor = YouTube.getChannelExtractor(channelUrl); - extractor.forceLocalization(currentLocalization); - extractor.fetchPage(); - itemsPage = defaultTestRelatedItems(extractor); - } catch (final Throwable e) { - System.out.println("[!] " + currentLocalization + " → failed"); - throw e; - } - - final List items = itemsPage.getItems(); - for (int i = 0; i < items.size(); i++) { - final StreamInfoItem item = items.get(i); - - String debugMessage = "[" + String.format("%02d", i) + "] " - + currentLocalization.getLocalizationCode() + " → " + item.getName() - + "\n:::: " + item.getStreamType() + ", views = " + item.getViewCount(); - final DateWrapper uploadDate = item.getUploadDate(); - if (uploadDate != null) { - String dateAsText = dateTimeFormatter.format(uploadDate.offsetDateTime()); - debugMessage += "\n:::: " + item.getTextualUploadDate() + - "\n:::: " + dateAsText; - } - if (DEBUG) System.out.println(debugMessage + "\n"); - } - results.put(currentLocalization, itemsPage.getItems()); - - if (DEBUG) System.out.println("\n===============================\n"); - } + static Stream provideDataForSupportedLocalizations() { + final List localizations = + new ArrayList<>(YouTube.getSupportedLocalizations()); + // Will already be checked in the references + localizations.remove(Localization.DEFAULT); + return CHANNEL_URLS.stream() + .flatMap(url -> localizations.stream().map(l -> Arguments.of(url, l))); + } - // Check results - final List referenceList = results.get(Localization.DEFAULT); - boolean someFail = false; - - for (Map.Entry> currentResultEntry : results.entrySet()) { - if (currentResultEntry.getKey().equals(Localization.DEFAULT)) { - continue; - } - - final String currentLocalizationCode = currentResultEntry.getKey().getLocalizationCode(); - final String referenceLocalizationCode = Localization.DEFAULT.getLocalizationCode(); - if (DEBUG) { - System.out.println("Comparing " + referenceLocalizationCode + " with " + - currentLocalizationCode); - } - - final List currentList = currentResultEntry.getValue(); - if (referenceList.size() != currentList.size()) { - if (DEBUG) System.out.println("[!] " + currentLocalizationCode + " → Lists are not equal"); - someFail = true; - continue; - } - - for (int i = 0; i < referenceList.size() - 1; i++) { - final StreamInfoItem referenceItem = referenceList.get(i); - final StreamInfoItem currentItem = currentList.get(i); - - final DateWrapper referenceUploadDate = referenceItem.getUploadDate(); - final DateWrapper currentUploadDate = currentItem.getUploadDate(); - - final String referenceDateString = referenceUploadDate == null ? "null" : - dateTimeFormatter.format(referenceUploadDate.offsetDateTime()); - final String currentDateString = currentUploadDate == null ? "null" : - dateTimeFormatter.format(currentUploadDate.offsetDateTime()); - - long difference = -1; - if (referenceUploadDate != null && currentUploadDate != null) { - difference = ChronoUnit.MILLIS.between(referenceUploadDate.offsetDateTime(), currentUploadDate.offsetDateTime()); - } - - final boolean areTimeEquals = difference < 5 * 60 * 1000L; - - if (!areTimeEquals) { - System.out.println("" + - " [!] " + currentLocalizationCode + " → [" + i + "] dates are not equal\n" + - " " + referenceLocalizationCode + ": " + - referenceDateString + " → " + referenceItem.getTextualUploadDate() + - "\n " + currentLocalizationCode + ": " + - currentDateString + " → " + currentItem.getTextualUploadDate()); - } - - } - } + @ParameterizedTest + @MethodSource("provideDataForSupportedLocalizations") + void testSupportedLocalizations( + final String channelUrl, + final Localization localization + ) throws Exception { + final List currentItems = getItemsPage(channelUrl, localization); + + final List refItems = REFERENCES.get(channelUrl); + + assertAll( + Stream.concat( + // Check if the lists match + Stream.of(() -> assertEquals( + refItems.size(), + currentItems.size(), + "Number of returned items doesn't match reference list")), + // Check all items + refItems.stream() + .map(refItem -> { + final StreamInfoItem curItem = + currentItems.get(refItems.indexOf(refItem)); + return checkItemAgainstReference(refItem, curItem); + }) + ) + ); + } - if (someFail) { - fail("Some localization failed"); - } else { - if (DEBUG) System.out.print("All tests passed" + - "\n\n===============================\n\n"); - } + private Executable checkItemAgainstReference( + final StreamInfoItem refItem, + final StreamInfoItem curItem + ) { + final DateWrapper refUploadDate = refItem.getUploadDate(); + final DateWrapper curUploadDate = curItem.getUploadDate(); + + final long difference = + refUploadDate == null || curUploadDate == null + ? -1 + : ChronoUnit.MINUTES.between( + refUploadDate.offsetDateTime(), + curUploadDate.offsetDateTime()); + return () -> assertTrue( + difference < 5, + () -> { + final String refDateStr = refUploadDate == null + ? "null" + : DTF.format(refUploadDate.offsetDateTime()); + final String curDateStr = curUploadDate == null + ? "null" + : DTF.format(curUploadDate.offsetDateTime()); + + return "Difference between reference '" + refDateStr + + "' and current '" + curDateStr + "' is too great"; + }); + } + + private static List getItemsPage( + final String channelUrl, + final Localization localization) throws Exception { + final ChannelExtractor extractor = YouTube.getChannelExtractor(channelUrl); + extractor.forceLocalization(localization); + extractor.fetchPage(); + return defaultTestRelatedItems(extractor).getItems(); } } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorAgeRestrictedTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorAgeRestrictedTest.java index 62315260e3..7a905a6705 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorAgeRestrictedTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorAgeRestrictedTest.java @@ -9,7 +9,6 @@ import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; import org.schabi.newpipe.extractor.services.youtube.YoutubeTestsUtils; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Collections; import java.util.List; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorControversialTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorControversialTest.java index 89c5b7f305..9ece3917a0 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorControversialTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorControversialTest.java @@ -10,7 +10,6 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeTestsUtils; import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Arrays; import java.util.List; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java index 82bbd8ae28..17683f4b74 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorDefaultTest.java @@ -46,7 +46,6 @@ import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamSegment; -import org.schabi.newpipe.extractor.stream.StreamType; import java.io.IOException; import java.net.MalformedURLException; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java index 7d5b0032b6..d84d078219 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java @@ -10,7 +10,6 @@ import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; import org.schabi.newpipe.extractor.services.youtube.YoutubeTestsUtils; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Arrays; import java.util.List; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorRelatedMixTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorRelatedMixTest.java index a33faed314..a1f56b1691 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorRelatedMixTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorRelatedMixTest.java @@ -20,7 +20,6 @@ import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; import org.schabi.newpipe.extractor.services.youtube.YoutubeTestsUtils; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Arrays; import java.util.List; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java index 5a5eb9d9e3..57df83f9a3 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorUnlistedTest.java @@ -11,7 +11,6 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeTestsUtils; import org.schabi.newpipe.extractor.stream.Privacy; import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.StreamType; import java.util.Arrays; import java.util.List; From 95581b24f21d524dc6b0bcafa3044bfc8f995534 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Thu, 16 Jun 2022 21:27:41 +0200 Subject: [PATCH 23/43] Started fixing YoutubeDashManifestCreatorsTest --- .../AbstractYoutubeDashManifestCreator.java | 4 +- ...YoutubeProgressiveDashManifestCreator.java | 1 + .../extractors/YoutubeStreamExtractor.java | 4 +- .../SimpleDASHManifestDeliveryDataImpl.java | 2 +- .../extractor/utils/ManifestCreatorCache.java | 255 ----------------- .../schabi/newpipe/extractor/utils/Pair.java | 13 +- .../YoutubeDashManifestCreatorsTest.java | 269 +++++++----------- .../utils/ManifestCreatorCacheTest.java | 74 ----- 8 files changed, 107 insertions(+), 515 deletions(-) delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java delete mode 100644 extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java index 3e4102befa..9bcedccd84 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java @@ -284,7 +284,7 @@ protected void generateRepresentationElement() { final String codec = itagInfo.getCodec(); if (isNullOrEmpty(codec)) { throw DashManifestCreationException.couldNotAddElement(ADAPTATION_SET, - "the codec value of the ItagItem is null or empty"); + "invalid codec=" + codec); } appendNewAttrWithValue( @@ -326,7 +326,7 @@ protected void generateRepresentationElement() { if (videoQualityData.fps() > 0) { appendNewAttrWithValue( - representationElement, "height", videoQualityData.fps()); + representationElement, "frameRate", videoQualityData.fps()); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java index 705c0ee235..20a6f82a15 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java @@ -34,6 +34,7 @@ public String generateManifest() { } generateDocumentAndCommonElements(streamDurationMs); + generateBaseUrlElement(itagInfo.getStreamUrl()); generateSegmentBaseElement(); generateInitializationElement(); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java index bae738397d..a01a54489f 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java @@ -846,7 +846,7 @@ public void onFetchPage(@Nonnull final Downloader downloader) .getBytes(StandardCharsets.UTF_8); nextResponse = getJsonPostResponse(NEXT, body, localization); - if ((!isAgeRestricted && !isLive && !isPostLive()) + if ((!isAgeRestricted && !isLive && !isPostLive) || isAndroidClientFetchForced) { try { fetchAndroidMobileJsonPlayer(contentCountry, localization, videoId); @@ -1368,7 +1368,7 @@ private > ItagInfo buildItagInfo( itagInfo.setFps(getNullableInteger(formatData, "fps")); } - itagInfo.setBitRate(getNullableInteger(formatData, "bitRate")); + itagInfo.setBitRate(getNullableInteger(formatData, "bitrate")); itagInfo.setQuality(formatData.getString("quality")); final String mimeType = formatData.getString("mimeType", ""); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java index 391634399e..54007e43af 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java @@ -39,7 +39,7 @@ public DashManifestCreator getDashManifestCreator() { @Override public String getCachedDashManifestAsString() { - if (cachedDashManifest != null) { + if (cachedDashManifest == null) { cachedDashManifest = getDashManifestCreator().generateManifest(); } return cachedDashManifest; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java deleted file mode 100644 index ac12f83f95..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java +++ /dev/null @@ -1,255 +0,0 @@ -package org.schabi.newpipe.extractor.utils; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * A {@link Serializable serializable} cache class used by the extractor to cache manifests - * generated with extractor's manifests generators. - * - *

- * It relies internally on a {@link ConcurrentHashMap} to allow concurrent access to the cache. - *

- * - * @param the type of cache keys, which must be {@link Serializable serializable} - * @param the type of the second element of {@link Pair pairs} used as values of the cache, - * which must be {@link Serializable serializable} - */ -public final class ManifestCreatorCache - implements Serializable { - - /** - * The default maximum size of a manifest cache. - */ - public static final int DEFAULT_MAXIMUM_SIZE = Integer.MAX_VALUE; - - /** - * The default clear factor of a manifest cache. - */ - public static final double DEFAULT_CLEAR_FACTOR = 0.75; - - /** - * The {@link ConcurrentHashMap} used internally as the cache of manifests. - */ - private final ConcurrentHashMap> concurrentHashMap; - - /** - * The maximum size of the cache. - * - *

- * The default value is {@link #DEFAULT_MAXIMUM_SIZE}. - *

- */ - private int maximumSize = DEFAULT_MAXIMUM_SIZE; - - /** - * The clear factor of the cache, which is a double between {@code 0} and {@code 1} excluded. - * - *

- * The default value is {@link #DEFAULT_CLEAR_FACTOR}. - *

- */ - private double clearFactor = DEFAULT_CLEAR_FACTOR; - - /** - * Creates a new {@link ManifestCreatorCache}. - */ - public ManifestCreatorCache() { - concurrentHashMap = new ConcurrentHashMap<>(); - } - - /** - * Tests if the specified key is in the cache. - * - * @param key the key to test its presence in the cache - * @return {@code true} if the key is in the cache, {@code false} otherwise. - */ - public boolean containsKey(final K key) { - return concurrentHashMap.containsKey(key); - } - - /** - * Returns the value to which the specified key is mapped, or {@code null} if the cache - * contains no mapping for the key. - * - * @param key the key to which getting its value - * @return the value to which the specified key is mapped, or {@code null} - */ - @Nullable - public Pair get(final K key) { - return concurrentHashMap.get(key); - } - - /** - * Adds a new element to the cache. - * - *

- * If the cache limit is reached, oldest elements will be cleared first using the load factor - * and the maximum size. - *

- * - * @param key the key to put - * @param value the value to associate to the key - * - * @return the previous value associated with the key, or {@code null} if there was no mapping - * for the key (note that a null return can also indicate that the cache previously associated - * {@code null} with the key). - */ - @Nullable - public V put(final K key, final V value) { - if (!concurrentHashMap.containsKey(key) && concurrentHashMap.size() == maximumSize) { - final int newCacheSize = (int) Math.round(maximumSize * clearFactor); - keepNewestEntries(newCacheSize != 0 ? newCacheSize : 1); - } - - final Pair returnValue = concurrentHashMap.put(key, - new Pair<>(concurrentHashMap.size(), value)); - return returnValue == null ? null : returnValue.getSecond(); - } - - /** - * Clears the cached manifests. - * - *

- * The cache will be empty after this method is called. - *

- */ - public void clear() { - concurrentHashMap.clear(); - } - - /** - * Resets the cache. - * - *

- * The cache will be empty and the clear factor and the maximum size will be reset to their - * default values. - *

- * - * @see #clear() - * @see #resetClearFactor() - * @see #resetMaximumSize() - */ - public void reset() { - clear(); - resetClearFactor(); - resetMaximumSize(); - } - - /** - * @return the number of cached manifests in the cache - */ - public int size() { - return concurrentHashMap.size(); - } - - /** - * @return the maximum size of the cache - */ - public long getMaximumSize() { - return maximumSize; - } - - /** - * Sets the maximum size of the cache. - * - * If the current cache size is more than the new maximum size, the percentage of one less the - * clear factor of the maximum new size of manifests in the cache will be removed. - * - * @param maximumSize the new maximum size of the cache - * @throws IllegalArgumentException if {@code maximumSize} is less than or equal to 0 - */ - public void setMaximumSize(final int maximumSize) { - if (maximumSize <= 0) { - throw new IllegalArgumentException("Invalid maximum size"); - } - - if (maximumSize < this.maximumSize && !concurrentHashMap.isEmpty()) { - final int newCacheSize = (int) Math.round(maximumSize * clearFactor); - keepNewestEntries(newCacheSize != 0 ? newCacheSize : 1); - } - - this.maximumSize = maximumSize; - } - - /** - * Resets the maximum size of the cache to its {@link #DEFAULT_MAXIMUM_SIZE default value}. - */ - public void resetMaximumSize() { - this.maximumSize = DEFAULT_MAXIMUM_SIZE; - } - - /** - * @return the current clear factor of the cache, used when the cache limit size is reached - */ - public double getClearFactor() { - return clearFactor; - } - - /** - * Sets the clear factor of the cache, used when the cache limit size is reached. - * - *

- * The clear factor must be a double between {@code 0} excluded and {@code 1} excluded. - *

- * - *

- * Note that it will be only used the next time the cache size limit is reached. - *

- * - * @param clearFactor the new clear factor of the cache - * @throws IllegalArgumentException if the clear factor passed a parameter is invalid - */ - public void setClearFactor(final double clearFactor) { - if (clearFactor <= 0 || clearFactor >= 1) { - throw new IllegalArgumentException("Invalid clear factor"); - } - - this.clearFactor = clearFactor; - } - - /** - * Resets the clear factor to its {@link #DEFAULT_CLEAR_FACTOR default value}. - */ - public void resetClearFactor() { - this.clearFactor = DEFAULT_CLEAR_FACTOR; - } - - @Nonnull - @Override - public String toString() { - return "ManifestCreatorCache[clearFactor=" + clearFactor + ", maximumSize=" + maximumSize - + ", concurrentHashMap=" + concurrentHashMap + "]"; - } - - /** - * Keeps only the newest entries in a cache. - * - *

- * This method will first collect the entries to remove by looping through the concurrent hash - * map - *

- * - * @param newLimit the new limit of the cache - */ - private void keepNewestEntries(final int newLimit) { - final int difference = concurrentHashMap.size() - newLimit; - final ArrayList>> entriesToRemove = new ArrayList<>(); - - concurrentHashMap.entrySet().forEach(entry -> { - final Pair value = entry.getValue(); - if (value.getFirst() < difference) { - entriesToRemove.add(entry); - } else { - value.setFirst(value.getFirst() - difference); - } - }); - - entriesToRemove.forEach(entry -> concurrentHashMap.remove(entry.getKey(), - entry.getValue())); - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Pair.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Pair.java index 6efb66b209..43da033ec5 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Pair.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Pair.java @@ -1,25 +1,20 @@ package org.schabi.newpipe.extractor.utils; -import java.io.Serializable; import java.util.Objects; /** - * Serializable class to create a pair of objects. + * class to create a pair of objects. * - *

- * The two objects of the pair must be {@link Serializable serializable} and can be of the same - * type. - *

* *

* Note that this class is not intended to be used as a general-purpose pair and should only be * used when interfacing with the extractor. *

* - * @param the type of the first object, which must be {@link Serializable} - * @param the type of the second object, which must be {@link Serializable} + * @param the type of the first object + * @param the type of the second object */ -public class Pair implements Serializable { +public class Pair { /** * The first object of the pair. diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java index 9a396a7f93..bf00d737a0 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java @@ -4,49 +4,52 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreater; -import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreaterOrEqual; import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsValidUrl; import static org.schabi.newpipe.extractor.ExtractorAsserts.assertNotBlank; import static org.schabi.newpipe.extractor.ServiceList.YouTube; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.ADAPTATION_SET; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.AUDIO_CHANNEL_CONFIGURATION; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.BASE_URL; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.INITIALIZATION; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.MPD; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.PERIOD; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.REPRESENTATION; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.ROLE; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.SEGMENT_BASE; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.SEGMENT_TEMPLATE; -import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeDashManifestCreatorsUtils.SEGMENT_TIMELINE; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.ADAPTATION_SET; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.AUDIO_CHANNEL_CONFIGURATION; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.BASE_URL; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.INITIALIZATION; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.MPD; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.PERIOD; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.REPRESENTATION; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.ROLE; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_BASE; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TEMPLATE; +import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TIMELINE; import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.schabi.newpipe.downloader.DownloaderTestImpl; +import org.junit.jupiter.api.function.Executable; +import org.schabi.newpipe.downloader.DownloaderFactory; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeOtfDashManifestCreator; import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeProgressiveDashManifestCreator; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; -import org.schabi.newpipe.extractor.stream.DeliveryMethod; -import org.schabi.newpipe.extractor.stream.Stream; -import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException; +import org.schabi.newpipe.extractor.streamdata.delivery.DASHManifestDeliveryData; +import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreator; +import org.schabi.newpipe.extractor.streamdata.stream.BaseAudioStream; +import org.schabi.newpipe.extractor.streamdata.stream.Stream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoStream; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; -import java.util.Random; -import java.util.function.Consumer; -import java.util.stream.Collectors; import java.util.stream.IntStream; import javax.annotation.Nonnull; +import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -83,124 +86,89 @@ class YoutubeDashManifestCreatorsTest { // Setting a higher number may let Google video servers return 403s private static final int MAX_STREAMS_TO_TEST_PER_METHOD = 3; private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM"; + + private static final String RESOURCE_PATH = + DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/dashmanifest/"; + private static YoutubeStreamExtractor extractor; - private static long videoLength; @BeforeAll public static void setUp() throws Exception { - YoutubeParsingHelper.resetClientVersionAndKey(); - YoutubeParsingHelper.setNumberGenerator(new Random(1)); - NewPipe.init(DownloaderTestImpl.getInstance()); + YoutubeTestsUtils.ensureStateless(); + NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH)); extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(url); extractor.fetchPage(); - videoLength = extractor.getLength(); } @Test - void testOtfStreams() throws Exception { - assertDashStreams(extractor.getVideoOnlyStreams()); - assertDashStreams(extractor.getAudioStreams()); - - // no video stream with audio uses the DASH delivery method (YouTube OTF stream type) - assertEquals(0, assertFilterStreams(extractor.getVideoStreams(), - DeliveryMethod.DASH).size()); + void testVideoOnlyStreams() throws ExtractionException { + checkStreams(extractor.getVideoOnlyStreams()); } @Test - void testProgressiveStreams() throws Exception { - assertProgressiveStreams(extractor.getVideoOnlyStreams()); - assertProgressiveStreams(extractor.getAudioStreams()); - - // we are not able to generate DASH manifests of video formats with audio - assertThrows(DashManifestCreationException.class, - () -> assertProgressiveStreams(extractor.getVideoStreams())); + void testVideoStreams() throws ExtractionException { + checkStreams(extractor.getVideoStreams()); } - private void assertDashStreams(final List streams) throws Exception { - - for (final Stream stream : assertFilterStreams(streams, DeliveryMethod.DASH)) { - //noinspection ConstantConditions - final String manifest = YoutubeOtfDashManifestCreator.fromOtfStreamingUrl( - stream.getContent(), stream.getItagItem(), videoLength); - assertNotBlank(manifest); - - assertManifestGenerated( - manifest, - stream.getItagItem(), - document -> assertAll( - () -> assertSegmentTemplateElement(document), - () -> assertSegmentTimelineAndSElements(document) - ) - ); - } - } - - private void assertProgressiveStreams(final List streams) throws Exception { - - for (final Stream stream : assertFilterStreams(streams, DeliveryMethod.PROGRESSIVE_HTTP)) { - //noinspection ConstantConditions - final String manifest = - YoutubeProgressiveDashManifestCreator.fromProgressiveStreamingUrl( - stream.getContent(), stream.getItagItem(), videoLength); - assertNotBlank(manifest); - - assertManifestGenerated( - manifest, - stream.getItagItem(), - document -> assertAll( - () -> assertBaseUrlElement(document), - () -> assertSegmentBaseElement(document, stream.getItagItem()), - () -> assertInitializationElement(document, stream.getItagItem()) - ) - ); - } + @Test + void testAudioStreams() throws ExtractionException { + checkStreams(extractor.getAudioStreams()); } - @Nonnull - private List assertFilterStreams( - @Nonnull final List streams, - final DeliveryMethod deliveryMethod) { - - final List filteredStreams = streams.stream() - .filter(stream -> stream.getDeliveryMethod() == deliveryMethod) + private > void checkStreams(final Collection streams) { + assertAll(streams.stream() + .filter(s -> s.deliveryData() instanceof DASHManifestDeliveryData) .limit(MAX_STREAMS_TO_TEST_PER_METHOD) - .collect(Collectors.toList()); - - assertAll(filteredStreams.stream() - .flatMap(stream -> java.util.stream.Stream.of( - () -> assertNotBlank(stream.getContent()), - () -> assertNotNull(stream.getItagItem()) - )) + .map(s -> + () -> checkManifest(s, + ((DASHManifestDeliveryData) s.deliveryData()) + .getCachedDashManifestAsString())) ); - - return filteredStreams; } - private void assertManifestGenerated(final String dashManifest, - final ItagItem itagItem, - final Consumer additionalAsserts) - throws Exception { + private void checkManifest( + final Stream stream, + final String manifestAsString) throws Exception { + assertNotBlank(manifestAsString, "Generated manifest string is blank"); + checkGeneratedManifest(manifestAsString, stream); + } - final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory - .newInstance(); + private void checkGeneratedManifest( + final String dashManifest, + final Stream stream + ) throws Exception { + final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); - final Document document = documentBuilder.parse(new InputSource( - new StringReader(dashManifest))); + final Document document = + documentBuilder.parse(new InputSource(new StringReader(dashManifest))); - assertAll( + final List asserts = new ArrayList<>(Arrays.asList( () -> assertMpdElement(document), () -> assertPeriodElement(document), - () -> assertAdaptationSetElement(document, itagItem), + () -> assertAdaptationSetElement(document), () -> assertRoleElement(document), - () -> assertRepresentationElement(document, itagItem), - () -> { - if (itagItem.itagType.equals(ItagItem.ItagType.AUDIO)) { - assertAudioChannelConfigurationElement(document, itagItem); - } - }, - () -> additionalAsserts.accept(document) - ); + () -> assertRepresentationElement(document, stream) + )); + + if (stream instanceof BaseAudioStream) { + asserts.add(() -> assertAudioChannelConfigurationElement(document)); + } + + final DashManifestCreator dashManifestCreator = + ((DASHManifestDeliveryData) stream.deliveryData()).getDashManifestCreator(); + if (dashManifestCreator instanceof YoutubeOtfDashManifestCreator) { + asserts.add(() -> assertSegmentTemplateElement(document)); + asserts.add(() -> assertSegmentTimelineAndSElements(document)); + } else if (dashManifestCreator instanceof YoutubeProgressiveDashManifestCreator) { + asserts.add(() -> assertBaseUrlElement(document)); + asserts.add(() -> assertSegmentBaseElement(document)); + asserts.add(() -> assertInitializationElement(document)); + } + + assertAll(asserts); } private void assertMpdElement(@Nonnull final Document document) { @@ -217,10 +185,9 @@ private void assertPeriodElement(@Nonnull final Document document) { assertGetElement(document, PERIOD, MPD); } - private void assertAdaptationSetElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { + private void assertAdaptationSetElement(@Nonnull final Document document) { final Element element = assertGetElement(document, ADAPTATION_SET, PERIOD); - assertAttrEquals(itagItem.getMediaFormat().getMimeType(), element, "mimeType"); + assertAttrNotBlank(element, "mimeType"); } private void assertRoleElement(@Nonnull final Document document) { @@ -228,27 +195,24 @@ private void assertRoleElement(@Nonnull final Document document) { } private void assertRepresentationElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { + @Nonnull final Stream stream) { final Element element = assertGetElement(document, REPRESENTATION, ADAPTATION_SET); - assertAttrEquals(itagItem.getBitrate(), element, "bandwidth"); - assertAttrEquals(itagItem.getCodec(), element, "codecs"); + assertAttrNotBlank(element, "bandwidth"); + assertAttrNotBlank(element, "codecs"); - if (itagItem.itagType == ItagItem.ItagType.VIDEO_ONLY - || itagItem.itagType == ItagItem.ItagType.VIDEO) { - assertAttrEquals(itagItem.getFps(), element, "frameRate"); - assertAttrEquals(itagItem.getHeight(), element, "height"); - assertAttrEquals(itagItem.getWidth(), element, "width"); + if (stream instanceof VideoStream) { + assertAttrNotBlank(element, "frameRate"); + assertAttrNotBlank(element, "height"); + assertAttrNotBlank(element, "width"); } - assertAttrEquals(itagItem.id, element, "id"); } - private void assertAudioChannelConfigurationElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { + private void assertAudioChannelConfigurationElement(@Nonnull final Document document) { final Element element = assertGetElement(document, AUDIO_CHANNEL_CONFIGURATION, REPRESENTATION); - assertAttrEquals(itagItem.getAudioChannels(), element, "value"); + assertAttrNotBlank(element, "value"); } private void assertSegmentTemplateElement(@Nonnull final Document document) { @@ -294,59 +258,20 @@ private void assertBaseUrlElement(@Nonnull final Document document) { assertIsValidUrl(element.getTextContent()); } - private void assertSegmentBaseElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { + private void assertSegmentBaseElement(@Nonnull final Document document) { final Element element = assertGetElement(document, SEGMENT_BASE, REPRESENTATION); - assertRangeEquals(itagItem.getIndexStart(), itagItem.getIndexEnd(), element, "indexRange"); + assertAttrNotBlank(element, "indexRange"); } - private void assertInitializationElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { + private void assertInitializationElement(@Nonnull final Document document) { final Element element = assertGetElement(document, INITIALIZATION, SEGMENT_BASE); - assertRangeEquals(itagItem.getInitStart(), itagItem.getInitEnd(), element, "range"); - } - - - private void assertAttrEquals(final int expected, - @Nonnull final Element element, - final String attribute) { - - final int actual = Integer.parseInt(element.getAttribute(attribute)); - assertAll( - () -> assertGreater(0, actual), - () -> assertEquals(expected, actual) - ); + assertAttrNotBlank(element, "range"); } - private void assertAttrEquals(final String expected, - @Nonnull final Element element, - final String attribute) { - final String actual = element.getAttribute(attribute); - assertAll( - () -> assertNotBlank(actual), - () -> assertEquals(expected, actual) - ); + private void assertAttrNotBlank(@Nonnull final Element element, final String attribute) { + assertNotBlank(element.getAttribute(attribute), "Attribute '" + attribute + "' is blank"); } - private void assertRangeEquals(final int expectedStart, - final int expectedEnd, - @Nonnull final Element element, - final String attribute) { - final String range = element.getAttribute(attribute); - assertNotBlank(range); - final String[] rangeParts = range.split("-"); - assertEquals(2, rangeParts.length); - - final int actualStart = Integer.parseInt(rangeParts[0]); - final int actualEnd = Integer.parseInt(rangeParts[1]); - - assertAll( - () -> assertGreaterOrEqual(0, actualStart), - () -> assertEquals(expectedStart, actualStart), - () -> assertGreater(0, actualEnd), - () -> assertEquals(expectedEnd, actualEnd) - ); - } @Nonnull private Element assertGetElement(@Nonnull final Document document, @@ -354,7 +279,7 @@ private Element assertGetElement(@Nonnull final Document document, final String expectedParentTagName) { final Element element = (Element) document.getElementsByTagName(tagName).item(0); - assertNotNull(element); + assertNotNull(element, "Could not get first element with tagName=" + tagName); assertTrue(element.getParentNode().isEqualNode( document.getElementsByTagName(expectedParentTagName).item(0)), "Element with tag name \"" + tagName + "\" does not have a parent node" diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java deleted file mode 100644 index 83c5c1dfb1..0000000000 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.schabi.newpipe.extractor.utils; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class ManifestCreatorCacheTest { - @Test - void basicMaximumSizeAndResetTest() { - final ManifestCreatorCache cache = new ManifestCreatorCache<>(); - - // 30 elements set -> cache resized to 23 -> 5 new elements set to the cache -> 28 - cache.setMaximumSize(30); - setCacheContent(cache); - assertEquals(28, cache.size(), - "Wrong cache size with default clear factor and 30 as the maximum size"); - cache.reset(); - - assertEquals(0, cache.size(), - "The cache has been not cleared after a reset call (wrong cache size)"); - assertEquals(ManifestCreatorCache.DEFAULT_MAXIMUM_SIZE, cache.getMaximumSize(), - "Wrong maximum size after cache reset"); - assertEquals(ManifestCreatorCache.DEFAULT_CLEAR_FACTOR, cache.getClearFactor(), - "Wrong clear factor after cache reset"); - } - - @Test - void maximumSizeAndClearFactorSettersAndResettersTest() { - final ManifestCreatorCache cache = new ManifestCreatorCache<>(); - cache.setMaximumSize(20); - cache.setClearFactor(0.5); - - setCacheContent(cache); - // 30 elements set -> cache resized to 10 -> 5 new elements set to the cache -> 15 - assertEquals(15, cache.size(), - "Wrong cache size with 0.5 as the clear factor and 20 as the maximum size"); - - // Clear factor and maximum size getters tests - assertEquals(0.5, cache.getClearFactor(), - "Wrong clear factor gotten from clear factor getter"); - assertEquals(20, cache.getMaximumSize(), - "Wrong maximum cache size gotten from maximum size getter"); - - // Resetters tests - cache.resetMaximumSize(); - assertEquals(ManifestCreatorCache.DEFAULT_MAXIMUM_SIZE, cache.getMaximumSize(), - "Wrong maximum cache size gotten from maximum size getter after maximum size " - + "resetter call"); - - cache.resetClearFactor(); - assertEquals(ManifestCreatorCache.DEFAULT_CLEAR_FACTOR, cache.getClearFactor(), - "Wrong clear factor gotten from clear factor getter after clear factor resetter " - + "call"); - } - - /** - * Adds sample strings to the provided manifest creator cache, in order to test clear factor and - * maximum size. - * @param cache the cache to fill with some data - */ - private static void setCacheContent(final ManifestCreatorCache cache) { - int i = 0; - while (i < 26) { - cache.put(String.valueOf((char) ('a' + i)), "V"); - ++i; - } - - i = 0; - while (i < 9) { - cache.put("a" + (char) ('a' + i), "V"); - ++i; - } - } -} From 6f45e5cc1495bdd03612d2538ec1402f3c3557cb Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 18 Jun 2022 21:51:22 +0200 Subject: [PATCH 24/43] Fixed YoutubeDashManifestCreatorsTest --- ...YoutubeProgressiveDashManifestCreator.java | 6 ++-- .../YoutubeDashManifestCreatorsTest.java | 32 ++++++++++++++----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java index 20a6f82a15..5e4a5d074f 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java @@ -57,9 +57,11 @@ public String generateManifest() { protected void generateBaseUrlElement(@Nonnull final String baseUrl) throws DashManifestCreationException { try { - final Element representationElement = getFirstElementByName(REPRESENTATION); + final Element baseURLElement = createElement(BASE_URL); - appendNewAttrWithValue(representationElement, BASE_URL, baseUrl); + baseURLElement.setTextContent(baseUrl); + + getFirstElementByName(REPRESENTATION).appendChild(baseURLElement); } catch (final DOMException e) { throw DashManifestCreationException.couldNotAddElement(BASE_URL, e); } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java index bf00d737a0..99e5441a57 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java @@ -33,9 +33,12 @@ import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.streamdata.delivery.DASHManifestDeliveryData; import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreator; +import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; import org.schabi.newpipe.extractor.streamdata.stream.BaseAudioStream; import org.schabi.newpipe.extractor.streamdata.stream.Stream; +import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; import org.schabi.newpipe.extractor.streamdata.stream.VideoStream; +import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -45,7 +48,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import java.util.stream.IntStream; import javax.annotation.Nonnull; @@ -84,8 +90,8 @@ */ class YoutubeDashManifestCreatorsTest { // Setting a higher number may let Google video servers return 403s - private static final int MAX_STREAMS_TO_TEST_PER_METHOD = 3; - private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM"; + private static final int MAX_STREAMS_TO_TEST_PER_METHOD = 5; + private static final String URL = "https://www.youtube.com/watch?v=DJ8GQUNUXGM"; private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/dashmanifest/"; @@ -97,28 +103,38 @@ public static void setUp() throws Exception { YoutubeTestsUtils.ensureStateless(); NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH)); - extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(url); + extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(URL); extractor.fetchPage(); } @Test void testVideoOnlyStreams() throws ExtractionException { - checkStreams(extractor.getVideoOnlyStreams()); + final List videoStreams = getDashStreams(extractor.getVideoOnlyStreams()); + assertTrue(videoStreams.size() > 0); + checkDashStreams(videoStreams); } @Test void testVideoStreams() throws ExtractionException { - checkStreams(extractor.getVideoStreams()); + List videoAudioStreams = getDashStreams(extractor.getVideoStreams()); + assertEquals(0, videoAudioStreams.size(), "There should be no dash streams for video-audio streams"); } @Test void testAudioStreams() throws ExtractionException { - checkStreams(extractor.getAudioStreams()); + final List audioStreams = getDashStreams(extractor.getAudioStreams()); + assertTrue(audioStreams.size() > 0); + checkDashStreams(audioStreams); } - private > void checkStreams(final Collection streams) { - assertAll(streams.stream() + private > List getDashStreams(final List streams) { + return streams.stream() .filter(s -> s.deliveryData() instanceof DASHManifestDeliveryData) + .collect(Collectors.toList()); + } + + private > void checkDashStreams(final Collection streams) { + assertAll(streams.stream() .limit(MAX_STREAMS_TO_TEST_PER_METHOD) .map(s -> () -> checkManifest(s, From 7f9e895f998a52035ebcabc4f97c55f0686dc608 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 18 Jun 2022 22:07:08 +0200 Subject: [PATCH 25/43] Fixed more tests --- .../peertube/extractors/PeertubeStreamExtractor.java | 1 - .../bandcamp/BandcampRadioStreamExtractorTest.java | 1 + .../services/bandcamp/BandcampStreamExtractorTest.java | 5 +++++ .../media_ccc/MediaCCCStreamExtractorTest.java | 10 ++++++---- .../services/peertube/PeertubeStreamExtractorTest.java | 1 + .../soundcloud/SoundcloudStreamExtractorTest.java | 3 +++ .../stream/YoutubeStreamExtractorLivestreamTest.java | 1 + 7 files changed, 17 insertions(+), 5 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index ba52268677..18d5c834f8 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -64,7 +64,6 @@ public class PeertubeStreamExtractor extends StreamExtractor { private static final String FILE_DOWNLOAD_URL = "fileDownloadUrl"; private static final String FILE_URL = "fileUrl"; private static final String PLAYLIST_URL = "playlistUrl"; - private static final String RESOLUTION_ID = "resolution.id"; private static final String STREAMING_PLAYLISTS = "streamingPlaylists"; private final String baseUrl; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampRadioStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampRadioStreamExtractorTest.java index 76976b02b5..fbd921de0a 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampRadioStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampRadioStreamExtractorTest.java @@ -49,6 +49,7 @@ public void testGettingCorrectStreamExtractor() throws ExtractionException { @Override public String expectedId() throws Exception { return "230"; } @Override public String expectedUrlContains() throws Exception { return URL; } @Override public String expectedOriginalUrlContains() throws Exception { return URL; } + @Override public boolean expectedHasVideoOnlyStreams() { return false; } @Override public boolean expectedHasVideoAndAudioStreams() { return false; } @Override public boolean expectedHasSubtitles() { return false; } @Override public boolean expectedHasFrames() { return false; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java index 761f09e8a9..94ef94e48c 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/bandcamp/BandcampStreamExtractorTest.java @@ -118,6 +118,11 @@ public long expectedDislikeCountAtLeast() { return Long.MIN_VALUE; } + @Override + public boolean expectedHasVideoOnlyStreams() { + return false; + } + @Override public boolean expectedHasVideoAndAudioStreams() { return false; diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java index c986c1f66f..eb7e6fc263 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCStreamExtractorTest.java @@ -52,6 +52,7 @@ public static void setUp() throws Exception { @Override public long expectedLikeCountAtLeast() { return -1; } @Override public long expectedDislikeCountAtLeast() { return -1; } @Override public boolean expectedHasRelatedItems() { return false; } + @Override public boolean expectedHasVideoOnlyStreams() { return false; } @Override public boolean expectedHasSubtitles() { return false; } @Override public boolean expectedHasFrames() { return false; } @Override public List expectedTags() { return Arrays.asList("gpn18", "105"); } @@ -74,8 +75,8 @@ public void testUploaderAvatarUrl() throws Exception { @Override @Test - public void testVideoOnlyStreams() throws Exception { - super.testVideoOnlyStreams(); + public void testVideoAudioStreams() throws Exception { + super.testVideoAudioStreams(); assertEquals(4, extractor.getVideoStreams().size()); } @@ -123,6 +124,7 @@ public static void setUp() throws Exception { @Override public long expectedLikeCountAtLeast() { return -1; } @Override public long expectedDislikeCountAtLeast() { return -1; } @Override public boolean expectedHasRelatedItems() { return false; } + @Override public boolean expectedHasVideoOnlyStreams() { return false; } @Override public boolean expectedHasSubtitles() { return false; } @Override public boolean expectedHasFrames() { return false; } @Override public List expectedTags() { return Arrays.asList("36c3", "10565", "2019", "Security", "Main"); } @@ -143,8 +145,8 @@ public void testUploaderAvatarUrl() throws Exception { @Override @Test - public void testVideoOnlyStreams() throws Exception { - super.testVideoOnlyStreams(); + public void testVideoAudioStreams() throws Exception { + super.testVideoAudioStreams(); assertEquals(8, extractor.getVideoStreams().size()); } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java index b4608259b6..39578f6597 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/peertube/PeertubeStreamExtractorTest.java @@ -24,6 +24,7 @@ public abstract class PeertubeStreamExtractorTest extends DefaultStreamExtractorTest { private static final String BASE_URL = "/videos/watch/"; + @Override public boolean expectedHasVideoOnlyStreams() { return false; } @Override public boolean expectedHasAudioStreams() { return false; } @Override public boolean expectedHasFrames() { return false; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java index 3c7e02415f..8c7f2f0961 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java @@ -67,6 +67,7 @@ public static void setUp() throws Exception { @Override public long expectedLikeCountAtLeast() { return -1; } @Override public long expectedDislikeCountAtLeast() { return -1; } @Override public boolean expectedHasAudioStreams() { return false; } + @Override public boolean expectedHasVideoOnlyStreams() { return false; } @Override public boolean expectedHasVideoAndAudioStreams() { return false; } @Override public boolean expectedHasSubtitles() { return false; } @Override public boolean expectedHasFrames() { return false; } @@ -130,6 +131,7 @@ public void testRelatedItems() throws Exception { @Override public long expectedLikeCountAtLeast() { return -1; } @Override public long expectedDislikeCountAtLeast() { return -1; } @Override public boolean expectedHasAudioStreams() { return false; } + @Override public boolean expectedHasVideoOnlyStreams() { return false; } @Override public boolean expectedHasVideoAndAudioStreams() { return false; } @Override public boolean expectedHasRelatedItems() { return true; } @Override public boolean expectedHasSubtitles() { return false; } @@ -172,6 +174,7 @@ public static void setUp() throws Exception { @Nullable @Override public String expectedTextualUploadDate() { return "2019-03-28 13:36:18"; } @Override public long expectedLikeCountAtLeast() { return -1; } @Override public long expectedDislikeCountAtLeast() { return -1; } + @Override public boolean expectedHasVideoOnlyStreams() { return false; } @Override public boolean expectedHasVideoAndAudioStreams() { return false; } @Override public boolean expectedHasSubtitles() { return false; } @Override public boolean expectedHasFrames() { return false; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java index d84d078219..ac5446f2b6 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/stream/YoutubeStreamExtractorLivestreamTest.java @@ -60,6 +60,7 @@ public void testUploaderName() throws Exception { @Nullable @Override public String expectedTextualUploadDate() { return "2022-07-12"; } @Override public long expectedLikeCountAtLeast() { return 340_000; } @Override public long expectedDislikeCountAtLeast() { return -1; } + @Override public boolean expectedHasVideoAndAudioStreams() { return false; } @Override public boolean expectedHasSubtitles() { return false; } @Nullable @Override public String expectedDashMpdUrlContains() { return "https://manifest.googlevideo.com/api/manifest/dash/"; } @Override public boolean expectedHasFrames() { return false; } From 34d11affcf17b98d5ea1748758877ddb18eebf65 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 18 Jun 2022 22:13:03 +0200 Subject: [PATCH 26/43] Made code non-nullable (MediaFormatRegistry) --- .../MediaCCCLiveStreamExtractor.java | 4 ++-- .../extractors/MediaCCCStreamExtractor.java | 5 +++-- .../extractors/PeertubeStreamExtractor.java | 6 ++--- .../extractors/SoundcloudStreamExtractor.java | 2 +- .../format/registry/MediaFormatRegistry.java | 22 ++++++++++++------- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index abd63eeaac..9a76177430 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -174,7 +174,7 @@ public List getAudioStreams() throws IOException, ExtractionExcepti return new SimpleAudioStreamImpl( // TODO: This looks wrong - new AudioFormatRegistry().getFromSuffix(dto.getUrlKey()), + new AudioFormatRegistry().getFromSuffixOrThrow(dto.getUrlKey()), deliveryData ); }) @@ -199,7 +199,7 @@ public List getVideoStreams() throws IOException, ExtractionEx return new SimpleVideoAudioStreamImpl( // TODO: This looks wrong - new VideoAudioFormatRegistry().getFromSuffix(dto.getUrlKey()), + new VideoAudioFormatRegistry().getFromSuffixOrThrow(dto.getUrlKey()), deliveryData, VideoQualityData.fromHeightWidth( /*height=*/videoSize.getInt(1, VideoQualityData.UNKNOWN), diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index b70957c546..9c99ac1191 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -98,7 +98,7 @@ public String getUploaderAvatarUrl() { public List getAudioStreams() throws ExtractionException { return getRecordingsByMimeType("audio") .map(o -> new SimpleAudioStreamImpl( - new AudioFormatRegistry().getFromMimeType(o.getString("mime_type")), + new AudioFormatRegistry().getFromMimeTypeOrThrow(o.getString("mime_type")), new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url")) )) .collect(Collectors.toList()); @@ -108,7 +108,8 @@ public List getAudioStreams() throws ExtractionException { public List getVideoStreams() throws ExtractionException { return getRecordingsByMimeType("video") .map(o -> new SimpleVideoAudioStreamImpl( - new VideoAudioFormatRegistry().getFromMimeType(o.getString("mime_type")), + new VideoAudioFormatRegistry() + .getFromMimeTypeOrThrow(o.getString("mime_type")), new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url")), VideoQualityData.fromHeightWidth( o.getInt("height", VideoQualityData.UNKNOWN), diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index 18d5c834f8..8d336a24b1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -402,7 +402,7 @@ private void tryExtractSubtitles() { return new SimpleSubtitleStreamImpl( // TODO: Check for null new SubtitleFormatRegistry() - .getFromSuffix( + .getFromSuffixOrThrow( url.substring(url.lastIndexOf(".") + 1)), new SimpleProgressiveHTTPDeliveryDataImpl(url), false, @@ -497,7 +497,7 @@ private void addStreamsFromArray( playlistUrl, (s, dd) -> new SimpleAudioStreamImpl( new AudioFormatRegistry() - .getFromSuffix(getExtensionFromStream(s)), + .getFromSuffixOrThrow(getExtensionFromStream(s)), dd ) ); @@ -510,7 +510,7 @@ private void addStreamsFromArray( playlistUrl, (s, dd) -> new SimpleVideoAudioStreamImpl( new VideoAudioFormatRegistry() - .getFromSuffix(getExtensionFromStream(s)), + .getFromSuffixOrThrow(getExtensionFromStream(s)), dd, VideoQualityData.fromHeightFps( resJson.getInt("id", VideoQualityData.UNKNOWN), diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index f77c9683c0..66c0a57dbb 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -324,7 +324,7 @@ private Optional extractDownloadableFileIfAvailable() { } return Optional.of(new SimpleAudioStreamImpl( - new AudioFormatRegistry().getFromSuffix(fileType), + new AudioFormatRegistry().getFromSuffixOrThrow(fileType), new SimpleProgressiveHTTPDeliveryDataImpl(downloadUrl) )); } catch (final Exception ignored) { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java index 792da1acd6..53d981a766 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/format/registry/MediaFormatRegistry.java @@ -57,23 +57,29 @@ public String getMimeById(final int id) { /** * Return the MediaFormat with the supplied mime type * - * @return MediaFormat associated with this mime type, - * or null if none match it. + * @return MediaFormat associated with this mime type + * @throws IllegalStateException if there is no matching MediaFormat */ - @Nullable - public F getFromMimeType(final String mimeType) { + @Nonnull + public F getFromMimeTypeOrThrow(final String mimeType) { return Arrays.stream(values()) .filter(mediaFormat -> mediaFormat.mimeType().equals(mimeType)) .findFirst() - .orElse(null); + .orElseThrow(() -> new IllegalStateException("No matching MediaFormat")); } - @Nullable - public F getFromSuffix(final String suffix) { + /** + * Return the MediaFormat with the supplied suffix + * + * @return MediaFormat associated with this suffix + * @throws IllegalStateException if there is no matching MediaFormat + */ + @Nonnull + public F getFromSuffixOrThrow(final String suffix) { return Arrays.stream(values()) .filter(mediaFormat -> mediaFormat.suffix().equals(suffix)) .findFirst() - .orElse(null); + .orElseThrow(() -> new IllegalStateException("No matching MediaFormat")); } } From 9c715d7c82dec0ac6245aab0d2b0878cad400ffc Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 18 Jun 2022 22:30:08 +0200 Subject: [PATCH 27/43] Fixed Soundcloud test: Return EVERYTHING that is found deduplication should be done by the extractor --- .../extractors/SoundcloudStreamExtractor.java | 20 +--- .../stream/util/NewPipeStreamCollectors.java | 98 ------------------- .../stream/util/NewPipeStreamUtil.java | 38 ------- .../SoundcloudStreamExtractorTest.java | 21 ++-- 4 files changed, 8 insertions(+), 169 deletions(-) delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 66c0a57dbb..7e633cfc1c 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -34,7 +34,6 @@ import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl; -import org.schabi.newpipe.extractor.streamdata.stream.util.NewPipeStreamCollectors; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -44,6 +43,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -185,15 +185,6 @@ public List getAudioStreams() throws ExtractionException { } } - private static boolean checkMp3ProgressivePresence(@Nonnull final JsonArray transcodings) { - return transcodings.stream() - .filter(JsonObject.class::isInstance) - .map(JsonObject.class::cast) - .anyMatch(transcoding -> transcoding.getString("preset").contains("mp3") - && transcoding.getObject("format").getString("protocol") - .equals("progressive")); - } - @Nonnull private String getTranscodingUrl(final String endpointUrl, final String protocol) @@ -237,8 +228,6 @@ private List extractAudioStreams() { return Collections.emptyList(); } - final boolean mp3ProgressiveInStreams = checkMp3ProgressivePresence(transcodings); - return transcodings.stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) @@ -262,11 +251,6 @@ private List extractAudioStreams() { final AudioMediaFormat mediaFormat; final int averageBitrate; if (preset.contains("mp3")) { - // Don't add the MP3 HLS stream if there is a progressive stream present - // because the two have the same bitrate - if (mp3ProgressiveInStreams && protocol.equals("hls")) { - return null; - } mediaFormat = AudioFormatRegistry.MP3; averageBitrate = 128; } else if (preset.contains("opus")) { @@ -285,7 +269,7 @@ private List extractAudioStreams() { ); }) .filter(Objects::nonNull) - .collect(NewPipeStreamCollectors.toDistinctList()); + .collect(Collectors.toList()); } /** diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java deleted file mode 100644 index bdbf84163e..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamCollectors.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.schabi.newpipe.extractor.streamdata.stream.util; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BinaryOperator; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collector; -import java.util.stream.Stream; - -public final class NewPipeStreamCollectors { - private NewPipeStreamCollectors() { - // No impl - } - - public static > - Collector> toDistinctList() { - return deduplicateEqualStreams(x -> x); - } - - public static > - Collector> toDistinctStream() { - return deduplicateEqualStreams(List::stream); - } - - public static , R> - Collector deduplicateEqualStreams(final Function, R> finisher) { - return new CollectorImpl<>( - (Supplier>) ArrayList::new, - List::add, - (left, right) -> { - for (final T rightElement : right) { - if (NewPipeStreamUtil.containSimilarStream(rightElement, left)) { - left.add(rightElement); - } - } - return left; - }, - finisher, - CH_ID); - } - - // region COPIED FROM java.util.stream.Collectors - - static final Set CH_ID = - Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); - - static class CollectorImpl implements Collector { - private final Supplier
supplier; - private final BiConsumer accumulator; - private final BinaryOperator combiner; - private final Function finisher; - private final Set characteristics; - - CollectorImpl(final Supplier supplier, - final BiConsumer accumulator, - final BinaryOperator combiner, - final Function finisher, - final Set characteristics) { - this.supplier = supplier; - this.accumulator = accumulator; - this.combiner = combiner; - this.finisher = finisher; - this.characteristics = characteristics; - } - - @Override - public BiConsumer accumulator() { - return accumulator; - } - - @Override - public Supplier supplier() { - return supplier; - } - - @Override - public BinaryOperator combiner() { - return combiner; - } - - @Override - public Function finisher() { - return finisher; - } - - @Override - public Set characteristics() { - return characteristics; - } - } - - //endregion -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java deleted file mode 100644 index 76fbc34cc0..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/util/NewPipeStreamUtil.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.schabi.newpipe.extractor.streamdata.stream.util; - -import org.schabi.newpipe.extractor.streamdata.stream.Stream; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public final class NewPipeStreamUtil { - private NewPipeStreamUtil() { - // No impl - } - - /** - * Checks if the list already contains a stream with the same statistics. - * - * @param stream the stream to be compared against the streams in the stream list - * @param streams 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, - final Collection streams) { - if (streams == null || streams.isEmpty()) { - return false; - } - return streams.stream().anyMatch(stream::equalsStream); - } - - public List removeEqualStreams(final Collection streams) { - final List returnList = new ArrayList<>(); - for (final T stream : streams) { - if (!containSimilarStream(stream, returnList)) { - returnList.add(stream); - } - } - return returnList; - } -} diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java index 8c7f2f0961..63835ca469 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java @@ -16,6 +16,7 @@ import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.streamdata.delivery.ProgressiveHTTPDeliveryData; +import org.schabi.newpipe.extractor.streamdata.delivery.UrlBasedDeliveryData; import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry; import org.schabi.newpipe.extractor.streamdata.stream.AudioStream; @@ -191,30 +192,20 @@ public void testAudioStreams() throws Exception { super.testAudioStreams(); final List audioStreams = extractor.getAudioStreams(); - assertEquals(2, audioStreams.size()); + assertEquals(3, audioStreams.size()); for (final AudioStream audioStream : audioStreams) { - assertTrue(audioStream.deliveryData() instanceof ProgressiveHTTPDeliveryData, + assertTrue(audioStream.deliveryData() instanceof UrlBasedDeliveryData, "Wrong delivery method for mediaFormat=" + audioStream.mediaFormat() + " , avgBR=" + audioStream.averageBitrate() + " , deliverDataType=" + audioStream.deliveryData().getClass() ); - final ProgressiveHTTPDeliveryData deliveryData = - (ProgressiveHTTPDeliveryData) audioStream.deliveryData(); + final UrlBasedDeliveryData deliveryData = + (UrlBasedDeliveryData) audioStream.deliveryData(); final String mediaUrl = deliveryData.url(); - if (audioStream.mediaFormat() == AudioFormatRegistry.OPUS) { - // Assert that it's an OPUS 64 kbps media URL with a single range which comes - // from an HLS SoundCloud CDN - ExtractorAsserts.assertContains("-hls-opus-media.sndcdn.com", mediaUrl); - ExtractorAsserts.assertContains(".64.opus", mediaUrl); - } else if (audioStream.mediaFormat() == AudioFormatRegistry.MP3) { - // Assert that it's a MP3 128 kbps media URL which comes from a progressive - // SoundCloud CDN - ExtractorAsserts.assertContains("-media.sndcdn.com/bKOA7Pwbut93.128.mp3", - mediaUrl); - } + ExtractorAsserts.assertContains("-media.sndcdn.com", mediaUrl); } } } From 579de7bb944f7f4f78d3386309968b193d1ec1ca Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 18 Jun 2022 23:13:54 +0200 Subject: [PATCH 28/43] Fix merge conflicts --- .../MediaCCCLiveStreamMapperDTO.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java new file mode 100644 index 0000000000..c06ef736bf --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java @@ -0,0 +1,29 @@ +package org.schabi.newpipe.extractor.services.media_ccc.extractors; + +import com.grack.nanojson.JsonObject; + +final class MediaCCCLiveStreamMapperDTO { + private final JsonObject streamJsonObj; + private final String urlKey; + private final JsonObject urlValue; + + MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj, + final String urlKey, + final JsonObject urlValue) { + this.streamJsonObj = streamJsonObj; + this.urlKey = urlKey; + this.urlValue = urlValue; + } + + JsonObject getStreamJsonObj() { + return streamJsonObj; + } + + String getUrlKey() { + return urlKey; + } + + JsonObject getUrlValue() { + return urlValue; + } +} From 8c71edf6d079fa6c6f3cd23ec42fb87a5d640926 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sat, 18 Jun 2022 23:25:18 +0200 Subject: [PATCH 29/43] Fixed merge conflicts from https://github.com/TeamNewPipe/NewPipeExtractor/pull/859 --- .../AbstractYoutubeDashManifestCreator.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java index 9bcedccd84..a0bcb47155 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java @@ -381,14 +381,16 @@ protected void generateAudioChannelConfigurationElement() { "schemeIdUri", "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"); - final Integer audioChannels = itagInfo.getAudioChannels(); + Integer audioChannels = itagInfo.getAudioChannels(); if (audioChannels != null && audioChannels <= 0) { - throw new DashManifestCreationException( - "Invalid value for 'audioChannels'=" + audioChannels); + // Encountered invalid number of audio channels + // Most audio streams have 2 audio channels, so use this value as fallback + // see also https://github.com/TeamNewPipe/NewPipeExtractor/pull/859 + audioChannels = 2; } appendNewAttrWithValue( - audioChannelConfigElement, "value", itagInfo.getAudioChannels()); + audioChannelConfigElement, "value", audioChannels); getFirstElementByName(REPRESENTATION).appendChild(audioChannelConfigElement); } catch (final DOMException e) { From eb74ae34053a50aa9b38e7ee2c2708833d123e93 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Sun, 19 Jun 2022 15:08:02 +0200 Subject: [PATCH 30/43] Fixed ``YoutubeDashManifestCreatorsTest`` --- .../youtube/YoutubeDashManifestCreatorsTest.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java index 99e5441a57..6b226c24e8 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; -import org.schabi.newpipe.downloader.DownloaderFactory; +import org.schabi.newpipe.downloader.DownloaderTestImpl; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeOtfDashManifestCreator; @@ -38,7 +38,6 @@ import org.schabi.newpipe.extractor.streamdata.stream.Stream; import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream; import org.schabi.newpipe.extractor.streamdata.stream.VideoStream; -import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -48,9 +47,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -93,15 +90,14 @@ class YoutubeDashManifestCreatorsTest { private static final int MAX_STREAMS_TO_TEST_PER_METHOD = 5; private static final String URL = "https://www.youtube.com/watch?v=DJ8GQUNUXGM"; - private static final String RESOURCE_PATH = - DownloaderFactory.RESOURCE_PATH + "services/youtube/extractor/dashmanifest/"; - private static YoutubeStreamExtractor extractor; @BeforeAll public static void setUp() throws Exception { YoutubeTestsUtils.ensureStateless(); - NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH)); + // Has to be done with a real downloader otherwise because there are secondary requests when + // building a DASHManifest which require valid requests with a real IP + NewPipe.init(DownloaderTestImpl.getInstance()); extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(URL); extractor.fetchPage(); @@ -116,7 +112,7 @@ void testVideoOnlyStreams() throws ExtractionException { @Test void testVideoStreams() throws ExtractionException { - List videoAudioStreams = getDashStreams(extractor.getVideoStreams()); + final List videoAudioStreams = getDashStreams(extractor.getVideoStreams()); assertEquals(0, videoAudioStreams.size(), "There should be no dash streams for video-audio streams"); } From d2e82bdc84f67ad866e9ddbe4653fd1702312973 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Mon, 20 Jun 2022 21:20:05 +0200 Subject: [PATCH 31/43] Removed unused ``equalsStream`` --- .../extractor/streamdata/stream/AudioStream.java | 16 +--------------- .../streamdata/stream/BaseAudioStream.java | 11 ----------- .../extractor/streamdata/stream/Stream.java | 5 ----- .../streamdata/stream/SubtitleStream.java | 14 -------------- .../streamdata/stream/VideoAudioStream.java | 11 +---------- .../extractor/streamdata/stream/VideoStream.java | 14 -------------- 6 files changed, 2 insertions(+), 69 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java index e3bb2a252e..dcb9fdce52 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/AudioStream.java @@ -2,23 +2,9 @@ import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat; -import java.util.Objects; - -import javax.annotation.Nullable; - /** * Represents a audio (only) stream. */ public interface AudioStream extends Stream, BaseAudioStream { - - @Override - default boolean equalsStream(@Nullable final Stream other) { - if (!(other instanceof AudioStream)) { - return false; - } - - final AudioStream otherAudioStream = (AudioStream) other; - return Objects.equals(mediaFormat(), otherAudioStream.mediaFormat()) - && averageBitrate() == otherAudioStream.averageBitrate(); - } + // Nothing } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/BaseAudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/BaseAudioStream.java index d6590b207b..ae37692b23 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/BaseAudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/BaseAudioStream.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.extractor.streamdata.stream; -import javax.annotation.Nullable; - public interface BaseAudioStream { int UNKNOWN_AVG_BITRATE = -1; @@ -13,13 +11,4 @@ public interface BaseAudioStream { default int averageBitrate() { return UNKNOWN_AVG_BITRATE; } - - default boolean equalsStream(@Nullable final Stream other) { - if (!(other instanceof BaseAudioStream)) { - return false; - } - - final BaseAudioStream otherAudioStream = (BaseAudioStream) other; - return averageBitrate() == otherAudioStream.averageBitrate(); - } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java index 3eccbe9b3e..ac3618811e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/Stream.java @@ -4,7 +4,6 @@ import org.schabi.newpipe.extractor.streamdata.format.MediaFormat; import javax.annotation.Nonnull; -import javax.annotation.Nullable; public interface Stream { @@ -18,8 +17,4 @@ public interface Stream { @Nonnull DeliveryData deliveryData(); - - - // TODO: May also have to check deliverydata - boolean equalsStream(@Nullable Stream other); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java index 4f7ca6c45b..98ceb9287a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java @@ -3,10 +3,8 @@ import org.schabi.newpipe.extractor.streamdata.format.SubtitleMediaFormat; import java.util.Locale; -import java.util.Objects; import javax.annotation.Nonnull; -import javax.annotation.Nullable; /** * Represents a subtitle (only) stream. @@ -42,16 +40,4 @@ default boolean autoGenerated() { * @return the {@link Locale locale} of the subtitles */ Locale locale(); - - @Override - default boolean equalsStream(@Nullable final Stream other) { - if (!(other instanceof SubtitleStream)) { - return false; - } - - final SubtitleStream otherSubtitleStream = (SubtitleStream) other; - return Objects.equals(mediaFormat(), otherSubtitleStream.mediaFormat()) - && autoGenerated() == otherSubtitleStream.autoGenerated() - && Objects.equals(languageCode(), otherSubtitleStream.languageCode()); - } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java index bffd0765e1..88904461be 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoAudioStream.java @@ -1,17 +1,8 @@ package org.schabi.newpipe.extractor.streamdata.stream; -import javax.annotation.Nullable; - /** * Represents a combined video+audio stream. */ public interface VideoAudioStream extends VideoStream, BaseAudioStream { - @Override - default boolean equalsStream(@Nullable final Stream other) { - if (!(other instanceof VideoAudioStream)) { - return false; - } - - return VideoStream.super.equalsStream(other) && BaseAudioStream.super.equalsStream(other); - } + // Nothing } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java index 5ad3b1f3f1..a5c32d2495 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java @@ -3,10 +3,7 @@ import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat; import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData; -import java.util.Objects; - import javax.annotation.Nonnull; -import javax.annotation.Nullable; /** * Represents a video (only) stream. @@ -14,15 +11,4 @@ public interface VideoStream extends Stream { @Nonnull VideoQualityData videoQualityData(); - - @Override - default boolean equalsStream(@Nullable final Stream other) { - if (!(other instanceof VideoStream)) { - return false; - } - - final VideoStream otherVideoStream = (VideoStream) other; - return Objects.equals(mediaFormat(), otherVideoStream.mediaFormat()) - && videoQualityData().equalsVideoQualityData(otherVideoStream.videoQualityData()); - } } From 3f1f2b291c0290900f6cd97a7f3b017b4876359a Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Tue, 21 Jun 2022 21:13:43 +0200 Subject: [PATCH 32/43] Naming changes --- .../streamdata/delivery/DASHManifestDeliveryData.java | 2 +- .../simpleimpl/SimpleDASHManifestDeliveryDataImpl.java | 4 ++-- .../extractor/services/DefaultStreamExtractorTest.java | 2 +- .../services/youtube/YoutubeDashManifestCreatorsTest.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java index 914c6c1d8c..412a512769 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java @@ -6,7 +6,7 @@ public interface DASHManifestDeliveryData extends DASHDeliveryData { @Nonnull - DashManifestCreator getDashManifestCreator(); + DashManifestCreator dashManifestCreator(); String getCachedDashManifestAsString(); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java index 54007e43af..d625016e63 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java @@ -33,14 +33,14 @@ public SimpleDASHManifestDeliveryDataImpl( @Override @Nonnull - public DashManifestCreator getDashManifestCreator() { + public DashManifestCreator dashManifestCreator() { return dashManifestCreator; } @Override public String getCachedDashManifestAsString() { if (cachedDashManifest == null) { - cachedDashManifest = getDashManifestCreator().generateManifest(); + cachedDashManifest = dashManifestCreator().generateManifest(); } return cachedDashManifest; } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java index 5ca143aec2..6dfcbdf5ba 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java @@ -340,7 +340,7 @@ private void checkDeliveryData(final DeliveryData deliveryData) { } else if (deliveryData instanceof DASHManifestDeliveryData) { final DASHManifestDeliveryData dashManifestDD = (DASHManifestDeliveryData) deliveryData; - assertNotNull(dashManifestDD.getDashManifestCreator()); + assertNotNull(dashManifestDD.dashManifestCreator()); } } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java index 6b226c24e8..a5f79d59e6 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java @@ -170,7 +170,7 @@ private void checkGeneratedManifest( } final DashManifestCreator dashManifestCreator = - ((DASHManifestDeliveryData) stream.deliveryData()).getDashManifestCreator(); + ((DASHManifestDeliveryData) stream.deliveryData()).dashManifestCreator(); if (dashManifestCreator instanceof YoutubeOtfDashManifestCreator) { asserts.add(() -> assertSegmentTemplateElement(document)); asserts.add(() -> assertSegmentTimelineAndSElements(document)); From 53909820f881c88c85df952c6aa8331b000c78c1 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Jun 2022 21:21:59 +0200 Subject: [PATCH 33/43] Cleanup ``MediaCCCLiveStreamExtractor`` --- .../MediaCCCLiveStreamExtractor.java | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 9a76177430..46659bf7fb 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -162,22 +163,16 @@ private String getManifestOfDeliveryMethodWanted(@Nonnull final String deliveryM public List getAudioStreams() throws IOException, ExtractionException { return getStreamDTOs("audio") .map(dto -> { - final String url = dto.getUrlValue().getString(URL); - final DeliveryData deliveryData; - if ("hls".equals(dto.getUrlKey())) { - deliveryData = new SimpleHLSDeliveryDataImpl(url); - } else if ("dash".equals(dto.getUrlKey())) { - deliveryData = new SimpleDASHUrlDeliveryDataImpl(url); - } else { - deliveryData = new SimpleProgressiveHTTPDeliveryDataImpl(url); + try { + return new SimpleAudioStreamImpl( + new AudioFormatRegistry().getFromSuffixOrThrow(dto.getUrlKey()), + buildDeliveryData(dto) + ); + } catch (final Exception ignored) { + return null; } - - return new SimpleAudioStreamImpl( - // TODO: This looks wrong - new AudioFormatRegistry().getFromSuffixOrThrow(dto.getUrlKey()), - deliveryData - ); }) + .filter(Objects::nonNull) .collect(Collectors.toList()); } @@ -185,30 +180,37 @@ public List getAudioStreams() throws IOException, ExtractionExcepti public List getVideoStreams() throws IOException, ExtractionException { return getStreamDTOs("video") .map(dto -> { - final String url = dto.getUrlValue().getString(URL); - final DeliveryData deliveryData; - if ("hls".equals(dto.getUrlKey())) { - deliveryData = new SimpleHLSDeliveryDataImpl(url); - } else if ("dash".equals(dto.getUrlKey())) { - deliveryData = new SimpleDASHUrlDeliveryDataImpl(url); - } else { - deliveryData = new SimpleProgressiveHTTPDeliveryDataImpl(url); + try { + final JsonArray videoSize = + dto.getStreamJsonObj().getArray("videoSize"); + + return new SimpleVideoAudioStreamImpl( + // TODO: This looks wrong + new VideoAudioFormatRegistry() + .getFromSuffixOrThrow(dto.getUrlKey()), + buildDeliveryData(dto), + VideoQualityData.fromHeightWidth( + videoSize.getInt(1, VideoQualityData.UNKNOWN), + videoSize.getInt(0, VideoQualityData.UNKNOWN)) + ); + } catch (final Exception ignored) { + return null; } - - final JsonArray videoSize = dto.getStreamJsonObj().getArray("videoSize"); - - return new SimpleVideoAudioStreamImpl( - // TODO: This looks wrong - new VideoAudioFormatRegistry().getFromSuffixOrThrow(dto.getUrlKey()), - deliveryData, - VideoQualityData.fromHeightWidth( - /*height=*/videoSize.getInt(1, VideoQualityData.UNKNOWN), - /*width=*/videoSize.getInt(0, VideoQualityData.UNKNOWN)) - ); }) + .filter(Objects::nonNull) .collect(Collectors.toList()); } + private DeliveryData buildDeliveryData(final MediaCCCLiveStreamMapperDTO dto) { + final String url = dto.getUrlValue().getString(URL); + if ("hls".equals(dto.getUrlKey())) { + return new SimpleHLSDeliveryDataImpl(url); + } else if ("dash".equals(dto.getUrlKey())) { + return new SimpleDASHUrlDeliveryDataImpl(url); + } + return new SimpleProgressiveHTTPDeliveryDataImpl(url); + } + private Stream getStreamDTOs(@Nonnull final String streamType) { return room.getArray(STREAMS).stream() // Ensure that we use only process JsonObjects From 0732bef14b7f1093f2565d867ff9e0ddbb704338 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Jun 2022 22:16:55 +0200 Subject: [PATCH 34/43] [Peertube] No special handling of live video streams, use ``getHlsMasterPlaylistUrl()`` instead --- .../extractors/PeertubeStreamExtractor.java | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index 8d336a24b1..aadbfa197d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -428,11 +428,6 @@ private void tryExtractStreams() throws ParsingException { audioStreams = new ArrayList<>(); videoStreams = new ArrayList<>(); - if (isLive()) { - extractLiveVideoStreams(); - return; - } - // Progressive streams try { addStreamsFromArray( @@ -455,28 +450,6 @@ private void tryExtractStreams() throws ParsingException { } } - private void extractLiveVideoStreams() throws ParsingException { - try { - json.getArray(STREAMING_PLAYLISTS) - .stream() - .filter(JsonObject.class::isInstance) - .map(JsonObject.class::cast) - // TODO Check! This is the master playlist! - .map(s -> new SimpleVideoAudioStreamImpl( - VideoAudioFormatRegistry.MPEG_4, - new SimpleHLSDeliveryDataImpl(s.getString(PLAYLIST_URL, ""))) - ) - // Don't use the containsSimilarStream method because it will always - // return - // false so if there are multiples HLS URLs returned, only the first - // will be - // extracted in this case. - .forEachOrdered(videoStreams::add); - } catch (final Exception e) { - throw new ParsingException("Could not get video streams", e); - } - } - private void addStreamsFromArray( @Nonnull final JsonArray streams, final String playlistUrl From 3ce7f2ef41aff471d4b6bba2b5ea205d2febe59c Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Jun 2022 22:07:46 +0200 Subject: [PATCH 35/43] Code cleanup Updated javadoc Cleanup more code * Improve /Add java doc * Remove unused code * Convert TODOs to followup issues --- ...BandcampSearchStreamInfoItemExtractor.java | 2 +- .../MediaCCCLiveStreamExtractor.java | 28 +++++++++++++++++- .../MediaCCCLiveStreamMapperDTO.java | 29 ------------------- .../extractors/PeertubeStreamExtractor.java | 1 - .../extractors/SoundcloudStreamExtractor.java | 2 +- .../AbstractYoutubeDashManifestCreator.java | 13 ++++++++- .../extractors/YoutubeStreamExtractor.java | 16 +++++++++- .../format/registry/ItagFormatRegistry.java | 10 ++++++- .../services/youtube/itag/info/ItagInfo.java | 1 - .../DashManifestCreatorConstants.java | 2 +- .../SimpleDASHManifestDeliveryDataImpl.java | 11 ------- .../SimpleVideoAudioStreamImpl.java | 7 ----- .../schabi/newpipe/extractor/utils/Pair.java | 2 +- 13 files changed, 67 insertions(+), 57 deletions(-) delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java index ef384496fc..d1a55b33a1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java @@ -22,7 +22,7 @@ public BandcampSearchStreamInfoItemExtractor(final Element searchResult, public String getUploaderName() { final String subhead = resultInfo.getElementsByClass("subhead").text(); final String[] splitBy = subhead.split("by "); - return splitBy.length > 1 ? splitBy[1] : splitBy[0]; + return splitBy[splitBy.length > 1 ? 1 : 0]; } @Nullable diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 46659bf7fb..6e839b5bce 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -185,7 +185,6 @@ public List getVideoStreams() throws IOException, ExtractionEx dto.getStreamJsonObj().getArray("videoSize"); return new SimpleVideoAudioStreamImpl( - // TODO: This looks wrong new VideoAudioFormatRegistry() .getFromSuffixOrThrow(dto.getUrlKey()), buildDeliveryData(dto), @@ -237,4 +236,31 @@ public boolean isLive() { public String getCategory() { return group; } + + static final class MediaCCCLiveStreamMapperDTO { + private final JsonObject streamJsonObj; + private final String urlKey; + private final JsonObject urlValue; + + MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj, + final String urlKey, + final JsonObject urlValue) { + this.streamJsonObj = streamJsonObj; + this.urlKey = urlKey; + this.urlValue = urlValue; + } + + JsonObject getStreamJsonObj() { + return streamJsonObj; + } + + String getUrlKey() { + return urlKey; + } + + JsonObject getUrlValue() { + return urlValue; + } + } + } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java deleted file mode 100644 index c06ef736bf..0000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.schabi.newpipe.extractor.services.media_ccc.extractors; - -import com.grack.nanojson.JsonObject; - -final class MediaCCCLiveStreamMapperDTO { - private final JsonObject streamJsonObj; - private final String urlKey; - private final JsonObject urlValue; - - MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj, - final String urlKey, - final JsonObject urlValue) { - this.streamJsonObj = streamJsonObj; - this.urlKey = urlKey; - this.urlValue = urlValue; - } - - JsonObject getStreamJsonObj() { - return streamJsonObj; - } - - String getUrlKey() { - return urlKey; - } - - JsonObject getUrlValue() { - return urlValue; - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index aadbfa197d..991d30d53f 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -400,7 +400,6 @@ private void tryExtractSubtitles() { baseUrl + JsonUtils.getString(caption, "captionPath"); return new SimpleSubtitleStreamImpl( - // TODO: Check for null new SubtitleFormatRegistry() .getFromSuffixOrThrow( url.substring(url.lastIndexOf(".") + 1)), diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 7e633cfc1c..a94840fac7 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -260,7 +260,7 @@ private List extractAudioStreams() { return null; } - return (AudioStream) new SimpleAudioStreamImpl( + return new SimpleAudioStreamImpl( mediaFormat, protocol.equals("hls") ? new SimpleHLSDeliveryDataImpl(mediaUrl) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java index a0bcb47155..f8c2cc9a2d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java @@ -54,7 +54,18 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; -// TODO: Doc +/** + * Abstract class for YouTube DASH manifest creation. + * + *

+ * This class includes common methods of manifest creators and useful constants. + *

+ * + *

+ * Generation of DASH documents and their conversion as a string is done using external classes + * from {@link org.w3c.dom} and {@link javax.xml} packages. + *

+ */ public abstract class AbstractYoutubeDashManifestCreator implements DashManifestCreator { /** diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java index a01a54489f..2d4f28734f 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java @@ -614,7 +614,8 @@ public List getAudioStreams() throws ExtractionException { @Override public List getVideoStreams() throws ExtractionException { - return buildStrems(FORMATS, + return buildStrems( + FORMATS, ItagFormatRegistry.VIDEO_AUDIO_FORMATS, (itagInfo, deliveryData) -> new SimpleVideoAudioStreamImpl( itagInfo.getItagFormat().mediaFormat(), @@ -1249,6 +1250,19 @@ I extends ItagFormat> List buildStrems( } } + /* + * Note: We build the manifests for YT ourself because the provided ones (according to AudricV) + *
    + *
  • aren't working https://github.com/google/ExoPlayer/issues/2422#issuecomment-283080031 + *
  • + *
  • causes memory problems; TransactionTooLargeException: data parcel size 3174340
  • + *
  • are not always returned, only for videos with OTF streams, or on (ended or not) + * livestreams
  • + *
  • Instead of downloading a 10MB manifest when you can generate one which is 1 or 2MB + * large
  • + *
  • Also, this manifest isn't used at all by modern YouTube clients.
  • + *
+ */ @Nonnull private > DeliveryData buildDeliveryData(final ItagInfo itagInfo) { final ItagFormatDeliveryData iDeliveryData = itagInfo.getItagFormat().deliveryData(); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java index e69dc49887..030230ee0a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java @@ -21,7 +21,15 @@ import java.util.stream.Stream; // CHECKSTYLE:OFF - Link is too long -// https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1195 +/** + * A registry that contains all supported YouTube itags. + *

+ * For additional information you may also check: + *

    + *
  • https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1195
  • + *
  • https://gist.github.com/AgentOak/34d47c65b1d28829bb17c24c04a0096f
  • + *
+ */ // CHECKSTYLE:ON public final class ItagFormatRegistry { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java index 5e8b7ed1a8..13c704d4cb 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java @@ -15,7 +15,6 @@ public class ItagInfo> { @Nonnull private final I itagFormat; - // TODO: Maybe generate the streamUrl on-demand and not always instantly? @Nonnull private final String streamUrl; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreatorConstants.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreatorConstants.java index 10d74239c0..01c494dcc4 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreatorConstants.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreatorConstants.java @@ -2,7 +2,7 @@ public final class DashManifestCreatorConstants { private DashManifestCreatorConstants() { - // No impl! + // No impl } // XML elements of DASH MPD manifests diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java index d625016e63..a32e6b86d6 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/simpleimpl/SimpleDASHManifestDeliveryDataImpl.java @@ -7,17 +7,6 @@ import javax.annotation.Nonnull; -/** - * Note we build the manifests for YT ourself because the provided ones (according to TiA4f8R) - *
    - *
  • aren't working https://github.com/google/ExoPlayer/issues/2422#issuecomment-283080031
  • - *
  • causes memory problems; TransactionTooLargeException: data parcel size 3174340
  • - *
  • are not always returned, only for videos with OTF streams, or on (ended or not) - * livestreams
  • - *
  • Instead of downloading a 10MB manifest when you can generate one which is 1 or 2MB large
  • - *
  • Also, this manifest isn't used at all by modern YouTube clients.
  • - *
- */ public class SimpleDASHManifestDeliveryDataImpl extends AbstractDeliveryDataImpl implements DASHManifestDeliveryData { @Nonnull diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java index 6d0bc18298..68d03a090e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java @@ -36,13 +36,6 @@ public SimpleVideoAudioStreamImpl( this(mediaFormat, deliveryData, videoQualityData, UNKNOWN_AVG_BITRATE); } - public SimpleVideoAudioStreamImpl( - @Nonnull final VideoAudioMediaFormat mediaFormat, - @Nonnull final DeliveryData deliveryData - ) { - this(mediaFormat, deliveryData, VideoQualityData.fromUnknown()); - } - @Nonnull @Override public VideoQualityData videoQualityData() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Pair.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Pair.java index 43da033ec5..943f9ddac2 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Pair.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/Pair.java @@ -3,7 +3,7 @@ import java.util.Objects; /** - * class to create a pair of objects. + * Class to create a pair of objects. * * *

From e3c989e1831897c2d1051b05bfc3933512ccb330 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 24 Jun 2022 22:55:48 +0200 Subject: [PATCH 36/43] Backported fixes from #864 https://github.com/TeamNewPipe/NewPipeExtractor/commit/090debd83b80b05e54d08abb2531453c5fc4e73c --- .../services/youtube/extractors/YoutubeStreamExtractor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java index 2d4f28734f..878424dd63 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java @@ -847,7 +847,8 @@ public void onFetchPage(@Nonnull final Downloader downloader) .getBytes(StandardCharsets.UTF_8); nextResponse = getJsonPostResponse(NEXT, body, localization); - if ((!isAgeRestricted && !isLive && !isPostLive) + // this will only be run for post-live and normal streams + if ((!isAgeRestricted && !isLive) || isAndroidClientFetchForced) { try { fetchAndroidMobileJsonPlayer(contentCountry, localization, videoId); From 889b5f2d9ea73ffa23f979fc991edfc4ca6a113b Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Wed, 6 Jul 2022 20:37:55 +0200 Subject: [PATCH 37/43] Improvements for NewPipe * Added support for content-length (important for downloader) * Code cleanup --- .../extractor/downloader/Downloader.java | 24 +++++++++++++++++-- .../AbstractYoutubeDashManifestCreator.java | 5 ++++ .../extractor/stream/StreamInfoItem.java | 8 +++---- .../stream/StreamInfoItemsCollector.java | 4 ++-- .../delivery/DASHManifestDeliveryData.java | 6 +++++ .../streamdata/delivery/DeliveryData.java | 10 +++++++- .../delivery/UrlBasedDeliveryData.java | 7 ++++++ .../DashManifestCreator.java | 9 +++++++ .../streamdata/stream/SubtitleStream.java | 5 ++++ .../streamdata/stream/VideoStream.java | 2 +- .../SimpleVideoAudioStreamImpl.java | 2 +- .../simpleimpl/SimpleVideoStreamImpl.java | 2 +- .../services/DefaultStreamExtractorTest.java | 4 ++-- 13 files changed, 74 insertions(+), 14 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/downloader/Downloader.java b/extractor/src/main/java/org/schabi/newpipe/extractor/downloader/Downloader.java index 75d0bbf805..18017ded2a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/downloader/Downloader.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/downloader/Downloader.java @@ -4,12 +4,13 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.localization.Localization; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.io.IOException; import java.util.List; import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * A base for downloader implementations that NewPipe will use * to download needed resources during extraction. @@ -148,8 +149,27 @@ public Response post(final String url, /** * Do a request using the specified {@link Request} object. * + * @param request The request to process * @return the result of the request */ public abstract Response execute(@Nonnull Request request) throws IOException, ReCaptchaException; + + /** + * Get the size of the content that the url is pointing by firing a HEAD request. + * + * @param url an url pointing to the content + * @return the size of the content, in bytes or -1 if unknown + */ + public long getContentLength(final String url) { + try { + final String contentLengthHeader = head(url).getHeader("Content-Length"); + if (contentLengthHeader == null) { + return -1; + } + return Long.parseLong(contentLengthHeader); + } catch (final Exception e) { + return -1; + } + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java index f8c2cc9a2d..2b55e2e874 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java @@ -102,6 +102,11 @@ protected AbstractYoutubeDashManifestCreator( this.durationSecondsFallback = durationSecondsFallback; } + @Override + public long getExpectedContentLength(final Downloader downloader) { + return downloader.getContentLength(itagInfo.getStreamUrl()); + } + protected boolean isLiveDelivery() { return false; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java index b88a275068..f3b45b6e63 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItem.java @@ -29,8 +29,8 @@ * Info object for previews of unopened videos, eg search results, related videos */ public class StreamInfoItem extends InfoItem { - private final boolean audioOnly; private final boolean live; + private final boolean audioOnly; private String uploaderName; private String shortDescription; @@ -47,11 +47,11 @@ public class StreamInfoItem extends InfoItem { public StreamInfoItem(final int serviceId, final String url, final String name, - final boolean audioOnly, - final boolean live) { + final boolean live, + final boolean audioOnly) { super(InfoType.STREAM, serviceId, url, name); - this.audioOnly = audioOnly; this.live = live; + this.audioOnly = audioOnly; } public boolean isAudioOnly() { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemsCollector.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemsCollector.java index d2d1b527a7..c294e18b77 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemsCollector.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfoItemsCollector.java @@ -48,8 +48,8 @@ public StreamInfoItem extract(final StreamInfoItemExtractor extractor) throws Pa getServiceId(), extractor.getUrl(), extractor.getName(), - extractor.isAudioOnly(), - extractor.isLive()); + extractor.isLive(), + extractor.isAudioOnly()); // optional information try { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java index 412a512769..56acf8e98c 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.extractor.streamdata.delivery; +import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreator; import javax.annotation.Nonnull; @@ -9,4 +10,9 @@ public interface DASHManifestDeliveryData extends DASHDeliveryData { DashManifestCreator dashManifestCreator(); String getCachedDashManifestAsString(); + + @Override + default long getExpectedContentLength(final Downloader downloader) { + return dashManifestCreator().getExpectedContentLength(downloader); + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java index 9149d7794c..1c861a6d8b 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java @@ -1,5 +1,13 @@ package org.schabi.newpipe.extractor.streamdata.delivery; +import org.schabi.newpipe.extractor.downloader.Downloader; + public interface DeliveryData { - // Only a marker so far + /** + * Returns the expected content length/size of the data. + * + * @param downloader The downloader that may be used for fetching (HTTP HEAD). + * @return the expected size/content length or -1 if unknown + */ + long getExpectedContentLength(Downloader downloader); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java index a0df54f913..1b7e56ecdc 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java @@ -1,8 +1,15 @@ package org.schabi.newpipe.extractor.streamdata.delivery; +import org.schabi.newpipe.extractor.downloader.Downloader; + import javax.annotation.Nonnull; public interface UrlBasedDeliveryData extends DeliveryData { @Nonnull String url(); + + @Override + default long getExpectedContentLength(final Downloader downloader) { + return downloader.getContentLength(url()); + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java index 66cd2d8607..0195f25ce9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java @@ -1,14 +1,23 @@ package org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator; +import org.schabi.newpipe.extractor.downloader.Downloader; + import javax.annotation.Nonnull; public interface DashManifestCreator { /** * Generates the DASH manifest. + * * @return The dash manifest as string. * @throws DashManifestCreationException May throw a CreationException */ @Nonnull String generateManifest(); + + /** + * See + * {@link org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData#getExpectedContentLength(Downloader)} + */ + long getExpectedContentLength(Downloader downloader); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java index 98ceb9287a..47aedce7d6 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/SubtitleStream.java @@ -39,5 +39,10 @@ default boolean autoGenerated() { * * @return the {@link Locale locale} of the subtitles */ + @Nonnull Locale locale(); + + default String getDisplayLanguageName() { + return locale().getDisplayName(locale()); + } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java index a5c32d2495..115d4bbd12 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/VideoStream.java @@ -10,5 +10,5 @@ */ public interface VideoStream extends Stream { @Nonnull - VideoQualityData videoQualityData(); + VideoQualityData qualityData(); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java index 68d03a090e..7dd0e266df 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoAudioStreamImpl.java @@ -38,7 +38,7 @@ public SimpleVideoAudioStreamImpl( @Nonnull @Override - public VideoQualityData videoQualityData() { + public VideoQualityData qualityData() { return videoQualityData; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java index 6f858e9f6d..1ea04b58de 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/stream/simpleimpl/SimpleVideoStreamImpl.java @@ -32,7 +32,7 @@ public SimpleVideoStreamImpl( @Nonnull @Override - public VideoQualityData videoQualityData() { + public VideoQualityData qualityData() { return videoQualityData; } } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java index 6dfcbdf5ba..24a74d5611 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java @@ -270,7 +270,7 @@ public void testVideoOnlyStreams() throws Exception { for (final VideoStream stream : videoOnlyStreams) { assertNotNull(stream.mediaFormat()); - assertNotNull(stream.videoQualityData()); + assertNotNull(stream.qualityData()); checkDeliveryData(stream.deliveryData()); } } else { @@ -289,7 +289,7 @@ public void testVideoAudioStreams() throws Exception { for (final VideoAudioStream stream : videoAudioStreams) { assertNotNull(stream.mediaFormat()); - assertNotNull(stream.videoQualityData()); + assertNotNull(stream.qualityData()); checkDeliveryData(stream.deliveryData()); } } else { From ff8eb685f3f9aa555f683daaa522115e27e421ca Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 26 Aug 2022 16:39:21 +0200 Subject: [PATCH 38/43] Modified type hierarchy for downloads --- .../AbstractYoutubeDashManifestCreator.java | 6 +++++ .../delivery/DASHManifestDeliveryData.java | 8 ++++++- .../delivery/DASHUrlDeliveryData.java | 2 +- .../streamdata/delivery/DeliveryData.java | 10 +-------- .../delivery/DownloadableDeliveryData.java | 22 +++++++++++++++++++ .../DownloadableUrlBasedDeliveryData.java | 19 ++++++++++++++++ .../streamdata/delivery/HLSDeliveryData.java | 2 +- .../delivery/ProgressiveHTTPDeliveryData.java | 2 +- .../delivery/UrlBasedDeliveryData.java | 7 ------ .../DashManifestCreator.java | 5 ++++- 10 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DownloadableDeliveryData.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DownloadableUrlBasedDeliveryData.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java index 2b55e2e874..1333c5db40 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java @@ -102,6 +102,12 @@ protected AbstractYoutubeDashManifestCreator( this.durationSecondsFallback = durationSecondsFallback; } + @Nonnull + @Override + public String downloadUrl() { + return itagInfo.getStreamUrl(); + } + @Override public long getExpectedContentLength(final Downloader downloader) { return downloader.getContentLength(itagInfo.getStreamUrl()); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java index 56acf8e98c..3360ca9e68 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHManifestDeliveryData.java @@ -5,12 +5,18 @@ import javax.annotation.Nonnull; -public interface DASHManifestDeliveryData extends DASHDeliveryData { +public interface DASHManifestDeliveryData extends DASHDeliveryData, DownloadableDeliveryData { @Nonnull DashManifestCreator dashManifestCreator(); String getCachedDashManifestAsString(); + @Nonnull + @Override + default String downloadUrl() { + return dashManifestCreator().downloadUrl(); + } + @Override default long getExpectedContentLength(final Downloader downloader) { return dashManifestCreator().getExpectedContentLength(downloader); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHUrlDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHUrlDeliveryData.java index ff9ff1b2e7..9204bb2b37 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHUrlDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DASHUrlDeliveryData.java @@ -1,5 +1,5 @@ package org.schabi.newpipe.extractor.streamdata.delivery; -public interface DASHUrlDeliveryData extends UrlBasedDeliveryData, DASHDeliveryData { +public interface DASHUrlDeliveryData extends DownloadableUrlBasedDeliveryData, DASHDeliveryData { // Nothing to implement additionally } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java index 1c861a6d8b..dfb2a158ab 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DeliveryData.java @@ -1,13 +1,5 @@ package org.schabi.newpipe.extractor.streamdata.delivery; -import org.schabi.newpipe.extractor.downloader.Downloader; - public interface DeliveryData { - /** - * Returns the expected content length/size of the data. - * - * @param downloader The downloader that may be used for fetching (HTTP HEAD). - * @return the expected size/content length or -1 if unknown - */ - long getExpectedContentLength(Downloader downloader); + // Just a marker } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DownloadableDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DownloadableDeliveryData.java new file mode 100644 index 0000000000..c4c57340c0 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DownloadableDeliveryData.java @@ -0,0 +1,22 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +import org.schabi.newpipe.extractor.downloader.Downloader; + +import javax.annotation.Nonnull; + +/** + * Provides information for downloading a stream. + */ +public interface DownloadableDeliveryData extends DeliveryData { + + @Nonnull + String downloadUrl(); + + /** + * Returns the expected content length/size of the data. + * + * @param downloader The downloader that may be used for fetching (HTTP HEAD). + * @return the expected size/content length or -1 if unknown + */ + long getExpectedContentLength(Downloader downloader); +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DownloadableUrlBasedDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DownloadableUrlBasedDeliveryData.java new file mode 100644 index 0000000000..e00b735289 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/DownloadableUrlBasedDeliveryData.java @@ -0,0 +1,19 @@ +package org.schabi.newpipe.extractor.streamdata.delivery; + +import org.schabi.newpipe.extractor.downloader.Downloader; + +import javax.annotation.Nonnull; + +public interface DownloadableUrlBasedDeliveryData + extends UrlBasedDeliveryData, DownloadableDeliveryData { + @Nonnull + @Override + default String downloadUrl() { + return url(); + } + + @Override + default long getExpectedContentLength(final Downloader downloader) { + return downloader.getContentLength(url()); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/HLSDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/HLSDeliveryData.java index 4023a32e55..abe46f86d5 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/HLSDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/HLSDeliveryData.java @@ -1,5 +1,5 @@ package org.schabi.newpipe.extractor.streamdata.delivery; -public interface HLSDeliveryData extends UrlBasedDeliveryData { +public interface HLSDeliveryData extends DownloadableUrlBasedDeliveryData { // Nothing to implement additionally } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java index 7591af4624..672ad1c512 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/ProgressiveHTTPDeliveryData.java @@ -1,5 +1,5 @@ package org.schabi.newpipe.extractor.streamdata.delivery; -public interface ProgressiveHTTPDeliveryData extends UrlBasedDeliveryData { +public interface ProgressiveHTTPDeliveryData extends DownloadableUrlBasedDeliveryData { // Nothing to implement additionally } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java index 1b7e56ecdc..a0df54f913 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/UrlBasedDeliveryData.java @@ -1,15 +1,8 @@ package org.schabi.newpipe.extractor.streamdata.delivery; -import org.schabi.newpipe.extractor.downloader.Downloader; - import javax.annotation.Nonnull; public interface UrlBasedDeliveryData extends DeliveryData { @Nonnull String url(); - - @Override - default long getExpectedContentLength(final Downloader downloader) { - return downloader.getContentLength(url()); - } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java index 0195f25ce9..dddfb951cc 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java @@ -15,9 +15,12 @@ public interface DashManifestCreator { @Nonnull String generateManifest(); + @Nonnull + String downloadUrl(); + /** * See - * {@link org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData#getExpectedContentLength(Downloader)} + * {@link org.schabi.newpipe.extractor.streamdata.delivery.DownloadableDeliveryData#getExpectedContentLength(Downloader)} */ long getExpectedContentLength(Downloader downloader); } From fa57ec08d2617668bd48c1a4d9c6cf9ca846f053 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 26 Aug 2022 16:52:38 +0200 Subject: [PATCH 39/43] Code improvements * Throw ContentNotSupportedExceptions when extracting (as was done before) --- .../YoutubeOtfDashManifestCreator.java | 8 +++----- .../org/schabi/newpipe/extractor/stream/StreamInfo.java | 4 ++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java index a29f48a639..4762d35875 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java @@ -50,11 +50,9 @@ public String generateManifest() { // Get all durations and repetitions which are separated by a comma .split(","); final int lastIndex = segmentsAndDurationsResponseSplit.length - 1; - if (isBlank(segmentsAndDurationsResponseSplit[lastIndex])) { - segmentDurations = Arrays.copyOf(segmentsAndDurationsResponseSplit, lastIndex); - } else { - segmentDurations = segmentsAndDurationsResponseSplit; - } + segmentDurations = isBlank(segmentsAndDurationsResponseSplit[lastIndex]) + ? Arrays.copyOf(segmentsAndDurationsResponseSplit, lastIndex) + : segmentsAndDurationsResponseSplit; } catch (final Exception e) { throw new DashManifestCreationException("Could not get segment durations", e); } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index 5b5710945a..2c16ce71b8 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -523,6 +523,8 @@ private static void extractStreams(final StreamInfo streamInfo, /* Extract video stream url */ try { streamInfo.setVideoStreams(extractor.getVideoStreams()); + } catch (final ContentNotSupportedException e) { + throw e; } catch (final Exception e) { streamInfo.addError(new ExtractionException("Couldn't get video streams", e)); } @@ -530,6 +532,8 @@ private static void extractStreams(final StreamInfo streamInfo, /* Extract video only stream url */ try { streamInfo.setVideoOnlyStreams(extractor.getVideoOnlyStreams()); + } catch (final ContentNotSupportedException e) { + throw e; } catch (final Exception e) { streamInfo.addError(new ExtractionException("Couldn't get video only streams", e)); } From c9e911ec13d3f602e3a89ad97ee43558be0124f0 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 26 Aug 2022 16:58:05 +0200 Subject: [PATCH 40/43] Implemented ``StreamResolvingStrategy`` Not finished yet --- .../extractor/stream/StreamExtractor.java | 30 ++++++++++++++++++ .../newpipe/extractor/stream/StreamInfo.java | 12 +++++++ .../stream/StreamResolvingStrategy.java | 31 +++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamResolvingStrategy.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java index 0d2d8d1d76..8d9d8b5ffe 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java @@ -38,6 +38,7 @@ import org.schabi.newpipe.extractor.utils.Parser; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -256,6 +257,35 @@ public String getSubChannelAvatarUrl() throws ParsingException { return ""; } + /** + * Defines how the current stream info should best be resolved. + * + *

+ * Service mostly offer different methods for streaming data. + * However the order is not always clearly defined. + * E.g. resolving a livestream might be better using the HLS master playlist. + *

+ * + * @return A list with the StreamResolutionMode order by priority (0 = highest priority) + */ + @Nonnull + public List getResolverStrategyPriority() { + if (isLive()) { + return Arrays.asList( + StreamResolvingStrategy.HLS_MASTER_PLAYLIST_URL, + StreamResolvingStrategy.DASH_MPD_URL, + StreamResolvingStrategy.VIDEO_ONLY_AND_AUDIO_STREAMS, + StreamResolvingStrategy.VIDEO_AUDIO_STREAMS + ); + } + return Arrays.asList( + StreamResolvingStrategy.VIDEO_ONLY_AND_AUDIO_STREAMS, + StreamResolvingStrategy.VIDEO_AUDIO_STREAMS, + StreamResolvingStrategy.HLS_MASTER_PLAYLIST_URL, + StreamResolvingStrategy.DASH_MPD_URL + ); + } + /** * Get the dash mpd url. * diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index 2c16ce71b8..a834d726d1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -72,6 +72,7 @@ public class StreamInfo extends Info { private String subChannelUrl = ""; private String subChannelAvatarUrl = ""; + private List streamResolvingStrategies = new ArrayList<>(); private List videoStreams = new ArrayList<>(); private List audioStreams = new ArrayList<>(); private List videoOnlyStreams = new ArrayList<>(); @@ -275,6 +276,15 @@ public void setSubChannelAvatarUrl(final String subChannelAvatarUrl) { this.subChannelAvatarUrl = subChannelAvatarUrl; } + @Nonnull + public List getStreamResolvingStrategies() { + return streamResolvingStrategies; + } + + public void setStreamResolvingStrategies(@Nonnull final List streamResolvingStrategies) { + this.streamResolvingStrategies = streamResolvingStrategies; + } + @Nonnull public List getVideoStreams() { return videoStreams; @@ -507,6 +517,8 @@ private static StreamInfo extractImportantData(@Nonnull final StreamExtractor ex private static void extractStreams(final StreamInfo streamInfo, final StreamExtractor extractor) throws ExtractionException { + streamInfo.setStreamResolvingStrategies(extractor.getResolverStrategyPriority()); + /* ---- Stream extraction goes here ---- */ // At least one type of stream has to be available, otherwise an exception will be thrown // directly into the frontend. diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamResolvingStrategy.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamResolvingStrategy.java new file mode 100644 index 0000000000..b024fcbad0 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamResolvingStrategy.java @@ -0,0 +1,31 @@ +package org.schabi.newpipe.extractor.stream; + +/** + * Defines what strategy of the extractor is used for playback. + */ +public enum StreamResolvingStrategy { + /** + * Uses video streams (with no audio) and separate audio streams. + * @see StreamExtractor#getVideoOnlyStreams() + * @see StreamExtractor#getAudioStreams() + */ + VIDEO_ONLY_AND_AUDIO_STREAMS, + /** + * Uses video streams that include audio data. + * + * @see StreamExtractor#getVideoStreams() + */ + VIDEO_AUDIO_STREAMS, + /** + * Uses the HLS master playlist url. + * + * @see StreamExtractor#getHlsMasterPlaylistUrl() + */ + HLS_MASTER_PLAYLIST_URL, + /** + * Uses the DASH MPD url. + * + * @see StreamExtractor#getDashMpdUrl() + */ + DASH_MPD_URL +} From 95a8b8b823bdbcd3e3c313c512c727874950fc2c Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 26 Aug 2022 17:27:38 +0200 Subject: [PATCH 41/43] Fixed compile problems after rebase --- .../media_ccc/extractors/MediaCCCLiveStreamExtractor.java | 3 --- .../dashmanifestcreator/YoutubeOtfDashManifestCreator.java | 7 +++---- .../org/schabi/newpipe/extractor/stream/StreamInfo.java | 3 ++- .../delivery/dashmanifestcreator/DashManifestCreator.java | 2 ++ 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 6e839b5bce..7322f75405 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -1,8 +1,5 @@ package org.schabi.newpipe.extractor.services.media_ccc.extractors; -import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; -import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN; - import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java index 4762d35875..df24ece3b6 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator; import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TIMELINE; -import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import static org.schabi.newpipe.extractor.utils.Utils.removeNonDigitCharacters; @@ -29,9 +28,9 @@ public String generateManifest() { // from video servers. final Response response = getInitializationResponse(itagInfo.getStreamUrl()); final String otfBaseStreamingUrl = response.latestUrl() - .replace(SQ_0, EMPTY_STRING) - .replace(RN_0, EMPTY_STRING) - .replace(ALR_YES, EMPTY_STRING); + .replace(SQ_0, "") + .replace(RN_0, "") + .replace(ALR_YES, ""); final int responseCode = response.responseCode(); if (responseCode != 200) { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index a834d726d1..ebdb2538a1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -281,7 +281,8 @@ public List getStreamResolvingStrategies() { return streamResolvingStrategies; } - public void setStreamResolvingStrategies(@Nonnull final List streamResolvingStrategies) { + public void setStreamResolvingStrategies( + @Nonnull final List streamResolvingStrategies) { this.streamResolvingStrategies = streamResolvingStrategies; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java index dddfb951cc..77a39b5299 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/streamdata/delivery/dashmanifestcreator/DashManifestCreator.java @@ -18,9 +18,11 @@ public interface DashManifestCreator { @Nonnull String downloadUrl(); + // CHECKSTYLE:OFF - Link is too long /** * See * {@link org.schabi.newpipe.extractor.streamdata.delivery.DownloadableDeliveryData#getExpectedContentLength(Downloader)} */ + // CHECKSTYLE:ON long getExpectedContentLength(Downloader downloader); } From 8b7cb62c8887dd7e23763f6909c508b05906b6ea Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 26 Aug 2022 17:28:31 +0200 Subject: [PATCH 42/43] Remove useless checkstyle off --- .../services/youtube/YoutubeParsingHelper.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index 273d17abd7..d9fd8cf283 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -680,18 +680,10 @@ private static void extractClientVersionAndKeyFromHtmlSearchResultsPage() } catch (final Parser.RegexException ignored) { } - if (isNullOrEmpty(key)) { - throw new ParsingException( - // CHECKSTYLE:OFF - "Could not extract YouTube WEB InnerTube API key from HTML search results page"); - // CHECKSTYLE:ON - } - - if (clientVersion == null) { - throw new ParsingException( - // CHECKSTYLE:OFF - "Could not extract YouTube WEB InnerTube client version from HTML search results page"); - // CHECKSTYLE:ON + if (isNullOrEmpty(key) || clientVersion == null) { + throw new ParsingException("Could not extract YouTube WEB InnerTube " + + (isNullOrEmpty(key) ? "API key" : "client version") + + " from HTML search results page"); } keyAndVersionExtracted = true; From c2f3c1aa069ac78d0f4b31ef0a6fb646e87a8db9 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Fri, 26 Aug 2022 17:35:17 +0200 Subject: [PATCH 43/43] Added changes from (missing) rebase commit 301a795ed388b84cdad030e18a2f4af4a9f8b75c https://github.com/TeamNewPipe/NewPipeExtractor/commit/301a795ed388b84cdad030e18a2f4af4a9f8b75c --- .../extractors/SoundcloudStreamExtractor.java | 51 ++----------------- 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index a94840fac7..049552b314 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -2,7 +2,6 @@ import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL; import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.clientId; -import static org.schabi.newpipe.extractor.utils.Utils.HTTPS; import static org.schabi.newpipe.extractor.utils.Utils.UTF_8; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; @@ -186,13 +185,10 @@ public List getAudioStreams() throws ExtractionException { } @Nonnull - private String getTranscodingUrl(final String endpointUrl, - final String protocol) + private String getTranscodingUrl(final String endpointUrl) throws IOException, ExtractionException { - final Downloader downloader = NewPipe.getDownloader(); - final String apiStreamUrl = endpointUrl + "?client_id=" - + clientId(); - final String response = downloader.get(apiStreamUrl).responseBody(); + final String apiStreamUrl = endpointUrl + "?client_id=" + clientId(); + final String response = NewPipe.getDownloader().get(apiStreamUrl).responseBody(); final JsonObject urlObject; try { urlObject = JsonParser.object().from(response); @@ -238,7 +234,7 @@ private List extractAudioStreams() { .getString("protocol"); final String mediaUrl; try { - mediaUrl = getTranscodingUrl(transcoding.getString("url"), protocol); + mediaUrl = getTranscodingUrl(transcoding.getString("url")); } catch (final Exception e) { return null; // Abort if something went wrong } @@ -262,7 +258,7 @@ private List extractAudioStreams() { return new SimpleAudioStreamImpl( mediaFormat, - protocol.equals("hls") + "hls".equals(protocol) ? new SimpleHLSDeliveryDataImpl(mediaUrl) : new SimpleProgressiveHTTPDeliveryDataImpl(mediaUrl), averageBitrate @@ -349,43 +345,6 @@ private String determineFileTypeFromDownloadUrl(final String downloadUrl) return null; } - /** - * Parses a SoundCloud HLS manifest to get a single URL of HLS streams. - * - *

- * This method downloads the provided manifest URL, finds all web occurrences in the manifest, - * gets the last segment URL, changes its segment range to {@code 0/track-length}, and return - * this as a string. - *

- * - * @param hlsManifestUrl the URL of the manifest to be parsed - * @return a single URL that contains a range equal to the length of the track - */ - @Nonnull - private static String getSingleUrlFromHlsManifest(@Nonnull final String hlsManifestUrl) - throws ParsingException { - final Downloader dl = NewPipe.getDownloader(); - final String hlsManifestResponse; - - try { - hlsManifestResponse = dl.get(hlsManifestUrl).responseBody(); - } catch (final IOException | ReCaptchaException e) { - throw new ParsingException("Could not get SoundCloud HLS manifest"); - } - - final String[] lines = hlsManifestResponse.split("\\r?\\n"); - for (int l = lines.length - 1; l >= 0; l--) { - final String line = lines[l]; - // Get the last URL from manifest, because it contains the range of the stream - if (line.trim().length() != 0 && !line.startsWith("#") && line.startsWith("https")) { - final String[] hlsLastRangeUrlArray = line.split("/"); - return HTTPS + hlsLastRangeUrlArray[2] + "/media/0/" + hlsLastRangeUrlArray[5] - + "/" + hlsLastRangeUrlArray[6]; - } - } - throw new ParsingException("Could not get any URL from HLS manifest"); - } - @Override public boolean isAudioOnly() { return true;