Implement .describe() and aliases.

This commit is contained in:
Mattia 2021-10-04 13:00:22 +02:00
parent a1191fb31f
commit d3316ca220
21 changed files with 261 additions and 111 deletions

View File

@ -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.

View File

@ -13,3 +13,4 @@ linter:
avoid_escaping_inner_quotes: false
prefer_const_constructors: true
avoid_positional_boolean_parameters: false
require_trailing_commas: false

View File

@ -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 =

View File

@ -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 {

View File

@ -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;

View File

@ -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 =

View File

@ -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)';

View File

@ -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);
}

View File

@ -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) =>

View File

@ -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)';

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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

View File

@ -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)';

View File

@ -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;
}
}

View File

@ -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));
}

View File

@ -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),

View File

@ -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();
}

View File

@ -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

View File

@ -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', () {

View File

@ -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);