Implement .describe() and aliases.
This commit is contained in:
parent
a1191fb31f
commit
d3316ca220
|
@ -1,3 +1,8 @@
|
||||||
|
## 1.10.8
|
||||||
|
- Added the following aliases: yt.videos.streams (instead of yt.videos.streamsClient) and yt.videos.comments (instead of yt.videos.commentsClient)
|
||||||
|
- Re-add more test cases.
|
||||||
|
- Implement `.describe()` on List<StreamInfo> which prints a formatted list like `youtube-dl -F` option.
|
||||||
|
|
||||||
## 1.10.7+1
|
## 1.10.7+1
|
||||||
- Fix tests.
|
- Fix tests.
|
||||||
- Remove debug leftovers.
|
- Remove debug leftovers.
|
||||||
|
|
|
@ -13,3 +13,4 @@ linter:
|
||||||
avoid_escaping_inner_quotes: false
|
avoid_escaping_inner_quotes: false
|
||||||
prefer_const_constructors: true
|
prefer_const_constructors: true
|
||||||
avoid_positional_boolean_parameters: false
|
avoid_positional_boolean_parameters: false
|
||||||
|
require_trailing_commas: false
|
||||||
|
|
|
@ -92,7 +92,7 @@ class _StreamInfo extends StreamInfoProvider {
|
||||||
late final int? bitrate = root.getT<int>('bitrate');
|
late final int? bitrate = root.getT<int>('bitrate');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final String? container = mimeType?.subtype;
|
late final String container = codec.subtype;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final int? contentLength = int.tryParse(
|
late final int? contentLength = int.tryParse(
|
||||||
|
@ -129,14 +129,20 @@ class _StreamInfo extends StreamInfoProvider {
|
||||||
late final int? videoHeight = root.getT<int>('height');
|
late final int? videoHeight = root.getT<int>('height');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final String? videoQualityLabel = root.getT<String>('qualityLabel');
|
@Deprecated('Use qualityLabel')
|
||||||
|
String get videoQualityLabel => qualityLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final String qualityLabel = root.getT<String>('qualityLabel') ??
|
||||||
|
'tiny'; // Not sure if 'tiny' is the correct placeholder.
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final int? videoWidth = root.getT<int>('width');
|
late final int? videoWidth = root.getT<int>('width');
|
||||||
|
|
||||||
late final bool isAudioOnly = mimeType?.type == 'audio';
|
late final bool isAudioOnly = codec.type == 'audio';
|
||||||
|
|
||||||
late final MediaType? mimeType = _getMimeType();
|
@override
|
||||||
|
late final MediaType codec = _getMimeType()!;
|
||||||
|
|
||||||
MediaType? _getMimeType() {
|
MediaType? _getMimeType() {
|
||||||
var mime = root.getT<String>('mimeType');
|
var mime = root.getT<String>('mimeType');
|
||||||
|
@ -146,7 +152,7 @@ class _StreamInfo extends StreamInfoProvider {
|
||||||
return MediaType.parse(mime);
|
return MediaType.parse(mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final String? codecs = mimeType?.parameters['codecs']?.toLowerCase();
|
late final String? codecs = codec.parameters['codecs']?.toLowerCase();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final String? audioCodec =
|
late final String? audioCodec =
|
||||||
|
|
|
@ -204,17 +204,25 @@ class _StreamInfo extends StreamInfoProvider {
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get container => _mimetype.subtype;
|
final MediaType codec;
|
||||||
|
|
||||||
final MediaType _mimetype;
|
|
||||||
|
|
||||||
bool get isAudioOnly => _mimetype.type == 'audio';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get audioCodec => isAudioOnly ? _mimetype.subtype : null;
|
String get container => codec.subtype;
|
||||||
|
|
||||||
|
bool get isAudioOnly => codec.type == 'audio';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get videoCodec => isAudioOnly ? null : _mimetype.subtype;
|
String? get audioCodec => isAudioOnly ? codec.subtype : null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get videoCodec => isAudioOnly ? null : codec.subtype;
|
||||||
|
|
||||||
|
@override
|
||||||
|
@Deprecated('Use qualityLabel')
|
||||||
|
String get videoQualityLabel => qualityLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final String qualityLabel = 'DASH';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int? videoWidth;
|
final int? videoWidth;
|
||||||
|
@ -231,8 +239,8 @@ class _StreamInfo extends StreamInfoProvider {
|
||||||
@override
|
@override
|
||||||
StreamSource get source => StreamSource.dash;
|
StreamSource get source => StreamSource.dash;
|
||||||
|
|
||||||
_StreamInfo(this.tag, this.url, this._mimetype, this.videoWidth,
|
_StreamInfo(this.tag, this.url, this.codec, this.videoWidth, this.videoHeight,
|
||||||
this.videoHeight, this.framerate, this.fragments);
|
this.framerate, this.fragments);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SegmentTimeline {
|
class _SegmentTimeline {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
import 'fragment.dart';
|
import 'fragment.dart';
|
||||||
|
|
||||||
enum StreamSource { muxed, adaptive, dash }
|
enum StreamSource { muxed, adaptive, dash }
|
||||||
|
@ -16,6 +18,8 @@ abstract class StreamInfoProvider {
|
||||||
///
|
///
|
||||||
String get url;
|
String get url;
|
||||||
|
|
||||||
|
MediaType get codec;
|
||||||
|
|
||||||
///
|
///
|
||||||
String? get signature => null;
|
String? get signature => null;
|
||||||
|
|
||||||
|
@ -38,8 +42,12 @@ abstract class StreamInfoProvider {
|
||||||
String? get videoCodec => null;
|
String? get videoCodec => null;
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@Deprecated('Use qualityLabel')
|
||||||
String? get videoQualityLabel => null;
|
String? get videoQualityLabel => null;
|
||||||
|
|
||||||
|
///
|
||||||
|
String get qualityLabel;
|
||||||
|
|
||||||
///
|
///
|
||||||
int? get videoWidth => null;
|
int? get videoWidth => null;
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,7 @@ class _StreamInfo extends StreamInfoProvider {
|
||||||
late final int? bitrate = root.getT<int>('bitrate');
|
late final int? bitrate = root.getT<int>('bitrate');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final String? container = mimeType?.subtype;
|
late final String container = codec.subtype;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final int? contentLength = int.tryParse(
|
late final int? contentLength = int.tryParse(
|
||||||
|
@ -206,14 +206,20 @@ class _StreamInfo extends StreamInfoProvider {
|
||||||
late final int? videoHeight = root.getT<int>('height');
|
late final int? videoHeight = root.getT<int>('height');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final String? videoQualityLabel = root.getT<String>('qualityLabel');
|
@Deprecated('Use qualityLabel')
|
||||||
|
String get videoQualityLabel => qualityLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final String qualityLabel = root.getT<String>('qualityLabel') ??
|
||||||
|
'tiny'; // Not sure if 'tiny' is the correct placeholder.
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final int? videoWidth = root.getT<int>('width');
|
late final int? videoWidth = root.getT<int>('width');
|
||||||
|
|
||||||
late final bool isAudioOnly = mimeType?.type == 'audio';
|
late final bool isAudioOnly = codec.type == 'audio';
|
||||||
|
|
||||||
late final MediaType? mimeType = _getMimeType();
|
@override
|
||||||
|
late final MediaType codec = _getMimeType()!;
|
||||||
|
|
||||||
MediaType? _getMimeType() {
|
MediaType? _getMimeType() {
|
||||||
var mime = root.getT<String>('mimeType');
|
var mime = root.getT<String>('mimeType');
|
||||||
|
@ -223,7 +229,7 @@ class _StreamInfo extends StreamInfoProvider {
|
||||||
return MediaType.parse(mime);
|
return MediaType.parse(mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final String? codecs = mimeType?.parameters['codecs']?.toLowerCase();
|
late final String? codecs = codec.parameters['codecs']?.toLowerCase();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final String? audioCodec =
|
late final String? audioCodec =
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
import '../../reverse_engineering/models/fragment.dart';
|
import '../../reverse_engineering/models/fragment.dart';
|
||||||
import 'streams.dart';
|
import 'streams.dart';
|
||||||
|
|
||||||
|
@ -10,8 +12,11 @@ class AudioOnlyStreamInfo extends AudioStreamInfo {
|
||||||
FileSize size,
|
FileSize size,
|
||||||
Bitrate bitrate,
|
Bitrate bitrate,
|
||||||
String audioCodec,
|
String audioCodec,
|
||||||
List<Fragment> fragments)
|
List<Fragment> fragments,
|
||||||
: super(tag, url, container, size, bitrate, audioCodec, fragments);
|
MediaType codec,
|
||||||
|
String qualityLabel)
|
||||||
|
: super(tag, url, container, size, bitrate, audioCodec, fragments, codec,
|
||||||
|
qualityLabel);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'Audio-only ($tag | $container)';
|
String toString() => 'Audio-only ($tag | $container)';
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
import '../../reverse_engineering/models/fragment.dart';
|
import '../../reverse_engineering/models/fragment.dart';
|
||||||
import 'streams.dart';
|
import 'streams.dart';
|
||||||
|
|
||||||
|
@ -7,7 +9,16 @@ abstract class AudioStreamInfo extends StreamInfo {
|
||||||
final String audioCodec;
|
final String audioCodec;
|
||||||
|
|
||||||
///
|
///
|
||||||
AudioStreamInfo(int tag, Uri url, StreamContainer container, FileSize size,
|
AudioStreamInfo(
|
||||||
Bitrate bitrate, this.audioCodec, List<Fragment> fragments)
|
int tag,
|
||||||
: super(tag, url, container, size, bitrate, fragments);
|
Uri url,
|
||||||
|
StreamContainer container,
|
||||||
|
FileSize size,
|
||||||
|
Bitrate bitrate,
|
||||||
|
this.audioCodec,
|
||||||
|
List<Fragment> fragments,
|
||||||
|
MediaType codec,
|
||||||
|
String qualityLabel)
|
||||||
|
: super(
|
||||||
|
tag, url, container, size, bitrate, fragments, codec, qualityLabel);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Framerate with Comparable<Framerate>, _$Framerate {
|
||||||
bool operator <(Framerate other) => framesPerSecond < other.framesPerSecond;
|
bool operator <(Framerate other) => framesPerSecond < other.framesPerSecond;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$framesPerSecond FPS';
|
String toString() => '${framesPerSecond}fps';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int compareTo(Framerate other) =>
|
int compareTo(Framerate other) =>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:http_parser/src/media_type.dart';
|
||||||
|
|
||||||
import '../../reverse_engineering/models/fragment.dart';
|
import '../../reverse_engineering/models/fragment.dart';
|
||||||
import 'audio_stream_info.dart';
|
import 'audio_stream_info.dart';
|
||||||
import 'bitrate.dart';
|
import 'bitrate.dart';
|
||||||
|
@ -32,6 +34,7 @@ class MuxedStreamInfo implements AudioStreamInfo, VideoStreamInfo {
|
||||||
final String videoCodec;
|
final String videoCodec;
|
||||||
|
|
||||||
/// Video quality label, as seen on YouTube.
|
/// Video quality label, as seen on YouTube.
|
||||||
|
@Deprecated('Use qualityLabel')
|
||||||
@override
|
@override
|
||||||
final String videoQualityLabel;
|
final String videoQualityLabel;
|
||||||
|
|
||||||
|
@ -51,6 +54,14 @@ class MuxedStreamInfo implements AudioStreamInfo, VideoStreamInfo {
|
||||||
@override
|
@override
|
||||||
List<Fragment> get fragments => const [];
|
List<Fragment> get fragments => const [];
|
||||||
|
|
||||||
|
/// Stream codec.
|
||||||
|
@override
|
||||||
|
final MediaType codec;
|
||||||
|
|
||||||
|
/// Stream codec.
|
||||||
|
@override
|
||||||
|
final String qualityLabel;
|
||||||
|
|
||||||
/// Initializes an instance of [MuxedStreamInfo]
|
/// Initializes an instance of [MuxedStreamInfo]
|
||||||
MuxedStreamInfo(
|
MuxedStreamInfo(
|
||||||
this.tag,
|
this.tag,
|
||||||
|
@ -63,7 +74,9 @@ class MuxedStreamInfo implements AudioStreamInfo, VideoStreamInfo {
|
||||||
this.videoQualityLabel,
|
this.videoQualityLabel,
|
||||||
this.videoQuality,
|
this.videoQuality,
|
||||||
this.videoResolution,
|
this.videoResolution,
|
||||||
this.framerate);
|
this.framerate,
|
||||||
|
this.codec,
|
||||||
|
this.qualityLabel);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'Muxed ($tag | $videoQualityLabel | $container)';
|
String toString() => 'Muxed ($tag | $videoQualityLabel | $container)';
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
import '../../reverse_engineering/models/fragment.dart';
|
import '../../reverse_engineering/models/fragment.dart';
|
||||||
|
import '../videos.dart';
|
||||||
import 'bitrate.dart';
|
import 'bitrate.dart';
|
||||||
import 'filesize.dart';
|
import 'filesize.dart';
|
||||||
import 'stream_container.dart';
|
import 'stream_container.dart';
|
||||||
|
@ -24,9 +27,15 @@ abstract class StreamInfo {
|
||||||
/// DASH streams contain multiple stream fragments.
|
/// DASH streams contain multiple stream fragments.
|
||||||
final List<Fragment> fragments;
|
final List<Fragment> fragments;
|
||||||
|
|
||||||
|
/// Streams codec.
|
||||||
|
final MediaType codec;
|
||||||
|
|
||||||
|
/// Stream quality label.
|
||||||
|
final String qualityLabel;
|
||||||
|
|
||||||
/// Initialize an instance of [StreamInfo].
|
/// Initialize an instance of [StreamInfo].
|
||||||
StreamInfo(this.tag, this.url, this.container, this.size, this.bitrate,
|
StreamInfo(this.tag, this.url, this.container, this.size, this.bitrate,
|
||||||
this.fragments);
|
this.fragments, this.codec, this.qualityLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extension for Iterables of StreamInfo.
|
/// Extension for Iterables of StreamInfo.
|
||||||
|
@ -38,4 +47,58 @@ extension StreamInfoIterableExt<T extends StreamInfo> on Iterable<T> {
|
||||||
/// This returns new list without editing the original list.
|
/// This returns new list without editing the original list.
|
||||||
List<T> sortByBitrate() =>
|
List<T> sortByBitrate() =>
|
||||||
toList()..sort((a, b) => a.bitrate.compareTo(b.bitrate));
|
toList()..sort((a, b) => a.bitrate.compareTo(b.bitrate));
|
||||||
|
|
||||||
|
/// Print a formatted text of all the streams. Like youtube-dl -F option.
|
||||||
|
String describe() {
|
||||||
|
final column = _Column(['format code', 'extension', 'resolution', 'note']);
|
||||||
|
for (final e in this) {
|
||||||
|
column.write([
|
||||||
|
e.tag,
|
||||||
|
e.container.name,
|
||||||
|
if (e is VideoStreamInfo) e.videoResolution else 'audio only',
|
||||||
|
e.qualityLabel,
|
||||||
|
e.bitrate,
|
||||||
|
e.codec.parameters['codecs'],
|
||||||
|
if (e is VideoStreamInfo) e.framerate,
|
||||||
|
if (e is VideoStreamInfo) 'video only',
|
||||||
|
e.size
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return column.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility for [StreamInfoIterableExt.describe]
|
||||||
|
class _Column {
|
||||||
|
final List<String> header;
|
||||||
|
final List<List<String>> _values = [];
|
||||||
|
|
||||||
|
_Column(this.header);
|
||||||
|
|
||||||
|
void write(List<Object?> value) => _values
|
||||||
|
.add(value.where((e) => e != null).map((e) => e.toString()).toList());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final headerLen = <int>[];
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (final e in header) {
|
||||||
|
headerLen.add(e.length + 2);
|
||||||
|
buffer.write('$e ');
|
||||||
|
}
|
||||||
|
buffer.writeln();
|
||||||
|
|
||||||
|
for (final valueList in _values) {
|
||||||
|
for (var i = 0; i < valueList.length; i++) {
|
||||||
|
final v = valueList[i];
|
||||||
|
if (headerLen.length <= i) {
|
||||||
|
buffer.write(', $v');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buffer.write(v.padRight(headerLen[i]));
|
||||||
|
}
|
||||||
|
buffer.writeln();
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,22 +16,28 @@ class StreamManifest {
|
||||||
|
|
||||||
/// Gets streams that contain audio
|
/// Gets streams that contain audio
|
||||||
/// (which includes muxed and audio-only streams).
|
/// (which includes muxed and audio-only streams).
|
||||||
Iterable<AudioStreamInfo> get audio => streams.whereType<AudioStreamInfo>();
|
late final UnmodifiableListView<AudioStreamInfo> audio =
|
||||||
|
UnmodifiableListView(streams.whereType<AudioStreamInfo>());
|
||||||
|
|
||||||
/// Gets streams that contain video
|
/// Gets streams that contain video
|
||||||
/// (which includes muxed and video-only streams).
|
/// (which includes muxed and video-only streams).
|
||||||
Iterable<VideoStreamInfo> get video => streams.whereType<VideoStreamInfo>();
|
late final UnmodifiableListView<VideoStreamInfo> video =
|
||||||
|
UnmodifiableListView(streams.whereType<VideoStreamInfo>());
|
||||||
|
|
||||||
/// Gets muxed streams (contain both audio and video).
|
/// Gets muxed streams (contain both audio and video).
|
||||||
/// Note that muxed streams are limited in quality and don't go beyond 720p30.
|
/// Note that muxed streams are limited in quality and don't go beyond 720p30.
|
||||||
Iterable<MuxedStreamInfo> get muxed => streams.whereType<MuxedStreamInfo>();
|
late final UnmodifiableListView<MuxedStreamInfo> muxed =
|
||||||
|
UnmodifiableListView(streams.whereType<MuxedStreamInfo>());
|
||||||
|
|
||||||
/// Gets audio-only streams (no video).
|
/// Gets audio-only streams (no video).
|
||||||
Iterable<AudioOnlyStreamInfo> get audioOnly =>
|
late final UnmodifiableListView<AudioOnlyStreamInfo> audioOnly =
|
||||||
streams.whereType<AudioOnlyStreamInfo>();
|
UnmodifiableListView(streams.whereType<AudioOnlyStreamInfo>());
|
||||||
|
|
||||||
/// Gets video-only streams (no audio).
|
/// Gets video-only streams (no audio).
|
||||||
/// These streams have the widest range of qualities available.
|
/// These streams have the widest range of qualities available.
|
||||||
Iterable<VideoOnlyStreamInfo> get videoOnly =>
|
late final UnmodifiableListView<VideoOnlyStreamInfo> videoOnly =
|
||||||
streams.whereType<VideoOnlyStreamInfo>();
|
UnmodifiableListView(streams.whereType<VideoOnlyStreamInfo>());
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => streams.describe();
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,51 +36,6 @@ class StreamsClient {
|
||||||
return DashManifest.get(_httpClient, dashManifestUrl);
|
return DashManifest.get(_httpClient, dashManifestUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not used anymore since Youtube removed the `video_info` endpoint.
|
|
||||||
/* Future<StreamContext> _getStreamContextFromVideoInfo(VideoId videoId) async {
|
|
||||||
var embedPage = await EmbedPage.get(_httpClient, videoId.toString());
|
|
||||||
var playerConfig = embedPage.playerConfig;
|
|
||||||
if (playerConfig == null) {
|
|
||||||
throw VideoUnplayableException.unplayable(videoId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerSource = await PlayerSource.get(
|
|
||||||
_httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl);
|
|
||||||
var cipherOperations = playerSource.getCipherOperations();
|
|
||||||
|
|
||||||
var videoInfoResponse = await VideoInfoClient.get(
|
|
||||||
_httpClient, videoId.toString(), playerSource.sts);
|
|
||||||
var playerResponse = videoInfoResponse.playerResponse;
|
|
||||||
|
|
||||||
var previewVideoId = playerResponse.previewVideoId;
|
|
||||||
if (!previewVideoId.isNullOrWhiteSpace) {
|
|
||||||
throw VideoRequiresPurchaseException.preview(
|
|
||||||
videoId, VideoId(previewVideoId!));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!playerResponse.isVideoPlayable) {
|
|
||||||
throw VideoUnplayableException.unplayable(videoId,
|
|
||||||
reason: playerResponse.videoPlayabilityError ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerResponse.isLive) {
|
|
||||||
throw VideoUnplayableException.liveStream(videoId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var streamInfoProviders = <StreamInfoProvider>[
|
|
||||||
...videoInfoResponse.streams,
|
|
||||||
...playerResponse.streams
|
|
||||||
];
|
|
||||||
|
|
||||||
var dashManifestUrl = playerResponse.dashManifestUrl;
|
|
||||||
if (!dashManifestUrl.isNullOrWhiteSpace) {
|
|
||||||
var dashManifest =
|
|
||||||
await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations);
|
|
||||||
streamInfoProviders.addAll(dashManifest.streams);
|
|
||||||
}
|
|
||||||
return StreamContext(streamInfoProviders, cipherOperations);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
Future<StreamContext> _getStreamContextFromEmbeddedClient(
|
Future<StreamContext> _getStreamContextFromEmbeddedClient(
|
||||||
VideoId videoId) async {
|
VideoId videoId) async {
|
||||||
final page = await EmbeddedPlayerClient.get(_httpClient, videoId.value);
|
final page = await EmbeddedPlayerClient.get(_httpClient, videoId.value);
|
||||||
|
@ -196,7 +151,9 @@ class StreamsClient {
|
||||||
videoQualityLabel,
|
videoQualityLabel,
|
||||||
videoQuality,
|
videoQuality,
|
||||||
videoResolution,
|
videoResolution,
|
||||||
framerate);
|
framerate,
|
||||||
|
streamInfo.codec,
|
||||||
|
streamInfo.qualityLabel);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,13 +169,23 @@ class StreamsClient {
|
||||||
videoQuality,
|
videoQuality,
|
||||||
videoResolution,
|
videoResolution,
|
||||||
framerate,
|
framerate,
|
||||||
streamInfo.fragments ?? const []);
|
streamInfo.fragments ?? const [],
|
||||||
|
streamInfo.codec,
|
||||||
|
streamInfo.qualityLabel);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Audio-only
|
// Audio-only
|
||||||
if (!audioCodec.isNullOrWhiteSpace) {
|
if (!audioCodec.isNullOrWhiteSpace) {
|
||||||
streams[tag] = AudioOnlyStreamInfo(tag, url, container, fileSize,
|
streams[tag] = AudioOnlyStreamInfo(
|
||||||
bitrate, audioCodec!, streamInfo.fragments ?? const []);
|
tag,
|
||||||
|
url,
|
||||||
|
container,
|
||||||
|
fileSize,
|
||||||
|
bitrate,
|
||||||
|
audioCodec!,
|
||||||
|
streamInfo.fragments ?? const [],
|
||||||
|
streamInfo.codec,
|
||||||
|
streamInfo.qualityLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #if DEBUG
|
// #if DEBUG
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
import '../../reverse_engineering/models/fragment.dart';
|
import '../../reverse_engineering/models/fragment.dart';
|
||||||
import 'bitrate.dart';
|
import 'bitrate.dart';
|
||||||
import 'filesize.dart';
|
import 'filesize.dart';
|
||||||
|
@ -20,9 +22,23 @@ class VideoOnlyStreamInfo extends VideoStreamInfo {
|
||||||
VideoQuality videoQuality,
|
VideoQuality videoQuality,
|
||||||
VideoResolution videoResolution,
|
VideoResolution videoResolution,
|
||||||
Framerate framerate,
|
Framerate framerate,
|
||||||
List<Fragment> fragments)
|
List<Fragment> fragments,
|
||||||
: super(tag, url, container, size, bitrate, videoCodec, videoQualityLabel,
|
MediaType codec,
|
||||||
videoQuality, videoResolution, framerate, fragments);
|
String qualityLabel)
|
||||||
|
: super(
|
||||||
|
tag,
|
||||||
|
url,
|
||||||
|
container,
|
||||||
|
size,
|
||||||
|
bitrate,
|
||||||
|
videoCodec,
|
||||||
|
videoQualityLabel,
|
||||||
|
videoQuality,
|
||||||
|
videoResolution,
|
||||||
|
framerate,
|
||||||
|
fragments,
|
||||||
|
codec,
|
||||||
|
qualityLabel);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'Video-only ($tag | $videoResolution | $container)';
|
String toString() => 'Video-only ($tag | $videoResolution | $container)';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/// Width and height of a video.
|
/// Width and height of a video.
|
||||||
class VideoResolution {
|
class VideoResolution implements Comparable<VideoResolution> {
|
||||||
/// Viewport width.
|
/// Viewport width.
|
||||||
final int width;
|
final int width;
|
||||||
|
|
||||||
|
@ -11,4 +11,20 @@ class VideoResolution {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '${width}x$height';
|
String toString() => '${width}x$height';
|
||||||
|
|
||||||
|
@override
|
||||||
|
int compareTo(VideoResolution other) {
|
||||||
|
if (width == other.width && height == other.height) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width > other.width) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width == other.width && height > other.height) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
import '../../reverse_engineering/models/fragment.dart';
|
import '../../reverse_engineering/models/fragment.dart';
|
||||||
import 'streams.dart';
|
import 'streams.dart';
|
||||||
|
|
||||||
|
@ -7,6 +9,7 @@ abstract class VideoStreamInfo extends StreamInfo {
|
||||||
final String videoCodec;
|
final String videoCodec;
|
||||||
|
|
||||||
/// Video quality label, as seen on YouTube.
|
/// Video quality label, as seen on YouTube.
|
||||||
|
@Deprecated('Use qualityLabel')
|
||||||
final String videoQualityLabel;
|
final String videoQualityLabel;
|
||||||
|
|
||||||
/// Video quality.
|
/// Video quality.
|
||||||
|
@ -30,8 +33,11 @@ abstract class VideoStreamInfo extends StreamInfo {
|
||||||
this.videoQuality,
|
this.videoQuality,
|
||||||
this.videoResolution,
|
this.videoResolution,
|
||||||
this.framerate,
|
this.framerate,
|
||||||
List<Fragment> fragments)
|
List<Fragment> fragments,
|
||||||
: super(tag, url, container, size, bitrate, fragments);
|
MediaType codec,
|
||||||
|
String qualityLabel)
|
||||||
|
: super(
|
||||||
|
tag, url, container, size, bitrate, fragments, codec, qualityLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extensions for Iterables of [VideoStreamInfo]
|
/// Extensions for Iterables of [VideoStreamInfo]
|
||||||
|
@ -55,5 +61,5 @@ extension VideoStreamInfoExtension<T extends VideoStreamInfo> on Iterable<T> {
|
||||||
/// This returns new list without editing the original list.
|
/// This returns new list without editing the original list.
|
||||||
List<T> sortByVideoQuality() => toList()
|
List<T> sortByVideoQuality() => toList()
|
||||||
..sort((a, b) => b.framerate.compareTo(a.framerate))
|
..sort((a, b) => b.framerate.compareTo(a.framerate))
|
||||||
..sort((a, b) => b.videoQuality.index.compareTo(a.videoQuality.index));
|
..sort((a, b) => b.videoResolution.compareTo(a.videoResolution));
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,20 @@ class VideoClient {
|
||||||
/// Queries related to media streams of YouTube videos.
|
/// Queries related to media streams of YouTube videos.
|
||||||
final StreamsClient streamsClient;
|
final StreamsClient streamsClient;
|
||||||
|
|
||||||
|
/// Queries related to media streams of YouTube videos.
|
||||||
|
/// Alias of [streamsClient].
|
||||||
|
StreamsClient get streams => streamsClient;
|
||||||
|
|
||||||
/// Queries related to closed captions of YouTube videos.
|
/// Queries related to closed captions of YouTube videos.
|
||||||
final ClosedCaptionClient closedCaptions;
|
final ClosedCaptionClient closedCaptions;
|
||||||
|
|
||||||
/// Queries related to a YouTube video.
|
/// Queries related to a YouTube video comments.
|
||||||
final CommentsClient commentsClient;
|
final CommentsClient commentsClient;
|
||||||
|
|
||||||
|
/// Queries related to a YouTube video comments.
|
||||||
|
/// Alias of [commentsClient].
|
||||||
|
CommentsClient get comments => commentsClient;
|
||||||
|
|
||||||
/// Initializes an instance of [VideoClient].
|
/// Initializes an instance of [VideoClient].
|
||||||
VideoClient(this._httpClient)
|
VideoClient(this._httpClient)
|
||||||
: streamsClient = StreamsClient(_httpClient),
|
: streamsClient = StreamsClient(_httpClient),
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
library youtube_explode.base;
|
library youtube_explode.base;
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
import 'channels/channels.dart';
|
import 'channels/channels.dart';
|
||||||
import 'playlists/playlist_client.dart';
|
import 'playlists/playlist_client.dart';
|
||||||
import 'reverse_engineering/youtube_http_client.dart';
|
import 'reverse_engineering/youtube_http_client.dart';
|
||||||
|
@ -10,8 +8,7 @@ import 'videos/video_client.dart';
|
||||||
|
|
||||||
/// Library entry point.
|
/// Library entry point.
|
||||||
class YoutubeExplode {
|
class YoutubeExplode {
|
||||||
@visibleForTesting
|
final YoutubeHttpClient _httpClient;
|
||||||
final YoutubeHttpClient httpClient;
|
|
||||||
|
|
||||||
/// Queries related to YouTube videos.
|
/// Queries related to YouTube videos.
|
||||||
late final VideoClient videos;
|
late final VideoClient videos;
|
||||||
|
@ -27,14 +24,14 @@ class YoutubeExplode {
|
||||||
|
|
||||||
/// Initializes an instance of [YoutubeClient].
|
/// Initializes an instance of [YoutubeClient].
|
||||||
YoutubeExplode([YoutubeHttpClient? httpClient])
|
YoutubeExplode([YoutubeHttpClient? httpClient])
|
||||||
: httpClient = httpClient ?? YoutubeHttpClient() {
|
: _httpClient = httpClient ?? YoutubeHttpClient() {
|
||||||
videos = VideoClient(this.httpClient);
|
videos = VideoClient(_httpClient);
|
||||||
playlists = PlaylistClient(this.httpClient);
|
playlists = PlaylistClient(_httpClient);
|
||||||
channels = ChannelClient(this.httpClient);
|
channels = ChannelClient(_httpClient);
|
||||||
search = SearchClient(this.httpClient);
|
search = SearchClient(_httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Closes the HttpClient assigned to this [YoutubeHttpClient].
|
/// Closes the HttpClient assigned to this [YoutubeHttpClient].
|
||||||
/// Should be called after this is not used anymore.
|
/// Should be called after this is not used anymore.
|
||||||
void close() => httpClient.close();
|
void close() => _httpClient.close();
|
||||||
}
|
}
|
||||||
|
|
12
pubspec.yaml
12
pubspec.yaml
|
@ -1,6 +1,6 @@
|
||||||
name: youtube_explode_dart
|
name: youtube_explode_dart
|
||||||
description: A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
|
description: A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
|
||||||
version: 1.10.7+1
|
version: 1.10.8
|
||||||
|
|
||||||
homepage: https://github.com/Hexer10/youtube_explode_dart
|
homepage: https://github.com/Hexer10/youtube_explode_dart
|
||||||
|
|
||||||
|
@ -18,10 +18,10 @@ dependencies:
|
||||||
xml: ^5.0.2
|
xml: ^5.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.0.6
|
build_runner: ^2.1.4
|
||||||
console: ^4.1.0
|
console: ^4.1.0
|
||||||
freezed: ^0.14.3
|
freezed: ^0.14.5
|
||||||
grinder: ^0.9.0
|
grinder: ^0.9.0
|
||||||
json_serializable: ^5.0.0
|
json_serializable: ^5.0.2
|
||||||
lint: ^1.5.3
|
lint: ^1.7.2
|
||||||
test: ^1.17.10
|
test: ^1.18.2
|
||||||
|
|
|
@ -50,7 +50,7 @@ void main() {
|
||||||
.getUploads(ChannelId(
|
.getUploads(ChannelId(
|
||||||
'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ'))
|
'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ'))
|
||||||
.toList();
|
.toList();
|
||||||
expect(videos.length, greaterThanOrEqualTo(79));
|
expect(videos.length, greaterThanOrEqualTo(75));
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Get the videos of any youtube channel', () {
|
group('Get the videos of any youtube channel', () {
|
||||||
|
|
|
@ -42,9 +42,7 @@ void main() {
|
||||||
|
|
||||||
test('Stream of age-limited video throws VideoUnplayableException', () {
|
test('Stream of age-limited video throws VideoUnplayableException', () {
|
||||||
expect(yt!.videos.streamsClient.getManifest(VideoId('SkRSXFQerZs')),
|
expect(yt!.videos.streamsClient.getManifest(VideoId('SkRSXFQerZs')),
|
||||||
throwsA(const TypeMatcher<VideoUnplayableException>()),
|
throwsA(const TypeMatcher<VideoUnplayableException>()));
|
||||||
skip:
|
|
||||||
'Seems that this is not consistent with the CI - There is can retrieve a StreamManifest.');
|
|
||||||
});
|
});
|
||||||
test('Get the hls manifest of a live stream', () async {
|
test('Get the hls manifest of a live stream', () async {
|
||||||
expect(
|
expect(
|
||||||
|
@ -65,8 +63,18 @@ void main() {
|
||||||
|
|
||||||
group('Get specific stream of any playable video', () {
|
group('Get specific stream of any playable video', () {
|
||||||
for (final val in {
|
for (final val in {
|
||||||
|
VideoId('9bZkp7q19f0'), //Normal
|
||||||
|
VideoId('rsAAeyAr-9Y'), //LiveStreamRecording
|
||||||
|
VideoId('V5Fsj_sCKdg'), //ContainsHighQualityStreams
|
||||||
VideoId('AI7ULzgf8RU'), //ContainsDashManifest
|
VideoId('AI7ULzgf8RU'), //ContainsDashManifest
|
||||||
|
VideoId('-xNN-bJQ4vI'), //Omnidirectional
|
||||||
|
VideoId('vX2vsvdq8nw'), //HighDynamicRange
|
||||||
|
VideoId('YltHGKX80Y8'), //ContainsClosedCaptions
|
||||||
|
VideoId('_kmeFXjjGfk'), //EmbedRestrictedByYouTube
|
||||||
|
VideoId('MeJVWBSsPAY'), //EmbedRestrictedByAuthor
|
||||||
|
VideoId('hySoCSoH-g8'), //AgeRestrictedEmbedRestricted
|
||||||
VideoId('5VGm0dczmHc'), //RatingDisabled
|
VideoId('5VGm0dczmHc'), //RatingDisabled
|
||||||
|
VideoId('-xNN-bJQ4vI'), // 360° video
|
||||||
}) {
|
}) {
|
||||||
test('VideoId - ${val.value}', () async {
|
test('VideoId - ${val.value}', () async {
|
||||||
var manifest = await yt!.videos.streamsClient.getManifest(val);
|
var manifest = await yt!.videos.streamsClient.getManifest(val);
|
||||||
|
|
Loading…
Reference in New Issue