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
|
||||
- Fix tests.
|
||||
- Remove debug leftovers.
|
||||
|
|
|
@ -13,3 +13,4 @@ linter:
|
|||
avoid_escaping_inner_quotes: false
|
||||
prefer_const_constructors: true
|
||||
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');
|
||||
|
||||
@override
|
||||
late final String? container = mimeType?.subtype;
|
||||
late final String container = codec.subtype;
|
||||
|
||||
@override
|
||||
late final int? contentLength = int.tryParse(
|
||||
|
@ -129,14 +129,20 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
late final int? videoHeight = root.getT<int>('height');
|
||||
|
||||
@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
|
||||
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() {
|
||||
var mime = root.getT<String>('mimeType');
|
||||
|
@ -146,7 +152,7 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
return MediaType.parse(mime);
|
||||
}
|
||||
|
||||
late final String? codecs = mimeType?.parameters['codecs']?.toLowerCase();
|
||||
late final String? codecs = codec.parameters['codecs']?.toLowerCase();
|
||||
|
||||
@override
|
||||
late final String? audioCodec =
|
||||
|
|
|
@ -204,17 +204,25 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
final String url;
|
||||
|
||||
@override
|
||||
String get container => _mimetype.subtype;
|
||||
|
||||
final MediaType _mimetype;
|
||||
|
||||
bool get isAudioOnly => _mimetype.type == 'audio';
|
||||
final MediaType codec;
|
||||
|
||||
@override
|
||||
String? get audioCodec => isAudioOnly ? _mimetype.subtype : null;
|
||||
String get container => codec.subtype;
|
||||
|
||||
bool get isAudioOnly => codec.type == 'audio';
|
||||
|
||||
@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
|
||||
final int? videoWidth;
|
||||
|
@ -231,8 +239,8 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
@override
|
||||
StreamSource get source => StreamSource.dash;
|
||||
|
||||
_StreamInfo(this.tag, this.url, this._mimetype, this.videoWidth,
|
||||
this.videoHeight, this.framerate, this.fragments);
|
||||
_StreamInfo(this.tag, this.url, this.codec, this.videoWidth, this.videoHeight,
|
||||
this.framerate, this.fragments);
|
||||
}
|
||||
|
||||
class _SegmentTimeline {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import 'fragment.dart';
|
||||
|
||||
enum StreamSource { muxed, adaptive, dash }
|
||||
|
@ -16,6 +18,8 @@ abstract class StreamInfoProvider {
|
|||
///
|
||||
String get url;
|
||||
|
||||
MediaType get codec;
|
||||
|
||||
///
|
||||
String? get signature => null;
|
||||
|
||||
|
@ -38,8 +42,12 @@ abstract class StreamInfoProvider {
|
|||
String? get videoCodec => null;
|
||||
|
||||
///
|
||||
@Deprecated('Use qualityLabel')
|
||||
String? get videoQualityLabel => null;
|
||||
|
||||
///
|
||||
String get qualityLabel;
|
||||
|
||||
///
|
||||
int? get videoWidth => null;
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
late final int? bitrate = root.getT<int>('bitrate');
|
||||
|
||||
@override
|
||||
late final String? container = mimeType?.subtype;
|
||||
late final String container = codec.subtype;
|
||||
|
||||
@override
|
||||
late final int? contentLength = int.tryParse(
|
||||
|
@ -206,14 +206,20 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
late final int? videoHeight = root.getT<int>('height');
|
||||
|
||||
@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
|
||||
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() {
|
||||
var mime = root.getT<String>('mimeType');
|
||||
|
@ -223,7 +229,7 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
return MediaType.parse(mime);
|
||||
}
|
||||
|
||||
late final String? codecs = mimeType?.parameters['codecs']?.toLowerCase();
|
||||
late final String? codecs = codec.parameters['codecs']?.toLowerCase();
|
||||
|
||||
@override
|
||||
late final String? audioCodec =
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import '../../reverse_engineering/models/fragment.dart';
|
||||
import 'streams.dart';
|
||||
|
||||
|
@ -10,8 +12,11 @@ class AudioOnlyStreamInfo extends AudioStreamInfo {
|
|||
FileSize size,
|
||||
Bitrate bitrate,
|
||||
String audioCodec,
|
||||
List<Fragment> fragments)
|
||||
: super(tag, url, container, size, bitrate, audioCodec, fragments);
|
||||
List<Fragment> fragments,
|
||||
MediaType codec,
|
||||
String qualityLabel)
|
||||
: super(tag, url, container, size, bitrate, audioCodec, fragments, codec,
|
||||
qualityLabel);
|
||||
|
||||
@override
|
||||
String toString() => 'Audio-only ($tag | $container)';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import '../../reverse_engineering/models/fragment.dart';
|
||||
import 'streams.dart';
|
||||
|
||||
|
@ -7,7 +9,16 @@ abstract class AudioStreamInfo extends StreamInfo {
|
|||
final String audioCodec;
|
||||
|
||||
///
|
||||
AudioStreamInfo(int tag, Uri url, StreamContainer container, FileSize size,
|
||||
Bitrate bitrate, this.audioCodec, List<Fragment> fragments)
|
||||
: super(tag, url, container, size, bitrate, fragments);
|
||||
AudioStreamInfo(
|
||||
int tag,
|
||||
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;
|
||||
|
||||
@override
|
||||
String toString() => '$framesPerSecond FPS';
|
||||
String toString() => '${framesPerSecond}fps';
|
||||
|
||||
@override
|
||||
int compareTo(Framerate other) =>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:http_parser/src/media_type.dart';
|
||||
|
||||
import '../../reverse_engineering/models/fragment.dart';
|
||||
import 'audio_stream_info.dart';
|
||||
import 'bitrate.dart';
|
||||
|
@ -32,6 +34,7 @@ class MuxedStreamInfo implements AudioStreamInfo, VideoStreamInfo {
|
|||
final String videoCodec;
|
||||
|
||||
/// Video quality label, as seen on YouTube.
|
||||
@Deprecated('Use qualityLabel')
|
||||
@override
|
||||
final String videoQualityLabel;
|
||||
|
||||
|
@ -51,6 +54,14 @@ class MuxedStreamInfo implements AudioStreamInfo, VideoStreamInfo {
|
|||
@override
|
||||
List<Fragment> get fragments => const [];
|
||||
|
||||
/// Stream codec.
|
||||
@override
|
||||
final MediaType codec;
|
||||
|
||||
/// Stream codec.
|
||||
@override
|
||||
final String qualityLabel;
|
||||
|
||||
/// Initializes an instance of [MuxedStreamInfo]
|
||||
MuxedStreamInfo(
|
||||
this.tag,
|
||||
|
@ -63,7 +74,9 @@ class MuxedStreamInfo implements AudioStreamInfo, VideoStreamInfo {
|
|||
this.videoQualityLabel,
|
||||
this.videoQuality,
|
||||
this.videoResolution,
|
||||
this.framerate);
|
||||
this.framerate,
|
||||
this.codec,
|
||||
this.qualityLabel);
|
||||
|
||||
@override
|
||||
String toString() => 'Muxed ($tag | $videoQualityLabel | $container)';
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import '../../reverse_engineering/models/fragment.dart';
|
||||
import '../videos.dart';
|
||||
import 'bitrate.dart';
|
||||
import 'filesize.dart';
|
||||
import 'stream_container.dart';
|
||||
|
@ -24,9 +27,15 @@ abstract class StreamInfo {
|
|||
/// DASH streams contain multiple stream fragments.
|
||||
final List<Fragment> fragments;
|
||||
|
||||
/// Streams codec.
|
||||
final MediaType codec;
|
||||
|
||||
/// Stream quality label.
|
||||
final String qualityLabel;
|
||||
|
||||
/// Initialize an instance of [StreamInfo].
|
||||
StreamInfo(this.tag, this.url, this.container, this.size, this.bitrate,
|
||||
this.fragments);
|
||||
this.fragments, this.codec, this.qualityLabel);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
List<T> sortByBitrate() =>
|
||||
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
|
||||
/// (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
|
||||
/// (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).
|
||||
/// 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).
|
||||
Iterable<AudioOnlyStreamInfo> get audioOnly =>
|
||||
streams.whereType<AudioOnlyStreamInfo>();
|
||||
late final UnmodifiableListView<AudioOnlyStreamInfo> audioOnly =
|
||||
UnmodifiableListView(streams.whereType<AudioOnlyStreamInfo>());
|
||||
|
||||
/// Gets video-only streams (no audio).
|
||||
/// These streams have the widest range of qualities available.
|
||||
Iterable<VideoOnlyStreamInfo> get videoOnly =>
|
||||
streams.whereType<VideoOnlyStreamInfo>();
|
||||
late final UnmodifiableListView<VideoOnlyStreamInfo> videoOnly =
|
||||
UnmodifiableListView(streams.whereType<VideoOnlyStreamInfo>());
|
||||
|
||||
@override
|
||||
String toString() => streams.describe();
|
||||
}
|
||||
|
|
|
@ -36,51 +36,6 @@ class StreamsClient {
|
|||
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(
|
||||
VideoId videoId) async {
|
||||
final page = await EmbeddedPlayerClient.get(_httpClient, videoId.value);
|
||||
|
@ -196,7 +151,9 @@ class StreamsClient {
|
|||
videoQualityLabel,
|
||||
videoQuality,
|
||||
videoResolution,
|
||||
framerate);
|
||||
framerate,
|
||||
streamInfo.codec,
|
||||
streamInfo.qualityLabel);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -212,13 +169,23 @@ class StreamsClient {
|
|||
videoQuality,
|
||||
videoResolution,
|
||||
framerate,
|
||||
streamInfo.fragments ?? const []);
|
||||
streamInfo.fragments ?? const [],
|
||||
streamInfo.codec,
|
||||
streamInfo.qualityLabel);
|
||||
continue;
|
||||
}
|
||||
// Audio-only
|
||||
if (!audioCodec.isNullOrWhiteSpace) {
|
||||
streams[tag] = AudioOnlyStreamInfo(tag, url, container, fileSize,
|
||||
bitrate, audioCodec!, streamInfo.fragments ?? const []);
|
||||
streams[tag] = AudioOnlyStreamInfo(
|
||||
tag,
|
||||
url,
|
||||
container,
|
||||
fileSize,
|
||||
bitrate,
|
||||
audioCodec!,
|
||||
streamInfo.fragments ?? const [],
|
||||
streamInfo.codec,
|
||||
streamInfo.qualityLabel);
|
||||
}
|
||||
|
||||
// #if DEBUG
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import '../../reverse_engineering/models/fragment.dart';
|
||||
import 'bitrate.dart';
|
||||
import 'filesize.dart';
|
||||
|
@ -20,9 +22,23 @@ class VideoOnlyStreamInfo extends VideoStreamInfo {
|
|||
VideoQuality videoQuality,
|
||||
VideoResolution videoResolution,
|
||||
Framerate framerate,
|
||||
List<Fragment> fragments)
|
||||
: super(tag, url, container, size, bitrate, videoCodec, videoQualityLabel,
|
||||
videoQuality, videoResolution, framerate, fragments);
|
||||
List<Fragment> fragments,
|
||||
MediaType codec,
|
||||
String qualityLabel)
|
||||
: super(
|
||||
tag,
|
||||
url,
|
||||
container,
|
||||
size,
|
||||
bitrate,
|
||||
videoCodec,
|
||||
videoQualityLabel,
|
||||
videoQuality,
|
||||
videoResolution,
|
||||
framerate,
|
||||
fragments,
|
||||
codec,
|
||||
qualityLabel);
|
||||
|
||||
@override
|
||||
String toString() => 'Video-only ($tag | $videoResolution | $container)';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/// Width and height of a video.
|
||||
class VideoResolution {
|
||||
class VideoResolution implements Comparable<VideoResolution> {
|
||||
/// Viewport width.
|
||||
final int width;
|
||||
|
||||
|
@ -11,4 +11,20 @@ class VideoResolution {
|
|||
|
||||
@override
|
||||
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 'streams.dart';
|
||||
|
||||
|
@ -7,6 +9,7 @@ abstract class VideoStreamInfo extends StreamInfo {
|
|||
final String videoCodec;
|
||||
|
||||
/// Video quality label, as seen on YouTube.
|
||||
@Deprecated('Use qualityLabel')
|
||||
final String videoQualityLabel;
|
||||
|
||||
/// Video quality.
|
||||
|
@ -30,8 +33,11 @@ abstract class VideoStreamInfo extends StreamInfo {
|
|||
this.videoQuality,
|
||||
this.videoResolution,
|
||||
this.framerate,
|
||||
List<Fragment> fragments)
|
||||
: super(tag, url, container, size, bitrate, fragments);
|
||||
List<Fragment> fragments,
|
||||
MediaType codec,
|
||||
String qualityLabel)
|
||||
: super(
|
||||
tag, url, container, size, bitrate, fragments, codec, qualityLabel);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
List<T> sortByVideoQuality() => toList()
|
||||
..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.
|
||||
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.
|
||||
final ClosedCaptionClient closedCaptions;
|
||||
|
||||
/// Queries related to a YouTube video.
|
||||
/// Queries related to a YouTube video comments.
|
||||
final CommentsClient commentsClient;
|
||||
|
||||
/// Queries related to a YouTube video comments.
|
||||
/// Alias of [commentsClient].
|
||||
CommentsClient get comments => commentsClient;
|
||||
|
||||
/// Initializes an instance of [VideoClient].
|
||||
VideoClient(this._httpClient)
|
||||
: streamsClient = StreamsClient(_httpClient),
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
library youtube_explode.base;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'channels/channels.dart';
|
||||
import 'playlists/playlist_client.dart';
|
||||
import 'reverse_engineering/youtube_http_client.dart';
|
||||
|
@ -10,8 +8,7 @@ import 'videos/video_client.dart';
|
|||
|
||||
/// Library entry point.
|
||||
class YoutubeExplode {
|
||||
@visibleForTesting
|
||||
final YoutubeHttpClient httpClient;
|
||||
final YoutubeHttpClient _httpClient;
|
||||
|
||||
/// Queries related to YouTube videos.
|
||||
late final VideoClient videos;
|
||||
|
@ -27,14 +24,14 @@ class YoutubeExplode {
|
|||
|
||||
/// Initializes an instance of [YoutubeClient].
|
||||
YoutubeExplode([YoutubeHttpClient? httpClient])
|
||||
: httpClient = httpClient ?? YoutubeHttpClient() {
|
||||
videos = VideoClient(this.httpClient);
|
||||
playlists = PlaylistClient(this.httpClient);
|
||||
channels = ChannelClient(this.httpClient);
|
||||
search = SearchClient(this.httpClient);
|
||||
: _httpClient = httpClient ?? YoutubeHttpClient() {
|
||||
videos = VideoClient(_httpClient);
|
||||
playlists = PlaylistClient(_httpClient);
|
||||
channels = ChannelClient(_httpClient);
|
||||
search = SearchClient(_httpClient);
|
||||
}
|
||||
|
||||
/// Closes the HttpClient assigned to this [YoutubeHttpClient].
|
||||
/// 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
|
||||
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
|
||||
|
||||
|
@ -18,10 +18,10 @@ dependencies:
|
|||
xml: ^5.0.2
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.0.6
|
||||
build_runner: ^2.1.4
|
||||
console: ^4.1.0
|
||||
freezed: ^0.14.3
|
||||
freezed: ^0.14.5
|
||||
grinder: ^0.9.0
|
||||
json_serializable: ^5.0.0
|
||||
lint: ^1.5.3
|
||||
test: ^1.17.10
|
||||
json_serializable: ^5.0.2
|
||||
lint: ^1.7.2
|
||||
test: ^1.18.2
|
||||
|
|
|
@ -50,7 +50,7 @@ void main() {
|
|||
.getUploads(ChannelId(
|
||||
'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ'))
|
||||
.toList();
|
||||
expect(videos.length, greaterThanOrEqualTo(79));
|
||||
expect(videos.length, greaterThanOrEqualTo(75));
|
||||
});
|
||||
|
||||
group('Get the videos of any youtube channel', () {
|
||||
|
|
|
@ -42,9 +42,7 @@ void main() {
|
|||
|
||||
test('Stream of age-limited video throws VideoUnplayableException', () {
|
||||
expect(yt!.videos.streamsClient.getManifest(VideoId('SkRSXFQerZs')),
|
||||
throwsA(const TypeMatcher<VideoUnplayableException>()),
|
||||
skip:
|
||||
'Seems that this is not consistent with the CI - There is can retrieve a StreamManifest.');
|
||||
throwsA(const TypeMatcher<VideoUnplayableException>()));
|
||||
});
|
||||
test('Get the hls manifest of a live stream', () async {
|
||||
expect(
|
||||
|
@ -65,8 +63,18 @@ void main() {
|
|||
|
||||
group('Get specific stream of any playable video', () {
|
||||
for (final val in {
|
||||
VideoId('9bZkp7q19f0'), //Normal
|
||||
VideoId('rsAAeyAr-9Y'), //LiveStreamRecording
|
||||
VideoId('V5Fsj_sCKdg'), //ContainsHighQualityStreams
|
||||
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('-xNN-bJQ4vI'), // 360° video
|
||||
}) {
|
||||
test('VideoId - ${val.value}', () async {
|
||||
var manifest = await yt!.videos.streamsClient.getManifest(val);
|
||||
|
|
Loading…
Reference in New Issue