Version 1.9.0-nullsafety.2

#107
This commit is contained in:
Mattia 2021-03-11 14:20:10 +01:00
parent cdca20011f
commit 42617b5ac0
70 changed files with 961 additions and 43284 deletions

View File

@ -1,3 +1,12 @@
## 1.9.0
- Support nnbd (dart 1.12)
## 1.8.0
- Fixed playlist client.
- Fixed search client.
- `search.getVideos` now returns a `Video` instance.
- Implemented `SearchList`.
## 1.8.0-beta.4 ## 1.8.0-beta.4
- Removed debug message - Removed debug message

View File

@ -1,69 +1,13 @@
include: package:effective_dart/analysis_options.yaml include: package:lint/analysis_options.yaml
analyzer: analyzer:
exclude: #most likely not all of these are needed, but as it is now it works. strong-mode:
- "**/*.g.dart" implicit-casts: true
- /**/*.g.dart implicit-dynamic: true
- \**\*.g.dart
- "*.g.dart"
- "**.g.dart"
- example\**
- lib\src\reverse_engineering\responses\generated\**
linter: linter:
rules: rules:
- valid_regexps prefer_final_locals: false
- prefer_const_constructors parameter_assignments: false
- prefer_const_declarations no_runtimetype_tostring: false
- prefer_const_literals_to_create_immutables avoid_escaping_inner_quotes: false
- prefer_constructors_over_static_methods
- prefer_contains
- annotate_overrides
- await_only_futures
- unawaited_futures
- avoid_empty_else
- avoid_returning_null_for_future
- avoid_types_as_parameter_names
- control_flow_in_finally
- empty_statements
- invariant_booleans
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
- prefer_void_to_null
- test_types_in_equals
- throw_in_finally
- unnecessary_statements
- unrelated_type_equality_checks
- always_declare_return_types
- always_put_control_body_on_new_line
- avoid_returning_null_for_void
- avoid_setters_without_getters
- avoid_shadowing_type_parameters
- avoid_unnecessary_containers
- avoid_void_async
- empty_catches
- null_closures
- prefer_conditional_assignment
- prefer_if_null_operators
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_null_aware_operators
- recursive_getters
- unnecessary_await_in_return
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_string_escapes
- unnecessary_string_interpolations
- use_string_buffers
- void_checks
- package_names
- prefer_single_quotes
- use_function_type_syntax_for_parameters

View File

@ -1,15 +1,14 @@
// ignore_for_file: avoid_print
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
Future<void> main() async { Future<void> main() async {
var yt = YoutubeExplode(); final yt = YoutubeExplode();
var manifest = await yt.videos.closedCaptions var manifest = await yt.videos.closedCaptions.getManifest('Pxgvgh9IFqA');
.getManifest('Pxgvgh9IFqA', autoGenerated: true);
print(manifest.tracks); print(manifest.tracks);
print('\n\n---------------------\n\n'); print('\n\n---------------------\n\n');
manifest = await yt.videos.closedCaptions manifest = await yt.videos.closedCaptions.getManifest('Pxgvgh9IFqA');
.getManifest('Pxgvgh9IFqA', autoGenerated: false);
print(manifest.tracks); print(manifest.tracks);
yt.close(); yt.close();
} }

View File

@ -12,7 +12,7 @@ final yt = YoutubeExplode();
Future<void> main() async { Future<void> main() async {
stdout.writeln('Type the video id or url: '); stdout.writeln('Type the video id or url: ');
var url = stdin.readLineSync().trim(); var url = stdin.readLineSync()!.trim();
// Save the video to the download directory. // Save the video to the download directory.
Directory('downloads').createSync(); Directory('downloads').createSync();
@ -66,7 +66,7 @@ Future<void> download(String id) async {
// Listen for data received. // Listen for data received.
var progressBar = ProgressBar(); var progressBar = ProgressBar();
await for (var data in audioStream) { await for (final data in audioStream) {
// Keep track of the current downloaded data. // Keep track of the current downloaded data.
count += data.length; count += data.length;

View File

@ -49,9 +49,6 @@ class ChannelClient {
Future<ChannelAbout> getAboutPage(dynamic id) async { Future<ChannelAbout> getAboutPage(dynamic id) async {
id = ChannelId.fromString(id); id = ChannelId.fromString(id);
var channelAboutPage = await ChannelAboutPage.get(_httpClient, id.value);
var iData = channelAboutPage.initialData;
assert(iData != null);
return ChannelAbout( return ChannelAbout(
id.description, id.description,
id.viewCount, id.viewCount,
@ -74,7 +71,6 @@ class ChannelClient {
var channelAboutPage = var channelAboutPage =
await ChannelAboutPage.getByUsername(_httpClient, username.value); await ChannelAboutPage.getByUsername(_httpClient, username.value);
var id = channelAboutPage.initialData; var id = channelAboutPage.initialData;
assert(id != null);
return ChannelAbout( return ChannelAbout(
id.description, id.description,
id.viewCount, id.viewCount,
@ -82,7 +78,7 @@ class ChannelClient {
id.title, id.title,
[ [
for (var e in id.avatar) for (var e in id.avatar)
Thumbnail(Uri.parse(e.url), e.height, e.width) Thumbnail(Uri.parse(e['url']), e['height'], e['width'])
], ],
id.country, id.country,
id.channelLinks); id.channelLinks);
@ -118,13 +114,13 @@ class ChannelClient {
Stream<ChannelVideo> getUploadsFromPage(dynamic channelId, Stream<ChannelVideo> getUploadsFromPage(dynamic channelId,
[VideoSorting videoSorting = VideoSorting.newest]) async* { [VideoSorting videoSorting = VideoSorting.newest]) async* {
channelId = ChannelId.fromString(channelId); channelId = ChannelId.fromString(channelId);
var page = await ChannelUploadPage.get( ChannelUploadPage? page = await ChannelUploadPage.get(
_httpClient, channelId.value, videoSorting.code); _httpClient, (channelId as ChannelId).value, videoSorting.code);
yield* Stream.fromIterable(page.initialData.uploads); yield* Stream.fromIterable(page.initialData.uploads);
// ignore: literal_only_boolean_expressions // ignore: literal_only_boolean_expressions
while (true) { while (true) {
page = await page.nextPage(_httpClient); page = await page!.nextPage(_httpClient);
if (page == null) { if (page == null) {
return; return;
} }

View File

@ -9,7 +9,7 @@ class ChannelId with EquatableMixin {
/// Initializes an instance of [ChannelId] /// Initializes an instance of [ChannelId]
ChannelId(String value) : value = parseChannelId(value) ?? '' { ChannelId(String value) : value = parseChannelId(value) ?? '' {
if (value.isEmpty) { if (this.value.isEmpty) {
throw ArgumentError.value(value); throw ArgumentError.value(value);
} }
} }

View File

@ -33,23 +33,3 @@ class Engagement extends Equatable {
@override @override
List<Object?> get props => [viewCount, likeCount, dislikeCount]; List<Object?> get props => [viewCount, likeCount, dislikeCount];
} }
/// User activity statistics.
/// No null types
class SafeEngagement extends Engagement {
@override
final int viewCount;
@override
final int likeCount;
@override
final int dislikeCount;
/// Initializes an instance of [Engagement]
const SafeEngagement(this.viewCount, this.likeCount, this.dislikeCount)
: super(viewCount, likeCount, dislikeCount);
@override
List<Object> get props => [viewCount, likeCount, dislikeCount];
}

View File

@ -22,5 +22,6 @@ Response: (${response.statusCode})
'''; ''';
@override @override
String toString() => '$runtimeType: $message'; String toString() =>
'$runtimeType: $message'; // ignore: no_runtimetype_tostring
} }

View File

@ -23,5 +23,6 @@ Response: $response
'''; ''';
@override @override
String toString() => '$runtimeType: $message'; String toString() =>
'$runtimeType: $message'; // ignore: no_runtimetype_tostring
} }

View File

@ -22,5 +22,6 @@ Response: $response
'''; ''';
@override @override
String toString() => '$runtimeType: $message'; String toString() =>
'$runtimeType: $message'; // ignore: no_runtimetype_tostring
} }

View File

@ -12,10 +12,12 @@ class VideoRequiresPurchaseException implements VideoUnplayableException {
/// Initializes an instance of [VideoRequiresPurchaseException]. /// Initializes an instance of [VideoRequiresPurchaseException].
VideoRequiresPurchaseException.preview(VideoId videoId, this.previewVideoId) VideoRequiresPurchaseException.preview(VideoId videoId, this.previewVideoId)
: message = 'Video `$videoId` is unplayable because it requires purchase.' : message =
'Streams are not available for this video.' 'Video `$videoId` is unplayable because it requires purchase.\n'
'There is a preview video available: `$previewVideoId`.'; 'Streams are not available for this video.\n'
'There is a preview video available: `$previewVideoId`.';
@override @override
String toString() => '$runtimeType: $message'; String toString() =>
'$runtimeType: $message'; // ignore: no_runtimetype_tostring
} }

View File

@ -22,5 +22,6 @@ class VideoUnavailableException implements VideoUnplayableException {
'Please report this issue on GitHub in that case.'; 'Please report this issue on GitHub in that case.';
@override @override
String toString() => '$runtimeType: $message'; String toString() =>
'$runtimeType: $message'; // ignore: no_runtimetype_tostring
} }

View File

@ -30,5 +30,7 @@ class VideoUnplayableException implements YoutubeExplodeException {
'Live stream manifest is not available for this video'; 'Live stream manifest is not available for this video';
@override @override
String toString() => '$runtimeType: $message'; // ignore:
String toString() =>
'$runtimeType: $message'; // ignore: no_runtimetype_tostring
} }

View File

@ -42,6 +42,8 @@ extension StringUtility on String {
} }
return buffer.toString(); return buffer.toString();
} }
DateTime parseDateTime() => DateTime.parse(this);
} }
/// Utility for Strings. /// Utility for Strings.
@ -66,7 +68,7 @@ extension StringUtility2 on String? {
extension ListDecipher on Iterable<CipherOperation> { extension ListDecipher on Iterable<CipherOperation> {
/// Apply every CipherOperation on the [signature] /// Apply every CipherOperation on the [signature]
String decipher(String signature) { String decipher(String signature) {
for (var operation in this) { for (final operation in this) {
signature = operation.decipher(signature); signature = operation.decipher(signature);
} }
@ -76,14 +78,6 @@ extension ListDecipher on Iterable<CipherOperation> {
/// List Utility. /// List Utility.
extension ListUtil<E> on Iterable<E> { extension ListUtil<E> on Iterable<E> {
/// Returns the first element of a list or null if empty.
E? get firstOrNull {
if (length == 0) {
return null;
}
return first;
}
/// Same as [elementAt] but if the index is higher than the length returns /// Same as [elementAt] but if the index is higher than the length returns
/// null /// null
E? elementAtSafe(int index) { E? elementAtSafe(int index) {
@ -92,16 +86,6 @@ extension ListUtil<E> on Iterable<E> {
} }
return elementAt(index); return elementAt(index);
} }
/// Same as [firstWhere] but returns null if no found
E? firstWhereNull(bool Function(E element) test) {
for (final element in this) {
if (test(element)) {
return element;
}
}
return null;
}
} }
/// Uri utility /// Uri utility
@ -180,5 +164,5 @@ extension UriUtils on Uri {
/// Parse properties with `text` method. /// Parse properties with `text` method.
extension RunsParser on List<dynamic> { extension RunsParser on List<dynamic> {
/// ///
String parseRuns() => map((e) => e['text']).join() ?? ''; String parseRuns() => map((e) => e['text']).join();
} }

View File

@ -22,9 +22,9 @@ class PlaylistClient {
var response = await PlaylistPage.get(_httpClient, id.value); var response = await PlaylistPage.get(_httpClient, id.value);
return Playlist( return Playlist(
id, id,
response.initialData.title, response.initialData.title ?? '',
response.initialData.author, response.initialData.author ?? '',
response.initialData.description, response.initialData.description ?? '',
ThumbnailSet(id.value), ThumbnailSet(id.value),
Engagement(response.initialData.viewCount ?? 0, null, null)); Engagement(response.initialData.viewCount ?? 0, null, null));
} }
@ -33,14 +33,14 @@ class PlaylistClient {
Stream<Video> getVideos(dynamic id) async* { Stream<Video> getVideos(dynamic id) async* {
id = PlaylistId.fromString(id); id = PlaylistId.fromString(id);
var encounteredVideoIds = <String>{}; var encounteredVideoIds = <String>{};
var continuationToken = ''; String? continuationToken = '';
// ignore: literal_only_boolean_expressions // ignore: literal_only_boolean_expressions
while (true) { while (true) {
var response = await PlaylistPage.get(_httpClient, id.value, var response = await PlaylistPage.get(_httpClient, id.value,
token: continuationToken); token: continuationToken);
for (var video in response.initialData.playlistVideos) { for (final video in response.initialData.playlistVideos) {
var videoId = video.id; var videoId = video.id;
// Already added // Already added

View File

@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser; import 'package:html/parser.dart' as parser;
@ -6,7 +9,6 @@ import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart'; import '../../extensions/helpers_extension.dart';
import '../../retry.dart'; import '../../retry.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
import 'generated/channel_about_page_id.g.dart';
/// ///
class ChannelAboutPage { class ChannelAboutPage {
@ -25,16 +27,16 @@ class ChannelAboutPage {
(e) => e.contains('window["ytInitialData"] ='), (e) => e.contains('window["ytInitialData"] ='),
orElse: () => ''); orElse: () => '');
if (initialDataText.isNotEmpty) { if (initialDataText.isNotEmpty) {
return _InitialData(ChannelAboutPageId.fromRawJson( return _InitialData(json
_extractJson(initialDataText, 'window["ytInitialData"] ='))); .decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
} }
initialDataText = scriptText.firstWhere( initialDataText = scriptText.firstWhere(
(e) => e.contains('var ytInitialData = '), (e) => e.contains('var ytInitialData = '),
orElse: () => ''); orElse: () => '');
if (initialDataText.isNotEmpty) { if (initialDataText.isNotEmpty) {
return _InitialData(ChannelAboutPageId.fromRawJson( return _InitialData(
_extractJson(initialDataText, 'var ytInitialData = '))); json.decode(_extractJson(initialDataText, 'var ytInitialData = ')));
} }
throw TransientFailureException( throw TransientFailureException(
@ -102,53 +104,69 @@ class ChannelAboutPage {
final _urlExp = RegExp(r'q=([^=]*)$'); final _urlExp = RegExp(r'q=([^=]*)$');
class _InitialData { class _InitialData {
// Json parsed class // Json parsed map
final ChannelAboutPageId root; final Map<String, dynamic> root;
_InitialData(this.root); _InitialData(this.root);
late final Map<String, dynamic> content = _getContentContext();
late final ChannelAboutFullMetadataRenderer content = _getContentContext(); Map<String, dynamic> _getContentContext() {
ChannelAboutFullMetadataRenderer _getContentContext() {
return root return root
.contents .get('contents')!
.twoColumnBrowseResultsRenderer .get('twoColumnBrowseResultsRenderer')!
.tabs[5] .getList('tabs')!
.tabRenderer .elementAtSafe(5)!
.content .get('tabRenderer')!
.sectionListRenderer .get('content')!
.contents .get('sectionListRenderer')!
.first .getList('contents')!
.itemSectionRenderer .firstOrNull!
.contents .get('itemSectionRenderer')!
.first .getList('contents')!
.channelAboutFullMetadataRenderer; .firstOrNull!
.get('channelAboutFullMetadataRenderer')!;
} }
String get description => content.description.simpleText; late final String description =
content.get('description')!.getT<String>('simpleText')!;
List<ChannelLink> get channelLinks { late final List<ChannelLink> channelLinks = content
return content.primaryLinks .getList('primaryLinks')!
.map((e) => ChannelLink( .map((e) => ChannelLink(
e.title.simpleText, e.get('title')?.getT<String>('simpleText') ?? '',
extractUrl(e.navigationEndpoint?.commandMetadata?.webCommandMetadata extractUrl(e
?.url ?? .get('navigationEndpoint')
e.navigationEndpoint.urlEndpoint.url), ?.get('commandMetadata')
Uri.parse(e.icon.thumbnails.first.url))) ?.get('webCommandMetadata')
.toList(); ?.getT<String>('url') ??
} e
.get('navigationEndpoint')
?.get('urlEndpoint')
?.getT<String>('url') ??
''),
Uri.parse(e
.get('icon')
?.getList('thumbnails')
?.firstOrNull
?.getT<String>('url') ??
'')))
.toList();
int get viewCount => late final int viewCount = int.parse(content
int.parse(content.viewCountText.simpleText.stripNonDigits()); .get('viewCountText')!
.getT<String>('simpleText')!
.stripNonDigits());
String get joinDate => content.joinedDateText.runs[1].text; late final String joinDate =
content.get('joinedDateText')!.getList('runs')![1].getT<String>('text')!;
String get title => content.title.simpleText; late final String title = content.get('title')!.getT<String>('simpleText')!;
List<AvatarThumbnail> get avatar => content.avatar.thumbnails; late final List<Map<String, dynamic>> avatar =
content.get('avatar')!.getList('thumbnails')!;
String get country => content.country.simpleText; String get country => content.get('country')!.getT<String>('simpleText')!;
String parseRuns(List<dynamic>? runs) => String parseRuns(List<dynamic>? runs) =>
runs?.map((e) => e.text).join() ?? ''; runs?.map((e) => e.text).join() ?? '';

View File

@ -1,26 +1,31 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser; import 'package:html/parser.dart' as parser;
import '../../channels/channel_video.dart'; import '../../channels/channel_video.dart';
import '../../exceptions/exceptions.dart'; import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart'; import '../../retry.dart';
import '../../videos/videos.dart'; import '../../videos/videos.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
import 'generated/channel_upload_page_id.g.dart';
/// ///
class ChannelUploadPage { class ChannelUploadPage {
/// ///
final String channelId; final String channelId;
final Document _root; final Document? _root;
late final _InitialData _initialData = _getInitialData(); late final _InitialData initialData = _getInitialData();
_InitialData? _initialData;
/// ///
_InitialData _getInitialData() { _InitialData _getInitialData() {
final scriptText = _root if (_initialData != null) {
return _initialData!;
}
final scriptText = _root!
.querySelectorAll('script') .querySelectorAll('script')
.map((e) => e.text) .map((e) => e.text)
.toList(growable: false); .toList(growable: false);
@ -29,16 +34,16 @@ class ChannelUploadPage {
(e) => e.contains('window["ytInitialData"] ='), (e) => e.contains('window["ytInitialData"] ='),
orElse: () => ''); orElse: () => '');
if (initialDataText.isNotEmpty) { if (initialDataText.isNotEmpty) {
return _InitialData(ChannelUploadPageId.fromRawJson( return _InitialData(json
_extractJson(initialDataText, 'window["ytInitialData"] ='))); .decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
} }
initialDataText = scriptText.firstWhere( initialDataText = scriptText.firstWhere(
(e) => e.contains('var ytInitialData = '), (e) => e.contains('var ytInitialData = '),
orElse: () => ''); orElse: () => '');
if (initialDataText.isNotEmpty) { if (initialDataText.isNotEmpty) {
return _InitialData(ChannelUploadPageId.fromRawJson( return _InitialData(
_extractJson(initialDataText, 'var ytInitialData = '))); json.decode(_extractJson(initialDataText, 'var ytInitialData = ')));
} }
throw TransientFailureException( throw TransientFailureException(
@ -69,11 +74,11 @@ class ChannelUploadPage {
} }
/// ///
ChannelUploadPage(this._root, this.channelId, [_InitialData initialData]) ChannelUploadPage(this._root, this.channelId, [_InitialData? initialData])
: _initialData = initialData; : _initialData = initialData;
/// ///
Future<ChannelUploadPage> nextPage(YoutubeHttpClient httpClient) { Future<ChannelUploadPage?> nextPage(YoutubeHttpClient httpClient) {
if (initialData.continuation.isEmpty) { if (initialData.continuation.isEmpty) {
return Future.value(null); return Future.value(null);
} }
@ -81,15 +86,14 @@ class ChannelUploadPage {
'https://www.youtube.com/browse_ajax?ctoken=${initialData.continuation}&continuation=${initialData.continuation}&itct=${initialData.clickTrackingParams}'; 'https://www.youtube.com/browse_ajax?ctoken=${initialData.continuation}&continuation=${initialData.continuation}&itct=${initialData.clickTrackingParams}';
return retry(() async { return retry(() async {
var raw = await httpClient.getString(url); var raw = await httpClient.getString(url);
return ChannelUploadPage(null, channelId, return ChannelUploadPage(
_InitialData(ChannelUploadPageId.fromJson(json.decode(raw)[1]))); null, channelId, _InitialData(json.decode(raw)[1]));
}); });
} }
/// ///
static Future<ChannelUploadPage> get( static Future<ChannelUploadPage> get(
YoutubeHttpClient httpClient, String channelId, String sorting) { YoutubeHttpClient httpClient, String channelId, String sorting) {
assert(sorting != null);
var url = var url =
'https://www.youtube.com/channel/$channelId/videos?view=0&sort=$sorting&flow=grid'; 'https://www.youtube.com/channel/$channelId/videos?view=0&sort=$sorting&flow=grid';
return retry(() async { return retry(() async {
@ -105,81 +109,100 @@ class ChannelUploadPage {
class _InitialData { class _InitialData {
// Json parsed map // Json parsed map
final ChannelUploadPageId root; final Map<String, dynamic> root;
_InitialData(this.root); _InitialData(this.root);
/* Cache results */ late final Map<String, dynamic>? continuationContext =
getContinuationContext();
List<ChannelVideo> _uploads; late final String clickTrackingParams =
String _continuation; continuationContext?.getT<String>('continuationContext') ?? '';
String _clickTrackingParams;
List<GridRendererItem> getContentContext() { late final List<ChannelVideo> uploads =
if (root.contents != null) { getContentContext().map(_parseContent).whereNotNull().toList();
return root.contents.twoColumnBrowseResultsRenderer.tabs
.map((e) => e.tabRenderer) late final String continuation =
.firstWhere((e) => e.selected) continuationContext?.getT<String>('continuation') ?? '';
.content
.sectionListRenderer List<Map<String, dynamic>> getContentContext() {
.contents List<Map<String, dynamic>>? context;
.first if (root.containsKey('contents')) {
.itemSectionRenderer context = root
.contents .get('contents')
.first ?.get('twoColumnBrowseResultsRenderer')
.gridRenderer ?.getList('tabs')
.items; ?.map((e) => e['tabRenderer'])
.cast<Map<String, dynamic>>()
.firstWhereOrNull((e) => e['selected'] as bool)
?.get('content')
?.get('sectionListRenderer')
?.getList('contents')
?.firstOrNull
?.get('itemSectionRenderer')
?.getList('contents')
?.firstOrNull
?.get('gridRenderer')
?.getList('items')
?.cast<Map<String, dynamic>>();
} }
if (root.response != null) { if (context == null && root.containsKey('response')) {
return root.response.continuationContents.gridContinuation.items; context = root
.get('response')
?.get('continuationContents')
?.get('gridContinuation')
?.getList('items')
?.cast<Map<String, dynamic>>();
} }
throw FatalFailureException('Failed to get initial data context.'); if (context == null) {
throw FatalFailureException('Failed to get initial data context.');
}
return context;
} }
NextContinuationData getContinuationContext() { Map<String, dynamic>? getContinuationContext() {
if (root.contents != null) { if (root.containsKey('contents')) {
return root.contents?.twoColumnBrowseResultsRenderer?.tabs return root
?.map((e) => e.tabRenderer) .get('contents')
?.firstWhere((e) => e.selected) ?.get('twoColumnBrowseResultsRenderer')
?.content ?.getList('tabs')
?.sectionListRenderer ?.map((e) => e['tabRenderer'])
?.contents .cast<Map<String, dynamic>>()
?.first .firstWhereOrNull((e) => e['selected'] as bool)
?.itemSectionRenderer ?.get('content')
?.contents ?.get('sectionListRenderer')
?.first ?.getList('contents')
?.gridRenderer ?.firstOrNull
?.continuations ?.get('itemSectionRenderer')
?.first ?.getList('contents')
?.nextContinuationData; ?.firstOrNull
?.get('gridRenderer')
?.getList('continuations')
?.firstOrNull
?.get('nextContinuationData');
} }
if (root.response != null) { if (root.containsKey('response')) {
return root?.response?.continuationContents?.gridContinuation return root
?.continuations?.first?.nextContinuationData; .get('response')
?.get('continuationContents')
?.get('gridContinuation')
?.getList('continuations')
?.firstOrNull
?.get('nextContinuationData');
} }
return null; return null;
} }
List<ChannelVideo> get uploads => _uploads ??= getContentContext() ChannelVideo? _parseContent(Map<String, dynamic>? content) {
?.map(_parseContent) if (content == null || !content.containsKey('gridVideoRenderer')) {
?.where((e) => e != null)
?.toList();
String get continuation =>
_continuation ??= getContinuationContext()?.continuation ?? '';
String get clickTrackingParams => _clickTrackingParams ??=
getContinuationContext()?.clickTrackingParams ?? '';
ChannelVideo _parseContent(GridRendererItem content) {
if (content == null || content.gridVideoRenderer == null) {
return null; return null;
} }
var video = content.gridVideoRenderer;
var video = content.get('gridVideoRenderer')!;
return ChannelVideo( return ChannelVideo(
VideoId(video.videoId), VideoId(video.getT<String>('videoId')!),
video.title?.simpleText ?? video.get('title')?.getT<String>('simpleText') ??
video.title?.runs?.map((e) => e.text)?.join() ?? video.get('title')?.getList('runs')?.map((e) => e['text']).join() ??
''); '');
} }
} }

View File

@ -6,20 +6,18 @@ import '../youtube_http_client.dart';
/// ///
class ClosedCaptionTrackResponse { class ClosedCaptionTrackResponse {
final xml.XmlDocument _root; final xml.XmlDocument root;
Iterable<ClosedCaption> _closedCaptions;
/// ///
Iterable<ClosedCaption> get closedCaptions => _closedCaptions ??= late final Iterable<ClosedCaption> closedCaptions =
_root.findAllElements('p').map((e) => ClosedCaption._(e)); root.findAllElements('p').map((e) => ClosedCaption._(e));
/// ///
ClosedCaptionTrackResponse(this._root); ClosedCaptionTrackResponse(this.root);
/// ///
// ignore: deprecated_member_use // ignore: deprecated_member_use
ClosedCaptionTrackResponse.parse(String raw) : _root = xml.parse(raw); ClosedCaptionTrackResponse.parse(String raw) : root = xml.parse(raw);
/// ///
static Future<ClosedCaptionTrackResponse> get( static Future<ClosedCaptionTrackResponse> get(
@ -34,46 +32,39 @@ class ClosedCaptionTrackResponse {
/// ///
class ClosedCaption { class ClosedCaption {
final xml.XmlElement _root; final xml.XmlElement root;
Duration _offset;
Duration _duration;
Duration _end;
List<ClosedCaptionPart> _parts;
/// ///
String get text => _root.text; String get text => root.text;
/// ///
Duration get offset => _offset ??= late final Duration offset =
Duration(milliseconds: int.parse(_root.getAttribute('t') ?? 0)); Duration(milliseconds: int.parse(root.getAttribute('t') ?? '0'));
/// ///
Duration get duration => _duration ??= late final Duration duration =
Duration(milliseconds: int.parse(_root.getAttribute('d') ?? 0)); Duration(milliseconds: int.parse(root.getAttribute('d') ?? '0'));
/// ///
Duration get end => _end ??= offset + duration; late final Duration end = offset + duration;
/// ///
List<ClosedCaptionPart> getParts() => _parts ??= late final List<ClosedCaptionPart> parts =
_root.findAllElements('s').map((e) => ClosedCaptionPart._(e)).toList(); root.findAllElements('s').map((e) => ClosedCaptionPart._(e)).toList();
ClosedCaption._(this._root); ClosedCaption._(this.root);
} }
/// ///
class ClosedCaptionPart { class ClosedCaptionPart {
final xml.XmlElement _root; final xml.XmlElement root;
Duration _offset;
/// ///
String get text => _root.text; String get text => root.text;
/// ///
Duration get offset => _offset ??= late final Duration offset =
Duration(milliseconds: int.parse(_root.getAttribute('t') ?? '0')); Duration(milliseconds: int.parse(root.getAttribute('t') ?? '0'));
ClosedCaptionPart._(this._root); ClosedCaptionPart._(this.root);
} }

View File

@ -9,15 +9,14 @@ class DashManifest {
static final _urlSignatureExp = RegExp(r'/s/(.*?)(?:/|$)'); static final _urlSignatureExp = RegExp(r'/s/(.*?)(?:/|$)');
final xml.XmlDocument _root; final xml.XmlDocument _root;
Iterable<_StreamInfo> _streams;
/// ///
Iterable<_StreamInfo> get streams => _streams ??= _root late final Iterable<_StreamInfo> streams = _root
.findElements('Representation') .findElements('Representation')
.where((e) => e .where((e) => e
.findElements('Initialization') .findElements('Initialization')
.first .first
.getAttribute('sourceURL') .getAttribute('sourceURL')!
.contains('sq/')) .contains('sq/'))
.map((e) => _StreamInfo(e)); .map((e) => _StreamInfo(e));
@ -37,50 +36,53 @@ class DashManifest {
} }
/// ///
static String getSignatureFromUrl(String url) => static String? getSignatureFromUrl(String url) =>
_urlSignatureExp.firstMatch(url)?.group(1); _urlSignatureExp.firstMatch(url)?.group(1);
} }
class _StreamInfo extends StreamInfoProvider { class _StreamInfo extends StreamInfoProvider {
static final _contentLenExp = RegExp(r'[/\?]clen[/=](\d+)'); static final _contentLenExp = RegExp(r'[/\?]clen[/=](\d+)');
static final _containerExp = RegExp(r'mime[/=]\w*%2F([\w\d]*)');
final xml.XmlElement _root; final xml.XmlElement root;
_StreamInfo(this._root); _StreamInfo(this.root);
@override @override
int get tag => int.parse(_root.getAttribute('id')); late final int tag = int.parse(root.getAttribute('id')!);
@override @override
String get url => _root.getAttribute('BaseURL'); late final String url = root.getAttribute('BaseURL')!;
@override @override
int get contentLength => int.parse(_root.getAttribute('contentLength') ?? late final int contentLength = int.parse(
_contentLenExp.firstMatch(url).group(1)); (root.getAttribute('contentLength') ??
_contentLenExp.firstMatch(url)?.group(1))!);
@override @override
int get bitrate => int.parse(_root.getAttribute('bandwidth')); late final int bitrate = int.parse(root.getAttribute('bandwidth')!);
@override @override
String get container => late final String? container = '';
Uri.decodeFull(_containerExp.firstMatch(url).group(1)); /*
Uri.decodeFull((_containerExp.firstMatch(url)?.group(1))!);*/
bool get isAudioOnly => late final bool isAudioOnly =
_root.findElements('AudioChannelConfiguration').isNotEmpty; root.findElements('AudioChannelConfiguration').isNotEmpty;
@override @override
String get audioCodec => isAudioOnly ? null : _root.getAttribute('codecs'); late final String? audioCodec =
isAudioOnly ? null : root.getAttribute('codecs');
@override @override
String get videoCodec => isAudioOnly ? _root.getAttribute('codecs') : null; late final String? videoCodec =
isAudioOnly ? root.getAttribute('codecs') : null;
@override @override
int get videoWidth => int.parse(_root.getAttribute('width')); late final int videoWidth = int.parse(root.getAttribute('width')!);
@override @override
int get videoHeight => int.parse(_root.getAttribute('height')); late final int videoHeight = int.parse(root.getAttribute('height')!);
@override @override
int get framerate => int.parse(_root.getAttribute('framerate')); late final int framerate = int.parse(root.getAttribute('framerate')!);
} }

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser; import 'package:html/parser.dart' as parser;
@ -14,16 +15,16 @@ class EmbedPage {
RegExp('[\'""]PLAYER_CONFIG[\'""]\\s*:\\s*(\\{.*\\})'); RegExp('[\'""]PLAYER_CONFIG[\'""]\\s*:\\s*(\\{.*\\})');
static final _playerConfigExp2 = RegExp(r'yt.setConfig\((\{.*\})'); static final _playerConfigExp2 = RegExp(r'yt.setConfig\((\{.*\})');
final Document _root; final Document root;
EmbedPlayerConfig _playerConfig; late final EmbedPlayerConfig? playerConfig = getPlayerConfig();
/// ///
String get sourceUrl { String? get sourceUrl {
var url = _root var url = root
.querySelectorAll('*[name="player_ias/base"]') .querySelectorAll('*[name="player_ias/base"]')
.map((e) => e.attributes['src']) .map((e) => e.attributes['src'])
.where((e) => !e.isNullOrWhiteSpace) .where((e) => !e.isNullOrWhiteSpace)
.firstWhere((e) => e.contains('player_ias') && e.endsWith('.js'), .firstWhere((e) => e!.contains('player_ias') && e.endsWith('.js'),
orElse: () => null); orElse: () => null);
// _root.querySelector('*[name="player_ias/base"]').attributes['src']; // _root.querySelector('*[name="player_ias/base"]').attributes['src'];
if (url == null) { if (url == null) {
@ -33,35 +34,31 @@ class EmbedPage {
} }
/// ///
EmbedPlayerConfig get playerConfig { EmbedPlayerConfig? getPlayerConfig() {
if (_playerConfig != null) {
return _playerConfig;
}
var playerConfigJson = _playerConfigJson ?? _playerConfigJson2; var playerConfigJson = _playerConfigJson ?? _playerConfigJson2;
if (playerConfigJson == null) { if (playerConfigJson == null) {
return null; return null;
} }
return _playerConfig = return EmbedPlayerConfig(json.decode(playerConfigJson.extractJson()));
EmbedPlayerConfig(json.decode(playerConfigJson.extractJson()));
} }
String get _playerConfigJson => _root String? get _playerConfigJson => root
.getElementsByTagName('script') .getElementsByTagName('script')
.map((e) => e.text) .map((e) => e.text)
.map((e) => _playerConfigExp.firstMatch(e)?.group(1)) .map((e) => _playerConfigExp.firstMatch(e)?.group(1))
.firstWhere((e) => !e.isNullOrWhiteSpace, orElse: () => null); .firstWhereOrNull((e) => !e.isNullOrWhiteSpace);
String get _playerConfigJson2 => _root String? get _playerConfigJson2 => root
.getElementsByTagName('script') .getElementsByTagName('script')
.map((e) => e.text) .map((e) => e.text)
.map((e) => _playerConfigExp2.firstMatch(e)?.group(1)) .map((e) => _playerConfigExp2.firstMatch(e)?.group(1))
.firstWhere((e) => !e.isNullOrWhiteSpace, orElse: () => null); .firstWhereOrNull((e) => !e.isNullOrWhiteSpace);
/// ///
EmbedPage(this._root); EmbedPage(this.root);
/// ///
EmbedPage.parse(String raw) : _root = parser.parse(raw); EmbedPage.parse(String raw) : root = parser.parse(raw);
/// ///
static Future<EmbedPage> get(YoutubeHttpClient httpClient, String videoId) { static Future<EmbedPage> get(YoutubeHttpClient httpClient, String videoId) {

View File

@ -1,239 +0,0 @@
// @dart=2.9
// To parse this JSON data, do
//
// final playerConfigJson = playerConfigJsonFromJson(jsonString);
import 'dart:convert';
class PlayerConfigJson {
PlayerConfigJson({
this.assets,
this.attrs,
this.args,
});
final Assets assets;
final Attrs attrs;
final Args args;
factory PlayerConfigJson.fromRawJson(String str) =>
PlayerConfigJson.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PlayerConfigJson.fromJson(Map<String, dynamic> json) =>
PlayerConfigJson(
assets: json["assets"] == null ? null : Assets.fromJson(json["assets"]),
attrs: json["attrs"] == null ? null : Attrs.fromJson(json["attrs"]),
args: json["args"] == null ? null : Args.fromJson(json["args"]),
);
Map<String, dynamic> toJson() => {
"assets": assets == null ? null : assets.toJson(),
"attrs": attrs == null ? null : attrs.toJson(),
"args": args == null ? null : args.toJson(),
};
}
class Args {
Args({
this.innertubeApiKey,
this.showMiniplayerButton,
this.useMiniplayerUi,
this.gapiHintParams,
this.playerResponse,
this.cbrver,
this.cbr,
this.innertubeApiVersion,
this.innertubeContextClientVersion,
this.vssHost,
this.hostLanguage,
this.cr,
this.externalFullscreen,
this.useFastSizingOnWatchDefault,
this.c,
this.ps,
this.csiPageType,
this.cos,
this.enablecsi,
this.watermark,
this.cver,
this.transparentBackground,
this.hl,
this.enablejsapi,
this.cosver,
this.loaderUrl,
});
final String innertubeApiKey;
final String showMiniplayerButton;
final String useMiniplayerUi;
final String gapiHintParams;
final String playerResponse;
final String cbrver;
final String cbr;
final String innertubeApiVersion;
final String innertubeContextClientVersion;
final String vssHost;
final String hostLanguage;
final String cr;
final bool externalFullscreen;
final bool useFastSizingOnWatchDefault;
final String c;
final String ps;
final String csiPageType;
final String cos;
final String enablecsi;
final String watermark;
final String cver;
final String transparentBackground;
final String hl;
final String enablejsapi;
final String cosver;
final String loaderUrl;
factory Args.fromRawJson(String str) => Args.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Args.fromJson(Map<String, dynamic> json) => Args(
innertubeApiKey: json["innertube_api_key"] == null
? null
: json["innertube_api_key"],
showMiniplayerButton: json["show_miniplayer_button"] == null
? null
: json["show_miniplayer_button"],
useMiniplayerUi: json["use_miniplayer_ui"] == null
? null
: json["use_miniplayer_ui"],
gapiHintParams:
json["gapi_hint_params"] == null ? null : json["gapi_hint_params"],
playerResponse:
json["player_response"] == null ? null : json["player_response"],
cbrver: json["cbrver"] == null ? null : json["cbrver"],
cbr: json["cbr"] == null ? null : json["cbr"],
innertubeApiVersion: json["innertube_api_version"] == null
? null
: json["innertube_api_version"],
innertubeContextClientVersion:
json["innertube_context_client_version"] == null
? null
: json["innertube_context_client_version"],
vssHost: json["vss_host"] == null ? null : json["vss_host"],
hostLanguage:
json["host_language"] == null ? null : json["host_language"],
cr: json["cr"] == null ? null : json["cr"],
externalFullscreen: json["external_fullscreen"] == null
? null
: json["external_fullscreen"],
useFastSizingOnWatchDefault:
json["use_fast_sizing_on_watch_default"] == null
? null
: json["use_fast_sizing_on_watch_default"],
c: json["c"] == null ? null : json["c"],
ps: json["ps"] == null ? null : json["ps"],
csiPageType:
json["csi_page_type"] == null ? null : json["csi_page_type"],
cos: json["cos"] == null ? null : json["cos"],
enablecsi: json["enablecsi"] == null ? null : json["enablecsi"],
watermark: json["watermark"] == null ? null : json["watermark"],
cver: json["cver"] == null ? null : json["cver"],
transparentBackground: json["transparent_background"] == null
? null
: json["transparent_background"],
hl: json["hl"] == null ? null : json["hl"],
enablejsapi: json["enablejsapi"] == null ? null : json["enablejsapi"],
cosver: json["cosver"] == null ? null : json["cosver"],
loaderUrl: json["loaderUrl"] == null ? null : json["loaderUrl"],
);
Map<String, dynamic> toJson() => {
"innertube_api_key": innertubeApiKey == null ? null : innertubeApiKey,
"show_miniplayer_button":
showMiniplayerButton == null ? null : showMiniplayerButton,
"use_miniplayer_ui": useMiniplayerUi == null ? null : useMiniplayerUi,
"gapi_hint_params": gapiHintParams == null ? null : gapiHintParams,
"player_response": playerResponse == null ? null : playerResponse,
"cbrver": cbrver == null ? null : cbrver,
"cbr": cbr == null ? null : cbr,
"innertube_api_version":
innertubeApiVersion == null ? null : innertubeApiVersion,
"innertube_context_client_version":
innertubeContextClientVersion == null
? null
: innertubeContextClientVersion,
"vss_host": vssHost == null ? null : vssHost,
"host_language": hostLanguage == null ? null : hostLanguage,
"cr": cr == null ? null : cr,
"external_fullscreen":
externalFullscreen == null ? null : externalFullscreen,
"use_fast_sizing_on_watch_default": useFastSizingOnWatchDefault == null
? null
: useFastSizingOnWatchDefault,
"c": c == null ? null : c,
"ps": ps == null ? null : ps,
"csi_page_type": csiPageType == null ? null : csiPageType,
"cos": cos == null ? null : cos,
"enablecsi": enablecsi == null ? null : enablecsi,
"watermark": watermark == null ? null : watermark,
"cver": cver == null ? null : cver,
"transparent_background":
transparentBackground == null ? null : transparentBackground,
"hl": hl == null ? null : hl,
"enablejsapi": enablejsapi == null ? null : enablejsapi,
"cosver": cosver == null ? null : cosver,
"loaderUrl": loaderUrl == null ? null : loaderUrl,
};
}
class Assets {
Assets({
this.playerCanaryState,
this.js,
this.css,
});
final String playerCanaryState;
final String js;
final String css;
factory Assets.fromRawJson(String str) => Assets.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Assets.fromJson(Map<String, dynamic> json) => Assets(
playerCanaryState: json["player_canary_state"] == null
? null
: json["player_canary_state"],
js: json["js"] == null ? null : json["js"],
css: json["css"] == null ? null : json["css"],
);
Map<String, dynamic> toJson() => {
"player_canary_state":
playerCanaryState == null ? null : playerCanaryState,
"js": js == null ? null : js,
"css": css == null ? null : css,
};
}
class Attrs {
Attrs({
this.id,
});
final String id;
factory Attrs.fromRawJson(String str) => Attrs.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Attrs.fromJson(Map<String, dynamic> json) => Attrs(
id: json["id"] == null ? null : json["id"],
);
Map<String, dynamic> toJson() => {
"id": id == null ? null : id,
};
}

View File

@ -1,205 +0,0 @@
// @dart=2.9
// To parse this JSON data, do
//
// final playlistResponseJson = playlistResponseJsonFromJson(jsonString);
import 'dart:convert';
class PlaylistResponseJson {
PlaylistResponseJson({
this.title,
this.views,
this.description,
this.video,
this.author,
this.likes,
this.dislikes,
});
final String title;
final int views;
final String description;
final List<Video> video;
final String author;
final int likes;
final int dislikes;
factory PlaylistResponseJson.fromRawJson(String str) =>
PlaylistResponseJson.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PlaylistResponseJson.fromJson(Map<String, dynamic> json) =>
PlaylistResponseJson(
title: json["title"] == null ? null : json["title"],
views: json["views"] == null ? null : json["views"],
description: json["description"] == null ? null : json["description"],
video: json["video"] == null
? null
: List<Video>.from(json["video"].map((x) => Video.fromJson(x))),
author: json["author"] == null ? null : json["author"],
likes: json["likes"] == null ? null : json["likes"],
dislikes: json["dislikes"] == null ? null : json["dislikes"],
);
Map<String, dynamic> toJson() => {
"title": title == null ? null : title,
"views": views == null ? null : views,
"description": description == null ? null : description,
"video": video == null
? null
: List<dynamic>.from(video.map((x) => x.toJson())),
"author": author == null ? null : author,
"likes": likes == null ? null : likes,
"dislikes": dislikes == null ? null : dislikes,
};
}
class Video {
Video({
this.sessionData,
this.timeCreated,
this.ccLicense,
this.title,
this.rating,
this.isHd,
this.privacy,
this.lengthSeconds,
this.keywords,
this.views,
this.encryptedId,
this.likes,
this.isCc,
this.description,
this.thumbnail,
this.userId,
this.added,
this.endscreenAutoplaySessionData,
this.comments,
this.dislikes,
this.categoryId,
this.duration,
this.author,
});
final SessionData sessionData;
final int timeCreated;
final bool ccLicense;
final String title;
final num rating;
final bool isHd;
final Privacy privacy;
final int lengthSeconds;
final String keywords;
final String views;
final String encryptedId;
final int likes;
final bool isCc;
final String description;
final String thumbnail;
final String userId;
final String added;
final EndscreenAutoplaySessionData endscreenAutoplaySessionData;
final String comments;
final int dislikes;
final int categoryId;
final String duration;
final String author;
factory Video.fromRawJson(String str) => Video.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory Video.fromJson(Map<String, dynamic> json) => Video(
sessionData: json["session_data"] == null
? null
: sessionDataValues.map[json["session_data"]],
timeCreated: json["time_created"] == null ? null : json["time_created"],
ccLicense: json["cc_license"] == null ? null : json["cc_license"],
title: json["title"] == null ? null : json["title"],
rating: json["rating"] == null ? null : json["rating"],
isHd: json["is_hd"] == null ? null : json["is_hd"],
privacy:
json["privacy"] == null ? null : privacyValues.map[json["privacy"]],
lengthSeconds:
json["length_seconds"] == null ? null : json["length_seconds"],
keywords: json["keywords"] == null ? null : json["keywords"],
views: json["views"] == null ? null : json["views"],
encryptedId: json["encrypted_id"] == null ? null : json["encrypted_id"],
likes: json["likes"] == null ? null : json["likes"],
isCc: json["is_cc"] == null ? null : json["is_cc"],
description: json["description"] == null ? null : json["description"],
thumbnail: json["thumbnail"] == null ? null : json["thumbnail"],
userId: json["user_id"] == null ? null : json["user_id"],
added: json["added"] == null ? null : json["added"],
endscreenAutoplaySessionData:
json["endscreen_autoplay_session_data"] == null
? null
: endscreenAutoplaySessionDataValues
.map[json["endscreen_autoplay_session_data"]],
comments: json["comments"] == null ? null : json["comments"],
dislikes: json["dislikes"] == null ? null : json["dislikes"],
categoryId: json["category_id"] == null ? null : json["category_id"],
duration: json["duration"] == null ? null : json["duration"],
author: json["author"] == null ? null : json["author"],
);
Map<String, dynamic> toJson() => {
"session_data":
sessionData == null ? null : sessionDataValues.reverse[sessionData],
"time_created": timeCreated == null ? null : timeCreated,
"cc_license": ccLicense == null ? null : ccLicense,
"title": title == null ? null : title,
"rating": rating == null ? null : rating,
"is_hd": isHd == null ? null : isHd,
"privacy": privacy == null ? null : privacyValues.reverse[privacy],
"length_seconds": lengthSeconds == null ? null : lengthSeconds,
"keywords": keywords == null ? null : keywords,
"views": views == null ? null : views,
"encrypted_id": encryptedId == null ? null : encryptedId,
"likes": likes == null ? null : likes,
"is_cc": isCc == null ? null : isCc,
"description": description == null ? null : description,
"thumbnail": thumbnail == null ? null : thumbnail,
"user_id": userId == null ? null : userId,
"added": added == null ? null : added,
"endscreen_autoplay_session_data": endscreenAutoplaySessionData == null
? null
: endscreenAutoplaySessionDataValues
.reverse[endscreenAutoplaySessionData],
"comments": comments == null ? null : comments,
"dislikes": dislikes == null ? null : dislikes,
"category_id": categoryId == null ? null : categoryId,
"duration": duration == null ? null : duration,
"author": author == null ? null : author,
};
}
enum EndscreenAutoplaySessionData { FEATURE_AUTOPLAY }
final endscreenAutoplaySessionDataValues = EnumValues(
{"feature=autoplay": EndscreenAutoplaySessionData.FEATURE_AUTOPLAY});
enum Privacy { PUBLIC }
final privacyValues = EnumValues({"public": Privacy.PUBLIC});
enum SessionData { FEATURE_PLAYLIST }
final sessionDataValues =
EnumValues({"feature=playlist": SessionData.FEATURE_PLAYLIST});
class EnumValues<T> {
Map<String, T> map;
Map<T, String> reverseMap;
EnumValues(this.map);
Map<T, String> get reverse {
if (reverseMap == null) {
reverseMap = map.map((k, v) => new MapEntry(v, k));
}
return reverseMap;
}
}

View File

@ -1,2 +0,0 @@
Files in this directory where generated using https://app.quicktype.io/ , using as source the youtube api.
https://pypi.org/project/jsonmerge/ was used to merge source from different requests.

View File

@ -1,27 +1,19 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import '../../extensions/helpers_extension.dart'; import '../../extensions/helpers_extension.dart';
import 'generated/player_response.g.dart';
import 'stream_info_provider.dart'; import 'stream_info_provider.dart';
/// ///
class PlayerResponse { class PlayerResponse {
// Json parsed class // Json parsed map
PlayerResponseJson _root; Map<String, dynamic> root;
/// Json parsed map
final Map<String, dynamic> _rawJson;
Iterable<StreamInfoProvider> _muxedStreams;
Iterable<StreamInfoProvider> _adaptiveStreams;
List<StreamInfoProvider> _streams;
Iterable<ClosedCaptionTrack> _closedCaptionTrack;
String _videoPlayabilityError;
/// ///
String get playabilityStatus => _root.playabilityStatus.status; late final String playabilityStatus =
root.get('playabilityStatus')!.getT<String>('status')!;
/// ///
bool get isVideoAvailable => playabilityStatus.toLowerCase() != 'error'; bool get isVideoAvailable => playabilityStatus.toLowerCase() != 'error';
@ -30,41 +22,51 @@ class PlayerResponse {
bool get isVideoPlayable => playabilityStatus.toLowerCase() == 'ok'; bool get isVideoPlayable => playabilityStatus.toLowerCase() == 'ok';
/// ///
String get videoTitle => _root.videoDetails.title; String get videoTitle => root.get('videoDetails')!.getT<String>('title')!;
/// ///
String get videoAuthor => _root.videoDetails.author; String get videoAuthor => root.get('videoDetails')!.getT<String>('author')!;
/// ///
DateTime get videoUploadDate => DateTime get videoUploadDate => root
_root.microformat.playerMicroformatRenderer.uploadDate; .get('microformat')!
.get('playerMicroformatRenderer')!
.getT<String>('uploadDate')!
.parseDateTime();
/// ///
String get videoChannelId => _root.videoDetails.channelId; String get videoChannelId =>
root.get('videoDetails')!.getT<String>('channelId')!;
/// ///
Duration get videoDuration => Duration get videoDuration => Duration(
Duration(seconds: int.parse(_root.videoDetails.lengthSeconds)); seconds:
int.parse(root.get('videoDetails')!.getT<String>('lengthSeconds')!));
/// ///
List<String> get videoKeywords => _root.videoDetails.keywords ?? const []; List<String> get videoKeywords =>
root
.get('videoDetails')
?.getT<List<dynamic>>('keywords')
?.cast<String>() ??
const [];
/// ///
String get videoDescription => _root.videoDetails.shortDescription; String get videoDescription =>
root.get('videoDetails')!.getT<String>('shortDescription')!;
/// ///
int get videoViewCount => int.parse(_root.videoDetails.viewCount); int get videoViewCount =>
int.parse(root.get('videoDetails')!.getT<String>('viewCount')!);
//TODO: Get these types
/// ///
// Can be null String? get previewVideoId =>
String get previewVideoId => root
_rawJson
.get('playabilityStatus') .get('playabilityStatus')
?.get('errorScreen') ?.get('errorScreen')
?.get('playerLegacyDesktopYpcTrailerRenderer') ?.get('playerLegacyDesktopYpcTrailerRenderer')
?.getValue('trailerVideoId') ?? ?.getValue('trailerVideoId') ??
Uri.splitQueryString(_rawJson Uri.splitQueryString(root
.get('playabilityStatus') .get('playabilityStatus')
?.get('errorScreen') ?.get('errorScreen')
?.get('') ?.get('')
@ -73,160 +75,159 @@ class PlayerResponse {
'')['video_id']; '')['video_id'];
/// ///
bool get isLive => _root.videoDetails.isLive ?? false; bool get isLive => root.get('videoDetails')?.getT<bool>('isLive') ?? false;
/// ///
// Can be null String? get hlsManifestUrl =>
String get hlsManifestUrl => _root.streamingData?.hlsManifestUrl; root.get('streamingData')?.getT<String>('hlsManifestUrl');
/// ///
// Can be null String? get dashManifestUrl =>
String get dashManifestUrl => _root.streamingData?.dashManifestUrl; root.get('streamingData')?.getT<String>('dashManifestUrl');
/// ///
List<StreamInfoProvider> get muxedStreams => late final List<StreamInfoProvider> muxedStreams = root
_muxedStreams ??= _root.streamingData?.formats .get('streamingData')
?.map((e) => _StreamInfo(e)) ?.getList('formats')
?.cast<StreamInfoProvider>() ?.map((e) => _StreamInfo(e))
?.toList() ?? .cast<StreamInfoProvider>()
const <StreamInfoProvider>[]; .toList() ??
const <StreamInfoProvider>[];
/// ///
List<StreamInfoProvider> get adaptiveStreams => late final List<StreamInfoProvider> adaptiveStreams = root
_adaptiveStreams ??= _root.streamingData?.adaptiveFormats .get('streamingData')
?.map((e) => _StreamInfo(e)) ?.getList('adaptiveFormats')
?.cast<StreamInfoProvider>() ?.map((e) => _StreamInfo(e))
?.toList() ?? .cast<StreamInfoProvider>()
const []; .toList() ??
const [];
/// ///
List<StreamInfoProvider> get streams => late final List<StreamInfoProvider> streams = [
_streams ??= [...muxedStreams, ...adaptiveStreams]; ...muxedStreams,
...adaptiveStreams
];
/// ///
List<ClosedCaptionTrack> get closedCaptionTrack => _closedCaptionTrack ??= late final List<ClosedCaptionTrack> closedCaptionTrack = root
_root.captions?.playerCaptionsTracklistRenderer?.captionTracks .get('captions')
?.map((e) => ClosedCaptionTrack(e)) ?.get('playerCaptionsTracklistRenderer')
?.cast<ClosedCaptionTrack>() ?.getList('captionTracks')
?.toList() ?? ?.map((e) => ClosedCaptionTrack(e))
const []; .cast<ClosedCaptionTrack>()
.toList() ??
/// Can be null const [];
String getVideoPlayabilityError() =>
_videoPlayabilityError ??= _root.playabilityStatus.reason;
/// ///
PlayerResponse.parse(String raw) : _rawJson = json.decode(raw) { late final String? videoPlayabilityError =
_root = PlayerResponseJson.fromJson(_rawJson); root.get('playabilityStatus')?.getT<String>('reason');
}
///
PlayerResponse.parse(String raw) : root = json.decode(raw);
} }
/// ///
class ClosedCaptionTrack { class ClosedCaptionTrack {
// Json parsed class // Json parsed class
final CaptionTrack _root; final Map<String, dynamic> root;
/// ///
String get url => _root.baseUrl; String get url => root.getT<String>('baseUrl')!;
/// ///
String get languageCode => _root.languageCode; String get languageCode => root.getT<String>('languageCode')!;
/// ///
String get languageName => _root.name.simpleText; String get languageName => root.get('name')!.getT<String>('simpleText')!;
/// ///
bool get autoGenerated => _root.vssId.toLowerCase().startsWith('a.'); bool get autoGenerated =>
root.getT<String>('vssId')!.toLowerCase().startsWith('a.');
/// ///
ClosedCaptionTrack(this._root); ClosedCaptionTrack(this.root);
} }
class _StreamInfo extends StreamInfoProvider { class _StreamInfo extends StreamInfoProvider {
static final _contentLenExp = RegExp(r'[\?&]clen=(\d+)'); static final _contentLenExp = RegExp(r'[\?&]clen=(\d+)');
// Json parsed class /// Json parsed map
final Format _root; final Map<String, dynamic> root;
int _bitrate;
String _container;
int _contentLength;
int _framerate;
String _signature;
String _signatureParameter;
int _tag;
String _url;
@override @override
int get bitrate => _bitrate ??= _root.bitrate; late final int? bitrate = root.getT<int>('bitrate');
@override @override
String get container => _container ??= mimeType.subtype; late final String? container = mimeType?.subtype;
@override @override
int get contentLength => late final int? contentLength = int.tryParse(
_contentLength ??= int.tryParse(_root.contentLength ?? '') ?? root.getT<String>('contentLength') ??
_contentLenExp.firstMatch(url)?.group(1); _contentLenExp.firstMatch(url)?.group(1) ??
'');
@override @override
int get framerate => _framerate ??= _root.fps; late final int? framerate = root.getT<int>('fps');
@override @override
String get signature => late final String? signature =
_signature ??= Uri.splitQueryString(_root.signatureCipher ?? '')['s']; Uri.splitQueryString(root.getT<String>('signatureCipher') ?? '')['s'];
@override @override
String get signatureParameter => late final String? signatureParameter = Uri.splitQueryString(
_signatureParameter ??= Uri.splitQueryString(_root.cipher ?? '')['sp'] ?? root.getT<String>('cipher') ?? '')['sp'] ??
Uri.splitQueryString(_root.signatureCipher ?? '')['sp']; Uri.splitQueryString(root.getT<String>('signatureCipher') ?? '')['sp'];
@override @override
int get tag => _tag ??= _root.itag; late final int tag = root.getT<int>('itag')!;
@override @override
String get url => _url ??= _getUrl(); late final String url = root.getT<String>('url') ??
Uri.splitQueryString(root.getT<String>('cipher') ?? '')['url'] ??
Uri.splitQueryString(root.getT<String>('signatureCipher') ?? '')['url']!;
String _getUrl() { @override
var url = _root.url; late final String? videoCodec = isAudioOnly
url ??= Uri.splitQueryString(_root.cipher ?? '')['url']; ? null
url ??= Uri.splitQueryString(_root.signatureCipher ?? '')['url']; : codecs?.split(',').firstOrNull?.trim().nullIfWhitespace;
return url;
@override
late final int? videoHeight = root.getT<int>('height');
@override
late final String? videoQualityLabel = root.getT<String>('qualityLabel');
@override
late final int? videoWidth = root.getT<int>('width');
late final bool isAudioOnly = mimeType?.type == 'audio';
late final MediaType? mimeType = _getMimeType();
MediaType? _getMimeType() {
var mime = root.getT<String>('mimeType');
if (mime == null) {
return null;
}
return MediaType.parse(mime);
} }
bool _isAudioOnly; late final String? codecs = mimeType?.parameters['codecs']?.toLowerCase();
MediaType _mimeType;
String _codecs;
@override @override
String get videoCodec => late final String? audioCodec =
isAudioOnly ? null : codecs.split(',').first.trim().nullIfWhitespace; isAudioOnly ? codecs : _getAudioCodec(codecs?.split(','))?.trim();
@override String? _getAudioCodec(List<String>? codecs) {
int get videoHeight => _root.height; if (codecs == null) {
return null;
@override }
String get videoQualityLabel => _root.qualityLabel;
@override
int get videoWidth => _root.width;
bool get isAudioOnly => _isAudioOnly ??= mimeType.type == 'audio';
MediaType get mimeType => _mimeType ??= MediaType.parse(_root.mimeType);
String get codecs =>
_codecs ??= mimeType?.parameters['codecs']?.toLowerCase();
@override
String get audioCodec =>
isAudioOnly ? codecs : _getAudioCodec(codecs.split(','))?.trim();
String _getAudioCodec(List<String> codecs) {
if (codecs.length == 1) { if (codecs.length == 1) {
return null; return null;
} }
return codecs.last; return codecs.last;
} }
_StreamInfo(this._root); _StreamInfo(this.root);
} }

View File

@ -1,72 +1,67 @@
import 'dart:async'; import 'dart:async';
import '../../exceptions/exceptions.dart'; import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart'; import '../../retry.dart';
import '../cipher/cipher_operations.dart'; import '../cipher/cipher_operations.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
/// ///
class PlayerSource { class PlayerSource {
final RegExp _statIndexExp = RegExp(r'\(\w+,(\d+)\)'); static final RegExp _statIndexExp = RegExp(r'\(\w+,(\d+)\)');
final RegExp _funcBodyExp = RegExp( static final RegExp _funcBodyExp = RegExp(
r'(\w+)=function\(\w+\){(\w+)=\2\.split\(\x22{2}\);.*?return\s+\2\.join\(\x22{2}\)}'); r'(\w+)=function\(\w+\){(\w+)=\2\.split\(\x22{2}\);.*?return\s+\2\.join\(\x22{2}\)}');
final RegExp _objNameExp = RegExp(r'([\$_\w]+).\w+\(\w+,\d+\);'); static final RegExp _objNameExp = RegExp(r'([\$_\w]+).\w+\(\w+,\d+\);');
final RegExp _calledFuncNameExp = RegExp(r'\w+(?:.|\[)(\"?\w+(?:\")?)\]?\('); static final RegExp _calledFuncNameExp =
RegExp(r'\w+(?:.|\[)(\"?\w+(?:\")?)\]?\(');
final String _root; final String root;
String _sts; late final String sts = getSts();
String _deciphererDefinitionBody;
/// ///
String get sts { String getSts() {
if (_sts != null) {
return _sts;
}
var val = RegExp(r'(?<=invalid namespace.*?;[\w\s]+=)\d+') var val = RegExp(r'(?<=invalid namespace.*?;[\w\s]+=)\d+')
.stringMatch(_root) .stringMatch(root)
?.nullIfWhitespace ?? ?.nullIfWhitespace ??
RegExp(r'(?<=signatureTimestamp[=\:])\d+') RegExp(r'(?<=signatureTimestamp[=\:])\d+')
.stringMatch(_root) .stringMatch(root)
?.nullIfWhitespace; ?.nullIfWhitespace;
if (val == null) { if (val == null) {
throw FatalFailureException('Could not find sts in player source.'); throw FatalFailureException('Could not find sts in player source.');
} }
return _sts ??= val; return val;
} }
/// ///
Iterable<CipherOperation> getCiperOperations() sync* { Iterable<CipherOperation> getCipherOperations() sync* {
var funcBody = _getDeciphererFuncBody(); if (deciphererFuncBody == null) {
if (funcBody == null) {
throw FatalFailureException( throw FatalFailureException(
'Could not find signature decipherer function body.'); 'Could not find signature decipherer function body.');
} }
var definitionBody = _getDeciphererDefinitionBody(funcBody); var definitionBody = _getDeciphererDefinitionBody(deciphererFuncBody!);
if (definitionBody == null) { if (definitionBody == null) {
throw FatalFailureException( throw FatalFailureException(
'Could not find signature decipherer definition body.'); 'Could not find signature decipherer definition body.');
} }
for (var statement in funcBody.split(';')) { for (final statement in deciphererFuncBody!.split(';')) {
var calledFuncName = _calledFuncNameExp.firstMatch(statement)?.group(1); var calledFuncName = _calledFuncNameExp.firstMatch(statement)?.group(1);
if (calledFuncName.isNullOrWhiteSpace) { if (calledFuncName.isNullOrWhiteSpace) {
continue; continue;
} }
var escapedFuncName = RegExp.escape(calledFuncName); var escapedFuncName = RegExp.escape(calledFuncName!);
// Slice // Slice
var exp = RegExp('$escapedFuncName' var exp = RegExp('$escapedFuncName'
r':\bfunction\b\([a],b\).(\breturn\b)?.?\w+\.'); r':\bfunction\b\([a],b\).(\breturn\b)?.?\w+\.');
if (exp.hasMatch(definitionBody)) { if (exp.hasMatch(definitionBody)) {
var index = int.parse(_statIndexExp.firstMatch(statement).group(1)); var index = int.parse(_statIndexExp.firstMatch(statement)!.group(1)!);
yield SliceCipherOperation(index); yield SliceCipherOperation(index);
} }
@ -74,7 +69,7 @@ class PlayerSource {
exp = RegExp( exp = RegExp(
'$escapedFuncName' r':\bfunction\b\(\w+\,\w\).\bvar\b.\bc=a\b'); '$escapedFuncName' r':\bfunction\b\(\w+\,\w\).\bvar\b.\bc=a\b');
if (exp.hasMatch(definitionBody)) { if (exp.hasMatch(definitionBody)) {
var index = int.parse(_statIndexExp.firstMatch(statement).group(1)); var index = int.parse(_statIndexExp.firstMatch(statement)!.group(1)!);
yield SwapCipherOperation(index); yield SwapCipherOperation(index);
} }
@ -86,28 +81,28 @@ class PlayerSource {
} }
} }
String _getDeciphererFuncBody() { late final String? deciphererFuncBody =
return _deciphererDefinitionBody ??= _funcBodyExp.firstMatch(root)?.group(0);
_funcBodyExp.firstMatch(_root).group(0);
}
String _getDeciphererDefinitionBody(String deciphererFuncBody) { String? _getDeciphererDefinitionBody(String deciphererFuncBody) {
var objName = _objNameExp.firstMatch(deciphererFuncBody).group(1); final objName = _objNameExp.firstMatch(deciphererFuncBody)?.group(1);
if (objName == null) {
return null;
}
var exp = RegExp( final exp = RegExp(
r'var\s+' r'var\s+'
'${RegExp.escape(objName)}' '${RegExp.escape(objName)}'
r'=\{(\w+:function\(\w+(,\w+)?\)\{(.*?)\}),?\};', r'=\{(\w+:function\(\w+(,\w+)?\)\{(.*?)\}),?\};',
dotAll: true); dotAll: true);
return exp.firstMatch(_root)?.group(0)?.nullIfWhitespace; return exp.firstMatch(root)?.group(0)?.nullIfWhitespace;
} }
/// ///
PlayerSource(this._root); PlayerSource(this.root);
/// /// Same as default constructor
// Same as default constructor PlayerSource.parse(this.root);
PlayerSource.parse(this._root);
/// ///
static Future<PlayerSource> get( static Future<PlayerSource> get(
@ -120,10 +115,10 @@ class PlayerSource {
if (_cache[url] == null) { if (_cache[url] == null) {
_cache[url] = _CachedValue(val); _cache[url] = _CachedValue(val);
} else { } else {
_cache[url].update(val); _cache[url]!.update(val);
} }
} }
return _cache[url].value; return _cache[url]!.value;
} }
static final Map<String, _CachedValue<PlayerSource>> _cache = {}; static final Map<String, _CachedValue<PlayerSource>> _cache = {};
@ -148,9 +143,8 @@ class _CachedValue<T> {
set value(T other) => _value = other; set value(T other) => _value = other;
_CachedValue(this._value, [this.expireTime, this.cacheTime = 600000]) { _CachedValue(this._value, [this.cacheTime = 600000])
expireTime ??= DateTime.now().millisecondsSinceEpoch + cacheTime; : expireTime = DateTime.now().millisecondsSinceEpoch + cacheTime;
}
void update(T newValue) { void update(T newValue) {
var now = DateTime.now().millisecondsSinceEpoch; var now = DateTime.now().millisecondsSinceEpoch;
@ -158,17 +152,3 @@ class _CachedValue<T> {
value = newValue; value = newValue;
} }
} }
extension on String {
String get nullIfWhitespace => trim().isEmpty ? null : this;
bool get isNullOrWhiteSpace {
if (this == null) {
return true;
}
if (trim().isEmpty) {
return true;
}
return false;
}
}

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser; import 'package:html/parser.dart' as parser;
@ -10,48 +11,35 @@ import '../youtube_http_client.dart';
/// ///
class PlaylistPage { class PlaylistPage {
final _apiKeyExp = RegExp(r'"INNERTUBE_API_KEY":"(\w+?)"');
/// ///
final String playlistId; final String playlistId;
final Document _root; final Document? root;
String _apiKey; late final _InitialData initialData = getInitialData();
_InitialData? _initialData;
/// ///
String get apiKey => _apiKey ??= _apiKeyExp _InitialData getInitialData() {
.firstMatch(_root
.querySelectorAll('script')
.firstWhere((e) => e.text.contains('INNERTUBE_API_KEY'))
.text)
.group(1);
_InitialData _initialData;
///
_InitialData get initialData {
if (_initialData != null) { if (_initialData != null) {
return _initialData; return _initialData!;
} }
final scriptText = _root final scriptText = root!
.querySelectorAll('script') .querySelectorAll('script')
.map((e) => e.text) .map((e) => e.text)
.toList(growable: false); .toList(growable: false);
var initialDataText = scriptText.firstWhere( var initialDataText = scriptText
(e) => e.contains('window["ytInitialData"] ='), .firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='));
orElse: () => null);
if (initialDataText != null) { if (initialDataText != null) {
return _initialData = _InitialData(json return _InitialData(json
.decode(_extractJson(initialDataText, 'window["ytInitialData"] ='))); .decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
} }
initialDataText = scriptText.firstWhere( initialDataText =
(e) => e.contains('var ytInitialData = '), scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = '));
orElse: () => null);
if (initialDataText != null) { if (initialDataText != null) {
return _initialData = _InitialData( return _InitialData(
json.decode(_extractJson(initialDataText, 'var ytInitialData = '))); json.decode(_extractJson(initialDataText, 'var ytInitialData = ')));
} }
@ -60,19 +48,17 @@ class PlaylistPage {
} }
String _extractJson(String html, String separator) { String _extractJson(String html, String separator) {
if (html == null || separator == null) {
return null;
}
var index = html.indexOf(separator) + separator.length; var index = html.indexOf(separator) + separator.length;
if (index > html.length) { if (index > html.length) {
return null; throw TransientFailureException(
'Failed to retrieve initial data from the search page, please report this to the project GitHub page. Couldn\'t extract json: $html');
} }
return _matchJson(html.substring(index)); return _matchJson(html.substring(index));
} }
String _matchJson(String str) { String _matchJson(String str) {
var bracketCount = 0; var bracketCount = 0;
int lastI; var lastI = 0;
for (var i = 0; i < str.length; i++) { for (var i = 0; i < str.length; i++) {
lastI = i; lastI = i;
if (str[i] == '{') { if (str[i] == '{') {
@ -89,12 +75,11 @@ class PlaylistPage {
} }
/// ///
PlaylistPage(this._root, this.playlistId, PlaylistPage(this.root, this.playlistId, [_InitialData? initialData])
[_InitialData initialData, this._apiKey])
: _initialData = initialData; : _initialData = initialData;
/// ///
Future<PlaylistPage> nextPage(YoutubeHttpClient httpClient) async { Future<PlaylistPage?> nextPage(YoutubeHttpClient httpClient) async {
if (initialData.continuationToken == null) { if (initialData.continuationToken == null) {
return null; return null;
} }
@ -103,7 +88,7 @@ class PlaylistPage {
/// ///
static Future<PlaylistPage> get(YoutubeHttpClient httpClient, String id, static Future<PlaylistPage> get(YoutubeHttpClient httpClient, String id,
{String token}) { {String? token}) {
if (token != null && token.isNotEmpty) { if (token != null && token.isNotEmpty) {
var url = var url =
'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; 'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
@ -120,7 +105,8 @@ class PlaylistPage {
'continuation': token 'continuation': token
}; };
var raw = await httpClient.post(url, body: json.encode(body)); var raw =
await httpClient.post(Uri.parse(url), body: json.encode(body));
return PlaylistPage(null, id, _InitialData(json.decode(raw.body))); return PlaylistPage(null, id, _InitialData(json.decode(raw.body)));
}); });
// Ask for next page, // Ask for next page,
@ -135,7 +121,7 @@ class PlaylistPage {
} }
/// ///
PlaylistPage.parse(String raw, this.playlistId) : _root = parser.parse(raw); PlaylistPage.parse(String raw, this.playlistId) : root = parser.parse(raw);
} }
class _InitialData { class _InitialData {
@ -144,12 +130,12 @@ class _InitialData {
_InitialData(this.root); _InitialData(this.root);
String get title => root late final String? title = root
?.get('metadata') .get('metadata')
?.get('playlistMetadataRenderer') ?.get('playlistMetadataRenderer')
?.getT<String>('title'); ?.getT<String>('title');
String get author => root late final String? author = root
.get('sidebar') .get('sidebar')
?.get('playlistSidebarRenderer') ?.get('playlistSidebarRenderer')
?.getList('items') ?.getList('items')
@ -161,13 +147,13 @@ class _InitialData {
?.getT<List<dynamic>>('runs') ?.getT<List<dynamic>>('runs')
?.parseRuns(); ?.parseRuns();
String get description => root late final String? description = root
?.get('metadata') .get('metadata')
?.get('playlistMetadataRenderer') ?.get('playlistMetadataRenderer')
?.getT<String>('description'); ?.getT<String>('description');
int get viewCount => root late final int? viewCount = root
?.get('sidebar') .get('sidebar')
?.get('playlistSidebarRenderer') ?.get('playlistSidebarRenderer')
?.getList('items') ?.getList('items')
?.firstOrNull ?.firstOrNull
@ -177,15 +163,15 @@ class _InitialData {
?.getT<String>('simpleText') ?.getT<String>('simpleText')
?.parseInt(); ?.parseInt();
String get continuationToken => (videosContent ?? playlistVideosContent) late final String? continuationToken =
?.firstWhere((e) => e['continuationItemRenderer'] != null, (videosContent ?? playlistVideosContent)
orElse: () => null) ?.firstWhereOrNull((e) => e['continuationItemRenderer'] != null)
?.get('continuationItemRenderer') ?.get('continuationItemRenderer')
?.get('continuationEndpoint') ?.get('continuationEndpoint')
?.get('continuationCommand') ?.get('continuationCommand')
?.getT<String>('token'); ?.getT<String>('token');
List<Map<String, dynamic>> get playlistVideosContent => List<Map<String, dynamic>>? get playlistVideosContent =>
root root
.get('contents') .get('contents')
?.get('twoColumnBrowseResultsRenderer') ?.get('twoColumnBrowseResultsRenderer')
@ -205,26 +191,25 @@ class _InitialData {
.getList('onResponseReceivedActions') .getList('onResponseReceivedActions')
?.firstOrNull ?.firstOrNull
?.get('appendContinuationItemsAction') ?.get('appendContinuationItemsAction')
?.get('continuationItems'); ?.getList('continuationItems');
List<Map<String, dynamic>> get videosContent => late final List<Map<String, dynamic>>? videosContent = root
root
.get('contents') .get('contents')
?.get('twoColumnSearchResultsRenderer') ?.get('twoColumnSearchResultsRenderer')
?.get('primaryContents') ?.get('primaryContents')
?.get('sectionListRenderer') ?.get('sectionListRenderer')
?.getList('contents') ?? ?.getList('contents') ??
root root
?.getList('onResponseReceivedCommands') .getList('onResponseReceivedCommands')
?.firstOrNull ?.firstOrNull
?.get('appendContinuationItemsAction') ?.get('appendContinuationItemsAction')
?.get('continuationItems'); ?.getList('continuationItems');
List<_Video> get playlistVideos => List<_Video> get playlistVideos =>
playlistVideosContent playlistVideosContent
?.where((e) => e['playlistVideoRenderer'] != null) ?.where((e) => e['playlistVideoRenderer'] != null)
?.map((e) => _Video(e['playlistVideoRenderer'])) .map((e) => _Video(e['playlistVideoRenderer']))
?.toList() ?? .toList() ??
const []; const [];
List<_Video> get videos => List<_Video> get videos =>
@ -232,8 +217,8 @@ class _InitialData {
?.get('itemSectionRenderer') ?.get('itemSectionRenderer')
?.getList('contents') ?.getList('contents')
?.where((e) => e['videoRenderer'] != null) ?.where((e) => e['videoRenderer'] != null)
?.map((e) => _Video(e)) .map((e) => _Video(e))
?.toList() ?? .toList() ??
const []; const [];
} }
@ -243,11 +228,11 @@ class _Video {
_Video(this.root); _Video(this.root);
String get id => root?.getT<String>('videoId'); String get id => root.getT<String>('videoId')!;
String get author => String get author =>
root?.get('ownerText')?.getT<List<dynamic>>('runs')?.parseRuns() ?? root.get('ownerText')?.getT<List<dynamic>>('runs')?.parseRuns() ??
root?.get('shortBylineText')?.getT<List<dynamic>>('runs')?.parseRuns() ?? root.get('shortBylineText')?.getT<List<dynamic>>('runs')?.parseRuns() ??
''; '';
String get channelId => String get channelId =>
@ -272,14 +257,14 @@ class _Video {
String get description => String get description =>
root.getList('descriptionSnippet')?.parseRuns() ?? ''; root.getList('descriptionSnippet')?.parseRuns() ?? '';
Duration get duration => Duration? get duration =>
_stringToDuration(root.get('lengthText')?.getT<String>('simpleText')); _stringToDuration(root.get('lengthText')?.getT<String>('simpleText'));
int get viewCount => int get viewCount =>
root.get('viewCountText')?.getT<String>('simpleText')?.parseInt() ?? 0; root.get('viewCountText')?.getT<String>('simpleText')?.parseInt() ?? 0;
/// Format: HH:MM:SS /// Format: HH:MM:SS
static Duration _stringToDuration(String string) { static Duration? _stringToDuration(String? string) {
if (string == null || string.trim().isEmpty) { if (string == null || string.trim().isEmpty) {
return null; return null;
} }

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser; import 'package:html/parser.dart' as parser;
@ -11,53 +12,39 @@ import '../../search/related_query.dart';
import '../../search/search_video.dart'; import '../../search/search_video.dart';
import '../../videos/videos.dart'; import '../../videos/videos.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
import 'generated/search_page_id.g.dart' hide PlaylistId;
/// ///
class SearchPage { class SearchPage {
final _apiKeyExp = RegExp(r'"INNERTUBE_API_KEY":"(\w+?)"');
/// ///
final String queryString; final String queryString;
final Document _root; final Document? root;
String _apiKey; late final _InitialData initialData = getInitialData();
_InitialData? _initialData;
/// ///
String get apiKey => _apiKey ??= _apiKeyExp _InitialData getInitialData() {
.firstMatch(_root
.querySelectorAll('script')
.firstWhere((e) => e.text.contains('INNERTUBE_API_KEY'))
.text)
.group(1);
_InitialData _initialData;
///
_InitialData get initialData {
if (_initialData != null) { if (_initialData != null) {
return _initialData; return _initialData!;
} }
final scriptText = _root final scriptText = root!
.querySelectorAll('script') .querySelectorAll('script')
.map((e) => e.text) .map((e) => e.text)
.toList(growable: false); .toList(growable: false);
var initialDataText = scriptText.firstWhere( var initialDataText = scriptText
(e) => e.contains('window["ytInitialData"] ='), .firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='));
orElse: () => null);
if (initialDataText != null) { if (initialDataText != null) {
return _initialData = _InitialData(SearchPageId.fromRawJson( return _initialData = _InitialData(json
_extractJson(initialDataText, 'window["ytInitialData"] ='))); .decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
} }
initialDataText = scriptText.firstWhere( initialDataText =
(e) => e.contains('var ytInitialData = '), scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = '));
orElse: () => null);
if (initialDataText != null) { if (initialDataText != null) {
return _initialData = _InitialData(SearchPageId.fromRawJson( return _initialData = _InitialData(
_extractJson(initialDataText, 'var ytInitialData = '))); json.decode(_extractJson(initialDataText, 'var ytInitialData = ')));
} }
throw TransientFailureException( throw TransientFailureException(
@ -65,19 +52,17 @@ class SearchPage {
} }
String _extractJson(String html, String separator) { String _extractJson(String html, String separator) {
if (html == null || separator == null) {
return null;
}
var index = html.indexOf(separator) + separator.length; var index = html.indexOf(separator) + separator.length;
if (index > html.length) { if (index > html.length) {
return null; throw TransientFailureException(
'Failed to retrieve initial data from the search page, please report this to the project GitHub page. Couldn\'t extract json: $html');
} }
return _matchJson(html.substring(index)); return _matchJson(html.substring(index));
} }
String _matchJson(String str) { String _matchJson(String str) {
var bracketCount = 0; var bracketCount = 0;
int lastI; var lastI = 0;
for (var i = 0; i < str.length; i++) { for (var i = 0; i < str.length; i++) {
lastI = i; lastI = i;
if (str[i] == '{') { if (str[i] == '{') {
@ -94,13 +79,10 @@ class SearchPage {
} }
/// ///
SearchPage(this._root, this.queryString, SearchPage(this.root, this.queryString, [_InitialData? initialData])
[_InitialData initialData, this._apiKey])
: _initialData = initialData; : _initialData = initialData;
/// Future<SearchPage?> nextPage(YoutubeHttpClient httpClient) async {
// TODO: Replace this in favour of async* when quering;
Future<SearchPage> nextPage(YoutubeHttpClient httpClient) async {
if (initialData.continuationToken == '' || if (initialData.continuationToken == '' ||
initialData.estimatedResults == 0) { initialData.estimatedResults == 0) {
return null; return null;
@ -111,7 +93,7 @@ class SearchPage {
/// ///
static Future<SearchPage> get( static Future<SearchPage> get(
YoutubeHttpClient httpClient, String queryString, YoutubeHttpClient httpClient, String queryString,
{String token}) { {String? token}) {
if (token != null) { if (token != null) {
var url = var url =
'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; 'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
@ -128,9 +110,10 @@ class SearchPage {
'continuation': token 'continuation': token
}; };
var raw = await httpClient.post(url, body: json.encode(body)); var raw =
return SearchPage(null, queryString, await httpClient.post(Uri.parse(url), body: json.encode(body));
_InitialData(SearchPageId.fromJson(json.decode(raw.body)))); return SearchPage(
null, queryString, _InitialData(json.decode(raw.body)));
}); });
// Ask for next page, // Ask for next page,
@ -145,129 +128,158 @@ class SearchPage {
} }
/// ///
SearchPage.parse(String raw, this.queryString) : _root = parser.parse(raw); SearchPage.parse(String raw, this.queryString) : root = parser.parse(raw);
} }
class _InitialData { class _InitialData {
// Json parsed map // Json parsed map
final SearchPageId root; final Map<String, dynamic> root;
_InitialData(this.root); _InitialData(this.root);
List<PurpleContent> getContentContext() { List<Map<String, dynamic>>? getContentContext() {
if (root.contents != null) { if (root['contents'] != null) {
return root.contents.twoColumnSearchResultsRenderer.primaryContents return root
.sectionListRenderer.contents.first.itemSectionRenderer.contents; .get('contents')
?.get('twoColumnSearchResultsRenderer')
?.get('primaryContents')
?.get('sectionListRenderer')
?.getList('contents')
?.firstOrNull
?.get('itemSectionRenderer')
?.getList('contents');
} }
if (root.onResponseReceivedCommands != null) { if (root['onResponseReceivedCommands'] != null) {
final itemSection = root return root
.onResponseReceivedCommands .getList('onResponseReceivedCommands')
.first ?.firstOrNull
.appendContinuationItemsAction ?.get('appendContinuationItemsAction')
.continuationItems[0] ?.getList('continuationItems')
.itemSectionRenderer; ?.firstOrNull
if (itemSection == null) { ?.get('itemSectionRenderer')
throw SearchItemSectionException(); ?.getList('contents');
}
return itemSection.contents;
} }
return null; return null;
} }
String _getContinuationToken() { String? _getContinuationToken() {
if (root.contents != null) { if (root['contents'] != null) {
var contents = root.contents.twoColumnSearchResultsRenderer var contents = root
.primaryContents.sectionListRenderer.contents; .get('contents')
?.get('twoColumnSearchResultsRenderer')
?.get('primaryContents')
?.get('sectionListRenderer')
?.getList('contents');
if (contents.length <= 1) { if (contents == null || contents.length <= 1) {
return null; return null;
} }
return contents[1] return contents
.continuationItemRenderer .elementAtSafe(1)
.continuationEndpoint ?.get('continuationItemRenderer')
.continuationCommand ?.get('continuationEndpoint')
.token; ?.get('continuationCommand')
?.getT<String>('token');
} }
if (root.onResponseReceivedCommands != null) { if (root['onResponseReceivedCommands'] != null) {
return root return root
.onResponseReceivedCommands .getList('onResponseReceivedCommands')
.first ?.firstOrNull
.appendContinuationItemsAction ?.get('appendContinuationItemsAction')
.continuationItems[1] ?.getList('continuationItems')
?.continuationItemRenderer ?.elementAtSafe(1)
?.continuationEndpoint ?.get('continuationItemRenderer')
?.continuationCommand ?.get('continuationEndpoint')
?.token ?? ?.get('continuationCommand')
' '; ?.getT<String>('token');
} }
return null; return null;
} }
// Contains only [SearchVideo] or [SearchPlaylist] // Contains only [SearchVideo] or [SearchPlaylist]
List<BaseSearchContent> get searchContent => late final List<BaseSearchContent> searchContent =
getContentContext().map(_parseContent).where((e) => e != null).toList(); getContentContext()?.map(_parseContent).whereNotNull().toList() ??
const [];
List<RelatedQuery> get relatedQueries => List<RelatedQuery> get relatedQueries =>
getContentContext() getContentContext()
?.where((e) => e.horizontalCardListRenderer != null) ?.where((e) => e['horizontalCardListRenderer'] != null)
?.map((e) => e.horizontalCardListRenderer.cards) .map((e) => e.get('horizontalCardListRenderer')?.getList('cards'))
?.firstOrNull .firstOrNull
?.map((e) => e.searchRefinementCardRenderer) ?.map((e) => e['searchRefinementCardRenderer'])
?.map((e) => RelatedQuery( .map((e) => RelatedQuery(
e.searchEndpoint.searchEndpoint.query, e.searchEndpoint.searchEndpoint.query,
VideoId( VideoId(
Uri.parse(e.thumbnail.thumbnails.first.url).pathSegments[1]))) Uri.parse(e.thumbnail.thumbnails.first.url).pathSegments[1])))
?.toList() .toList()
?.cast<RelatedQuery>() ?? .cast<RelatedQuery>() ??
const []; const [];
List<dynamic> get relatedVideos => List<dynamic> get relatedVideos =>
getContentContext() getContentContext()
?.where((e) => e.shelfRenderer != null) ?.where((e) => e['shelfRenderer'] != null)
?.map((e) => e.shelfRenderer.content.verticalListRenderer.items) .map((e) => e
?.firstOrNull .get('shelfRenderer')
?.get('content')
?.get('verticalListRenderer')
?.getList('items'))
.firstOrNull
?.map(_parseContent) ?.map(_parseContent)
?.toList() ?? .whereNotNull()
.toList() ??
const []; const [];
String get continuationToken => _getContinuationToken(); late final String? continuationToken = _getContinuationToken();
int get estimatedResults => int.parse(root.estimatedResults ?? 0); late final int estimatedResults =
int.parse(root.getT<String>('estimatedResults') ?? '0');
BaseSearchContent _parseContent(PurpleContent content) { BaseSearchContent? _parseContent(Map<String, dynamic>? content) {
if (content == null) { if (content == null) {
return null; return null;
} }
if (content.videoRenderer != null) { if (content['videoRenderer'] != null) {
var renderer = content.videoRenderer; var renderer = content.get('videoRenderer')!;
//TODO: Add if it's a live
return SearchVideo( return SearchVideo(
VideoId(renderer.videoId), VideoId(renderer.getT<String>('videoId')!),
_parseRuns(renderer.title.runs), _parseRuns(renderer.get('title')?.getList('runs')),
_parseRuns(renderer.ownerText.runs), _parseRuns(renderer.get('ownerText')?.getList('runs')),
_parseRuns(renderer.descriptionSnippet?.runs), _parseRuns(renderer.get('descriptionSnippet')?.getList('runs')),
renderer.lengthText?.simpleText ?? '', renderer.get('lengthText')?.getT<String>('simpleText') ?? '',
int.parse(renderer.viewCountText?.simpleText int.parse(renderer
.get('viewCountText')
?.getT<String>('simpleText')
?.stripNonDigits() ?.stripNonDigits()
?.nullIfWhitespace ?? .nullIfWhitespace ??
renderer.viewCountText?.runs?.first?.text renderer
.get('viewCountText')
?.getList('runs')
?.firstOrNull
?.getT<String>('text')
?.stripNonDigits() ?.stripNonDigits()
?.nullIfWhitespace ?? .nullIfWhitespace ??
'0'), '0'),
(renderer.thumbnail.thumbnails ?? <ThumbnailElement>[]) (renderer.get('thumbnail')?.getList('thumbnails') ?? const [])
.map((e) => Thumbnail(Uri.parse(e.url), e.height, e.width)) .map((e) =>
Thumbnail(Uri.parse(e['url']), e['height'], e['width']))
.toList(), .toList(),
renderer.publishedTimeText?.simpleText, renderer.get('publishedTimeText')?.getT<String>('simpleText'),
renderer?.viewCountText?.runs?.elementAt(1)?.text?.trim() == renderer
.get('viewCountText')
?.getList('runs')
?.elementAtSafe(1)
?.getT<String>('text')
?.trim() ==
'watching'); 'watching');
} }
if (content.radioRenderer != null) { if (content['radioRenderer'] != null) {
var renderer = content.radioRenderer; var renderer = content.get('radioRenderer')!;
return SearchPlaylist( return SearchPlaylist(
PlaylistId(renderer.playlistId), PlaylistId(renderer.getT<String>('playlistId')!),
renderer.title.simpleText, renderer.get('title')!.getT<String>('simpleText')!,
int.parse(_parseRuns(renderer.videoCountText.runs) int.parse(_parseRuns(renderer.get('videoCountText')?.getList('runs'))
.stripNonDigits() .stripNonDigits()
.nullIfWhitespace ?? .nullIfWhitespace ??
'0')); '0'));
@ -276,6 +288,6 @@ class _InitialData {
return null; return null;
} }
String _parseRuns(List<dynamic> runs) => String _parseRuns(List<dynamic>? runs) =>
runs?.map((e) => e.text)?.join() ?? ''; runs?.map((e) => e['text']).join() ?? '';
} }

View File

@ -10,57 +10,35 @@ abstract class StreamInfoProvider {
String get url; String get url;
/// ///
// Can be null String? get signature => null;
// ignore: avoid_returning_null
String get signature => null;
/// ///
// Can be null String? get signatureParameter => null;
// ignore: avoid_returning_null
String get signatureParameter => null;
/// ///
// Can be null int? get contentLength => null;
// ignore: avoid_returning_null
int get contentLength => null;
/// ///
// Can be null int? get bitrate;
// ignore: avoid_returning_null
int get bitrate;
/// ///
// Can be null String? get container;
// ignore: avoid_returning_null
String get container;
/// ///
// Can be null String? get audioCodec => null;
// ignore: avoid_returning_null
String get audioCodec => null;
/// ///
// Can be null String? get videoCodec => null;
// ignore: avoid_returning_null
String get videoCodec => null;
/// ///
// Can be null String? get videoQualityLabel => null;
// ignore: avoid_returning_null
String get videoQualityLabel => null;
/// ///
// Can be null int? get videoWidth => null;
// ignore: avoid_returning_null
int get videoWidth => null;
/// ///
// Can be null int? get videoHeight => null;
// ignore: avoid_returning_null
int get videoHeight => null;
/// ///
// Can be null int? get framerate => null;
// ignore: avoid_returning_null
int get framerate => null;
} }

View File

@ -1,6 +1,7 @@
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import '../../exceptions/exceptions.dart'; import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart'; import '../../retry.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
import 'player_response.dart'; import 'player_response.dart';
@ -8,56 +9,49 @@ import 'stream_info_provider.dart';
/// ///
class VideoInfoResponse { class VideoInfoResponse {
final Map<String, String> _root; final Map<String, String> root;
String _status;
bool _isVideoAvailable;
PlayerResponse _playerResponse;
Iterable<_StreamInfo> _muxedStreams;
Iterable<_StreamInfo> _adaptiveStreams;
Iterable<_StreamInfo> _streams;
/// ///
String get status => _status ??= _root['status']; late final String status = root['status']!;
/// ///
bool get isVideoAvailable => late final bool isVideoAvailable = status.toLowerCase() != 'fail';
_isVideoAvailable ??= status.toLowerCase() != 'fail';
/// ///
PlayerResponse get playerResponse => late final PlayerResponse playerResponse =
_playerResponse ??= PlayerResponse.parse(_root['player_response']); PlayerResponse.parse(root['player_response']!);
/// ///
Iterable<_StreamInfo> get muxedStreams => late final Iterable<_StreamInfo> muxedStreams =
_muxedStreams ??= _root['url_encoded_fmt_stream_map'] root['url_encoded_fmt_stream_map']
?.split(',') ?.split(',')
?.map(Uri.splitQueryString) .map(Uri.splitQueryString)
?.map((e) => _StreamInfo(e)) ?? .map((e) => _StreamInfo(e)) ??
const []; const [];
/// ///
Iterable<_StreamInfo> get adaptiveStreams => late final Iterable<_StreamInfo> adaptiveStreams = root['adaptive_fmts']
_adaptiveStreams ??= _root['adaptive_fmts'] ?.split(',')
?.split(',') .map(Uri.splitQueryString)
?.map(Uri.splitQueryString) .map((e) => _StreamInfo(e)) ??
?.map((e) => _StreamInfo(e)) ?? const [];
const [];
/// ///
Iterable<_StreamInfo> get streams => late final Iterable<_StreamInfo> streams = [
_streams ??= [...muxedStreams, ...adaptiveStreams]; ...muxedStreams,
...adaptiveStreams
];
/// ///
VideoInfoResponse(this._root); VideoInfoResponse(this.root);
/// ///
VideoInfoResponse.parse(String raw) : _root = Uri.splitQueryString(raw); VideoInfoResponse.parse(String raw) : root = Uri.splitQueryString(raw);
/// ///
static Future<VideoInfoResponse> get( static Future<VideoInfoResponse> get(
YoutubeHttpClient httpClient, String videoId, YoutubeHttpClient httpClient, String videoId,
[String sts]) { [String? sts]) {
var eurl = Uri.encodeFull('https://youtube.googleapis.com/v/$videoId'); var eurl = Uri.encodeFull('https://youtube.googleapis.com/v/$videoId');
var url = var url =
'https://youtube.com/get_video_info?video_id=$videoId&el=embedded&eurl=$eurl&hl=en${sts != null ? '&sts=$sts' : ''}'; 'https://youtube.com/get_video_info?video_id=$videoId&el=embedded&eurl=$eurl&hl=en${sts != null ? '&sts=$sts' : ''}';
@ -74,75 +68,65 @@ class VideoInfoResponse {
} }
class _StreamInfo extends StreamInfoProvider { class _StreamInfo extends StreamInfoProvider {
final Map<String, String> _root; final Map<String, String> root;
int _tag;
String _url;
String _signature;
String _signatureParameter;
int _contentLength;
int _bitrate;
MediaType _mimeType;
String _container;
List<String> _codecs;
String _audioCodec;
String _videoCodec;
bool _isAudioOnly;
String _videoQualityLabel;
List<int> __size;
int _videoWidth;
int _videoHeight;
int _framerate;
@override @override
int get tag => _tag ??= int.parse(_root['itag']); late final int tag = int.parse(root['itag']!);
@override @override
String get url => _url ??= _root['url']; late final String url = root['url']!;
@override @override
String get signature => _signature ??= _root['s']; late final String? signature = root['s'];
@override @override
String get signatureParameter => _signatureParameter ??= _root['sp']; late final String? signatureParameter = root['sp'];
@override @override
int get contentLength => _contentLength ??= int.tryParse(_root['clen'] ?? late final int? contentLength = int.tryParse(root['clen'] ??
StreamInfoProvider.contentLenExp.firstMatch(url).group(1)); StreamInfoProvider.contentLenExp.firstMatch(url)?.group(1) ??
'');
@override @override
int get bitrate => _bitrate ??= int.parse(_root['bitrate']); late final int? bitrate = int.tryParse(root['bitrate'] ?? '');
MediaType get mimeType => _mimeType ??= MediaType.parse(_root['type']); late final MediaType mimeType = MediaType.parse(root['type']!);
@override @override
String get container => _container ??= mimeType.subtype; late final String container = mimeType.subtype;
List<String> get codecs => late final List<String> codecs = mimeType.parameters['codecs']!
_codecs ??= mimeType.parameters['codecs'].split(',').map((e) => e.trim()); .split(',')
.map((e) => e.trim())
.toList()
.cast<String>();
@override @override
String get audioCodec => _audioCodec ??= codecs.last; late final String audioCodec = codecs.last;
@override @override
String get videoCodec => _videoCodec ??= isAudioOnly ? null : codecs.first; late final String? videoCodec = isAudioOnly ? null : codecs.first;
bool get isAudioOnly => _isAudioOnly ??= mimeType.type == 'audio'; late final bool isAudioOnly = mimeType.type == 'audio';
@override @override
String get videoQualityLabel => _videoQualityLabel ??= _root['quality_label']; late final String? videoQualityLabel = root['quality_label'];
List<int> get _size => late final List<int>? _size = root
__size ??= _root['size'].split(',').map((e) => int.tryParse(e ?? '')); .getT<String>('size')
?.split(',')
.map((e) => int.tryParse(e))
.toList()
.cast<int>();
@override @override
int get videoWidth => _videoWidth ??= _size.first; late final int? videoWidth = _size?.first;
@override @override
int get videoHeight => _videoHeight ??= _size.last; late final int? videoHeight = _size?.last;
@override @override
int get framerate => _framerate ??= int.tryParse(_root['fps'] ?? ''); late final int? framerate = int.tryParse(root['fps'] ?? '');
_StreamInfo(this._root); _StreamInfo(this.root);
} }

View File

@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser; import 'package:html/parser.dart' as parser;
@ -6,8 +9,6 @@ import '../../extensions/helpers_extension.dart';
import '../../retry.dart'; import '../../retry.dart';
import '../../videos/video_id.dart'; import '../../videos/video_id.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
import 'generated/player_response_json.g.dart';
import 'generated/watch_page_id.g.dart';
import 'player_config_base.dart'; import 'player_config_base.dart';
import 'player_response.dart'; import 'player_response.dart';
@ -25,7 +26,7 @@ class WatchPage {
static final _xsfrTokenExp = RegExp(r'"XSRF_TOKEN"\s*:\s*"(.+?)"'); static final _xsfrTokenExp = RegExp(r'"XSRF_TOKEN"\s*:\s*"(.+?)"');
final Document _root; final Document root;
/// ///
final String visitorInfoLive; final String visitorInfoLive;
@ -33,122 +34,131 @@ class WatchPage {
/// ///
final String ysc; final String ysc;
_InitialData _initialData; _InitialData? _initialData;
String _xsfrToken;
WatchPlayerConfig _playerConfig;
/// ///
String get sourceUrl { String? get sourceUrl {
var url = _root var url = root
.querySelectorAll('script') .querySelectorAll('script')
.map((e) => e.attributes['src']) .map((e) => e.attributes['src'])
.where((e) => !e.isNullOrWhiteSpace) .whereNotNull()
.firstWhere((e) => e.contains('player_ias') && e.endsWith('.js'), .firstWhereOrNull((e) => e.contains('player_ias') && e.endsWith('.js'));
orElse: () => null);
if (url == null) { if (url == null) {
return null; return null;
} }
return 'https://youtube.com$url'; return 'https://youtube.com$url';
} }
late final _InitialData initialData = getInitialData();
/// ///
_InitialData get initialData { _InitialData getInitialData() {
if (_initialData != null) { if (_initialData != null) {
return _initialData; return _initialData!;
} }
final scriptText = _root final scriptText = root
.querySelectorAll('script') .querySelectorAll('script')
.map((e) => e.text) .map((e) => e.text)
.toList(growable: false); .toList(growable: false);
var initialDataText = scriptText.firstWhere( var initialDataText = scriptText
(e) => e.contains('window["ytInitialData"] ='), .firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='));
orElse: () => null);
if (initialDataText != null) { if (initialDataText != null) {
return _initialData = _InitialData(WatchPageId.fromRawJson( return _initialData = _InitialData(json
_extractJson(initialDataText, 'window["ytInitialData"] ='))); .decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
} }
initialDataText = scriptText.firstWhere( initialDataText =
(e) => e.contains('var ytInitialData = '), scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = '));
orElse: () => null);
if (initialDataText != null) { if (initialDataText != null) {
return _initialData = _InitialData(WatchPageId.fromRawJson( return _initialData = _InitialData(
_extractJson(initialDataText, 'var ytInitialData = '))); json.decode(_extractJson(initialDataText, 'var ytInitialData = ')));
} }
throw TransientFailureException( throw TransientFailureException(
'Failed to retrieve initial data from the watch page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars 'Failed to retrieve initial data from the watch page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars
} }
/// late final String xsfrToken = getXsfrToken()!;
String get xsfrToken => _xsfrToken ??= _xsfrTokenExp
.firstMatch(_root
.querySelectorAll('script')
.firstWhere((e) => _xsfrTokenExp.hasMatch(e.text))
.text)
.group(1);
/// ///
bool get isOk => _root.body.querySelector('#player') != null; String? getXsfrToken() {
return _xsfrTokenExp
.firstMatch(root
.querySelectorAll('script')
.firstWhere((e) => _xsfrTokenExp.hasMatch(e.text))
.text)
?.group(1);
}
///
bool get isOk => root.body?.querySelector('#player') != null;
/// ///
bool get isVideoAvailable => bool get isVideoAvailable =>
_root.querySelector('meta[property="og:url"]') != null; root.querySelector('meta[property="og:url"]') != null;
/// ///
int get videoLikeCount => int.parse(_videoLikeExp int get videoLikeCount => int.parse(_videoLikeExp
.firstMatch(_root.outerHtml) .firstMatch(root.outerHtml)
?.group(1) ?.group(1)
?.stripNonDigits() ?.stripNonDigits()
?.nullIfWhitespace ?? .nullIfWhitespace ??
_root root
.querySelector('.like-button-renderer-like-button') .querySelector('.like-button-renderer-like-button')
?.text ?.text
?.stripNonDigits() .stripNonDigits()
?.nullIfWhitespace ?? .nullIfWhitespace ??
'0'); '0');
/// ///
int get videoDislikeCount => int.parse(_videoDislikeExp int get videoDislikeCount => int.parse(_videoDislikeExp
.firstMatch(_root.outerHtml) .firstMatch(root.outerHtml)
?.group(1) ?.group(1)
?.stripNonDigits() ?.stripNonDigits()
?.nullIfWhitespace ?? .nullIfWhitespace ??
_root root
.querySelector('.like-button-renderer-dislike-button') .querySelector('.like-button-renderer-dislike-button')
?.text ?.text
?.stripNonDigits() .stripNonDigits()
?.nullIfWhitespace ?? .nullIfWhitespace ??
'0'); '0');
static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\})'); static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\})');
/// late final WatchPlayerConfig playerConfig = WatchPlayerConfig(json.decode(
WatchPlayerConfig get playerConfig => _playerConfig ??= WatchPlayerConfig( _playerConfigExp
PlayerConfigJson.fromRawJson(_playerConfigExp .firstMatch(root.getElementsByTagName('html').first.text)
.firstMatch(_root.getElementsByTagName('html').first.text) ?.group(1)
?.group(1) ?.extractJson() ??
?.extractJson())); 'a'));
late final PlayerResponse? playerResponse = getPlayerResponse();
/// ///
PlayerResponse get playerResponse => PlayerResponse.parse(_root PlayerResponse? getPlayerResponse() {
.querySelectorAll('script') final val = root
.map((e) => e.text) .querySelectorAll('script')
.map((e) => _playerResponseExp.firstMatch(e)?.group(1)) .map((e) => e.text)
.firstWhere((e) => !e.isNullOrWhiteSpace) .map((e) => _playerResponseExp.firstMatch(e)?.group(1))
.extractJson()); .firstWhereOrNull((e) => !e.isNullOrWhiteSpace)
?.extractJson();
if (val == null) {
return null;
}
return PlayerResponse.parse(val);
}
String _extractJson(String html, String separator) => String _extractJson(String html, String separator) =>
html.substring(html.indexOf(separator) + separator.length).extractJson(); html.substring(html.indexOf(separator) + separator.length).extractJson();
/// ///
WatchPage(this._root, this.visitorInfoLive, this.ysc); WatchPage(this.root, this.visitorInfoLive, this.ysc);
/// ///
WatchPage.parse(String raw, this.visitorInfoLive, this.ysc) WatchPage.parse(String raw, this.visitorInfoLive, this.ysc)
: _root = parser.parse(raw); : root = parser.parse(raw);
/// ///
static Future<WatchPage> get(YoutubeHttpClient httpClient, String videoId) { static Future<WatchPage> get(YoutubeHttpClient httpClient, String videoId) {
@ -156,9 +166,9 @@ class WatchPage {
return retry(() async { return retry(() async {
var req = await httpClient.get(url, validate: true); var req = await httpClient.get(url, validate: true);
var cookies = req.headers['set-cookie']; var cookies = req.headers['set-cookie']!;
var visitorInfoLive = _visitorInfoLiveExp.firstMatch(cookies).group(1); var visitorInfoLive = _visitorInfoLiveExp.firstMatch(cookies)!.group(1)!;
var ysc = _yscExp.firstMatch(cookies).group(1); var ysc = _yscExp.firstMatch(cookies)!.group(1)!;
var result = WatchPage.parse(req.body, visitorInfoLive, ysc); var result = WatchPage.parse(req.body, visitorInfoLive, ysc);
if (!result.isOk) { if (!result.isOk) {
@ -174,47 +184,48 @@ class WatchPage {
} }
/// Used internally /// Used internally
class WatchPlayerConfig implements PlayerConfigBase<PlayerConfigJson> { class WatchPlayerConfig implements PlayerConfigBase<Map<String, dynamic>> {
@override @override
final PlayerConfigJson root; final Map<String, dynamic> root;
/// ///
WatchPlayerConfig(this.root); WatchPlayerConfig(this.root);
@override @override
String get sourceUrl => 'https://youtube.com${root.assets.js}'; late final String sourceUrl =
'https://youtube.com${root.get('assets')!.getT<String>('js')}';
/// ///
PlayerResponse get playerResponse => late final PlayerResponse playerResponse =
PlayerResponse.parse(root.args.playerResponse); PlayerResponse.parse(root.get('args')!.getT<String>('playerResponse')!);
} }
class _InitialData { class _InitialData {
// Json parsed map // Json parsed map
final WatchPageId root; final Map<String, dynamic> root;
_InitialData(this.root); _InitialData(this.root);
/* Cache results */ Map<String, dynamic>? getContinuationContext() {
if (root['contents'] != null) {
String _continuation; return root
String _clickTrackingParams; .get('contents')
?.get('twoColumnWatchNextResults')
NextContinuationData getContinuationContext() { ?.get('results')
if (root.contents != null) { ?.get('results')
return root.contents.twoColumnWatchNextResults.results.results.contents ?.getList('contents')
.firstWhere((e) => e.itemSectionRenderer != null) ?.firstWhere((e) => e['itemSectionRenderer'] != null)
.itemSectionRenderer .get('itemSectionRenderer')
.continuations ?.getList('continuations')
.first ?.firstOrNull
.nextContinuationData; ?.get('nextContinuationData');
} }
return null; return null;
} }
String get continuation => late final String continuation =
_continuation ??= getContinuationContext()?.continuation ?? ''; getContinuationContext()?.getT<String>('continuation') ?? '';
String get clickTrackingParams => _clickTrackingParams ??= late final String clickTrackingParams =
getContinuationContext()?.clickTrackingParams ?? ''; getContinuationContext()?.getT<String>('clickTrackingParams') ?? '';
} }

View File

@ -63,6 +63,10 @@ class YoutubeHttpClient extends http.BaseClient {
@override @override
Future<http.Response> get(dynamic url, Future<http.Response> get(dynamic url,
{Map<String, String>? headers = const {}, bool validate = false}) async { {Map<String, String>? headers = const {}, bool validate = false}) async {
assert(url is String || url is Uri);
if (url is String) {
url = Uri.parse(url);
}
var response = await super.get(url, headers: headers); var response = await super.get(url, headers: headers);
if (validate) { if (validate) {
_validateResponse(response, response.statusCode); _validateResponse(response, response.statusCode);

View File

@ -18,8 +18,7 @@ class SearchClient {
/// The videos are sent in batch of 20 videos. /// The videos are sent in batch of 20 videos.
/// You [SearchList.nextPage] to get the next batch of videos. /// You [SearchList.nextPage] to get the next batch of videos.
Future<SearchList> getVideos(String searchQuery) { Future<SearchList> getVideos(String searchQuery) {
var stream = var stream = getVideosFromPage(searchQuery).cast<SearchVideo>();
getVideosFromPage(searchQuery, onlyVideos: true).cast<SearchVideo>();
return SearchList.create(stream); return SearchList.create(stream);
} }
@ -28,18 +27,18 @@ class SearchClient {
/// Contains only instances of [SearchVideo] or [SearchPlaylist] /// Contains only instances of [SearchVideo] or [SearchPlaylist]
Stream<BaseSearchContent> getVideosFromPage(String searchQuery, Stream<BaseSearchContent> getVideosFromPage(String searchQuery,
{bool onlyVideos = true}) async* { {bool onlyVideos = true}) async* {
var page = SearchPage? page =
await retry(() async => SearchPage.get(_httpClient, searchQuery)); await retry(() async => SearchPage.get(_httpClient, searchQuery));
if (onlyVideos) { if (onlyVideos) {
yield* Stream.fromIterable( yield* Stream.fromIterable(
page.initialData.searchContent.whereType<SearchVideo>()); page!.initialData.searchContent.whereType<SearchVideo>());
} else { } else {
yield* Stream.fromIterable(page.initialData.searchContent); yield* Stream.fromIterable(page!.initialData.searchContent);
} }
// ignore: literal_only_boolean_expressions // ignore: literal_only_boolean_expressions
while (true) { while (true) {
page = await page.nextPage(_httpClient); page = await page!.nextPage(_httpClient);
if (page == null) { if (page == null) {
return; return;
} }

View File

@ -43,13 +43,14 @@ class SearchList extends DelegatingList<Video> {
} }
/// Format: <quantity> <unit> ago (5 years ago) /// Format: <quantity> <unit> ago (5 years ago)
static DateTime _stringToDateTime(String string) { static DateTime? _stringToDateTime(String? string) {
if (string == null) { if (string == null) {
return null; return null;
} }
var parts = string.split(' '); var parts = string.split(' ');
if (parts.length == 4) { // Streamed x y ago if (parts.length == 4) {
// Streamed x y ago
parts = parts.skip(1).toList(); parts = parts.skip(1).toList();
} }
assert(parts.length == 3); assert(parts.length == 3);
@ -82,8 +83,8 @@ class SearchList extends DelegatingList<Video> {
} }
/// Format: HH:MM:SS /// Format: HH:MM:SS
static Duration _stringToDuration(String string) { static Duration? _stringToDuration(String string) {
if (string == null || string.trim().isEmpty) { if (/*string == null ||*/ string.trim().isEmpty) {
return null; return null;
} }

View File

@ -23,7 +23,7 @@ class SearchQuery {
/// Get the data of the next page. /// Get the data of the next page.
/// Returns null if there is no next page. /// Returns null if there is no next page.
Future<SearchQuery> nextPage() async { Future<SearchQuery?> nextPage() async {
var page = await _page.nextPage(_httpClient); var page = await _page.nextPage(_httpClient);
if (page == null) { if (page == null) {
return null; return null;

View File

@ -26,7 +26,7 @@ class SearchVideo extends BaseSearchContent {
final List<Thumbnail> thumbnails; final List<Thumbnail> thumbnails;
/// Video upload date - As string: 5 years ago. /// Video upload date - As string: 5 years ago.
final String uploadDate; final String? uploadDate;
/// True if this video is a live stream. /// True if this video is a live stream.
final bool isLive; final bool isLive;

View File

@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'closed_caption_part.dart'; import 'closed_caption_part.dart';
@ -35,8 +36,8 @@ class ClosedCaption {
/// relative to this caption's offset. /// relative to this caption's offset.
/// Returns null if not found. /// Returns null if not found.
/// Note that some captions may not have any parts at all. /// Note that some captions may not have any parts at all.
ClosedCaptionPart getPartByTime(Duration offset) => ClosedCaptionPart? getPartByTime(Duration offset) =>
parts.firstWhere((e) => e.offset >= offset, orElse: () => null)); parts.firstWhereOrNull((e) => e.offset >= offset);
@override @override
String toString() => 'Text: $text'; String toString() => 'Text: $text';

View File

@ -9,22 +9,17 @@ part of 'closed_caption.dart';
ClosedCaption _$ClosedCaptionFromJson(Map<String, dynamic> json) { ClosedCaption _$ClosedCaptionFromJson(Map<String, dynamic> json) {
return ClosedCaption( return ClosedCaption(
json['text'] as String, json['text'] as String,
json['offset'] == null Duration(microseconds: json['offset'] as int),
? null Duration(microseconds: json['duration'] as int),
: Duration(microseconds: json['offset'] as int), (json['parts'] as List<dynamic>)
json['duration'] == null .map((e) => ClosedCaptionPart.fromJson(e as Map<String, dynamic>)),
? null
: Duration(microseconds: json['duration'] as int),
(json['parts'] as List)?.map((e) => e == null
? null
: ClosedCaptionPart.fromJson(e as Map<String, dynamic>)),
); );
} }
Map<String, dynamic> _$ClosedCaptionToJson(ClosedCaption instance) => Map<String, dynamic> _$ClosedCaptionToJson(ClosedCaption instance) =>
<String, dynamic>{ <String, dynamic>{
'text': instance.text, 'text': instance.text,
'offset': instance.offset?.inMicroseconds, 'offset': instance.offset.inMicroseconds,
'duration': instance.duration?.inMicroseconds, 'duration': instance.duration.inMicroseconds,
'parts': instance.parts, 'parts': instance.parts,
}; };

View File

@ -1,5 +1,3 @@
import 'package:xml/xml.dart' as xml;
import '../../extensions/helpers_extension.dart'; import '../../extensions/helpers_extension.dart';
import '../../reverse_engineering/responses/responses.dart' import '../../reverse_engineering/responses/responses.dart'
hide ClosedCaption, ClosedCaptionPart, ClosedCaptionTrack; hide ClosedCaption, ClosedCaptionPart, ClosedCaptionTrack;
@ -39,8 +37,8 @@ class ClosedCaptionClient {
await VideoInfoResponse.get(_httpClient, videoId.value); await VideoInfoResponse.get(_httpClient, videoId.value);
var playerResponse = videoInfoResponse.playerResponse; var playerResponse = videoInfoResponse.playerResponse;
for (var track in playerResponse.closedCaptionTrack) { for (final track in playerResponse.closedCaptionTrack) {
for (var ext in formats) { for (final ext in formats) {
tracks.add(ClosedCaptionTrackInfo( tracks.add(ClosedCaptionTrackInfo(
Uri.parse(track.url) Uri.parse(track.url)
.replaceQueryParameters({'fmt': ext.formatCode}), .replaceQueryParameters({'fmt': ext.formatCode}),
@ -61,7 +59,7 @@ class ClosedCaptionClient {
var captions = response.closedCaptions var captions = response.closedCaptions
.where((e) => !e.text.isNullOrWhiteSpace) .where((e) => !e.text.isNullOrWhiteSpace)
.map((e) => ClosedCaption(e.text, e.offset, e.duration, .map((e) => ClosedCaption(e.text, e.offset, e.duration,
e.getParts().map((f) => ClosedCaptionPart(f.text, f.offset)))); e.parts.map((f) => ClosedCaptionPart(f.text, f.offset))));
return ClosedCaptionTrack(captions); return ClosedCaptionTrack(captions);
} }

View File

@ -17,7 +17,7 @@ class ClosedCaptionManifest {
/// If [autoGenerated] is true auto generated tracks are included as well. /// If [autoGenerated] is true auto generated tracks are included as well.
/// Returns an empty list of no track is found. /// Returns an empty list of no track is found.
List<ClosedCaptionTrackInfo> getByLanguage(String language, List<ClosedCaptionTrackInfo> getByLanguage(String language,
{ClosedCaptionFormat format, bool autoGenerated = false}) { {ClosedCaptionFormat? format, bool autoGenerated = false}) {
language = language.toLowerCase(); language = language.toLowerCase();
return tracks return tracks
.where((e) => .where((e) =>

View File

@ -9,14 +9,12 @@ part of 'closed_caption_part.dart';
ClosedCaptionPart _$ClosedCaptionPartFromJson(Map<String, dynamic> json) { ClosedCaptionPart _$ClosedCaptionPartFromJson(Map<String, dynamic> json) {
return ClosedCaptionPart( return ClosedCaptionPart(
json['text'] as String, json['text'] as String,
json['offset'] == null Duration(microseconds: json['offset'] as int),
? null
: Duration(microseconds: json['offset'] as int),
); );
} }
Map<String, dynamic> _$ClosedCaptionPartToJson(ClosedCaptionPart instance) => Map<String, dynamic> _$ClosedCaptionPartToJson(ClosedCaptionPart instance) =>
<String, dynamic>{ <String, dynamic>{
'text': instance.text, 'text': instance.text,
'offset': instance.offset?.inMicroseconds, 'offset': instance.offset.inMicroseconds,
}; };

View File

@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'closed_caption.dart'; import 'closed_caption.dart';
@ -18,8 +19,8 @@ class ClosedCaptionTrack {
/// Gets the caption displayed at the specified point in time. /// Gets the caption displayed at the specified point in time.
/// Returns null if not found. /// Returns null if not found.
ClosedCaption getByTime(Duration time) => captions ClosedCaption? getByTime(Duration time) =>
.firstWhere((e) => time >= e.offset && time <= e.end, orElse: () => null); captions.firstWhereOrNull((e) => time >= e.offset && time <= e.end);
/// ///
factory ClosedCaptionTrack.fromJson(Map<String, dynamic> json) => factory ClosedCaptionTrack.fromJson(Map<String, dynamic> json) =>

View File

@ -8,8 +8,8 @@ part of 'closed_caption_track.dart';
ClosedCaptionTrack _$ClosedCaptionTrackFromJson(Map<String, dynamic> json) { ClosedCaptionTrack _$ClosedCaptionTrackFromJson(Map<String, dynamic> json) {
return ClosedCaptionTrack( return ClosedCaptionTrack(
(json['captions'] as List)?.map((e) => (json['captions'] as List<dynamic>)
e == null ? null : ClosedCaption.fromJson(e as Map<String, dynamic>)), .map((e) => ClosedCaption.fromJson(e as Map<String, dynamic>)),
); );
} }

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
@ -26,8 +24,7 @@ class ClosedCaptionTrackInfo extends Equatable {
/// Initializes an instance of [ClosedCaptionTrackInfo] /// Initializes an instance of [ClosedCaptionTrackInfo]
const ClosedCaptionTrackInfo(this.url, this.language, const ClosedCaptionTrackInfo(this.url, this.language,
{this.isAutoGenerated = false, this.format}) {this.isAutoGenerated = false, required this.format});
: assert(format != null);
/// Returns this auto-translated to another language. /// Returns this auto-translated to another language.
/// Keeping the same format. /// Keeping the same format.

View File

@ -9,21 +9,18 @@ part of 'closed_caption_track_info.dart';
ClosedCaptionTrackInfo _$ClosedCaptionTrackInfoFromJson( ClosedCaptionTrackInfo _$ClosedCaptionTrackInfoFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) {
return ClosedCaptionTrackInfo( return ClosedCaptionTrackInfo(
json['url'] == null ? null : Uri.parse(json['url'] as String), Uri.parse(json['url'] as String),
json['language'] == null Language.fromJson(json['language'] as Map<String, dynamic>),
? null
: Language.fromJson(json['language'] as Map<String, dynamic>),
isAutoGenerated: json['isAutoGenerated'] as bool, isAutoGenerated: json['isAutoGenerated'] as bool,
format: json['format'] == null format:
? null ClosedCaptionFormat.fromJson(json['format'] as Map<String, dynamic>),
: ClosedCaptionFormat.fromJson(json['format'] as Map<String, dynamic>),
); );
} }
Map<String, dynamic> _$ClosedCaptionTrackInfoToJson( Map<String, dynamic> _$ClosedCaptionTrackInfoToJson(
ClosedCaptionTrackInfo instance) => ClosedCaptionTrackInfo instance) =>
<String, dynamic>{ <String, dynamic>{
'url': instance.url?.toString(), 'url': instance.url.toString(),
'language': instance.language, 'language': instance.language,
'isAutoGenerated': instance.isAutoGenerated, 'isAutoGenerated': instance.isAutoGenerated,
'format': instance.format, 'format': instance.format,

View File

@ -27,11 +27,11 @@ class Comment with EquatableMixin {
/// Used internally. /// Used internally.
/// Shouldn't be used in the code. /// Shouldn't be used in the code.
final String continuation; final String? continuation;
/// Used internally. /// Used internally.
/// Shouldn't be used in the code. /// Shouldn't be used in the code.
final String clicktrackingParams; final String? clicktrackingParams;
/// Initializes an instance of [Comment] /// Initializes an instance of [Comment]
Comment( Comment(

View File

@ -52,11 +52,11 @@ class CommentsClient {
return; return;
} }
yield* _getComments( yield* _getComments(
video.watchPage.initialData.continuation, video.watchPage!.initialData.continuation,
video.watchPage.initialData.clickTrackingParams, video.watchPage!.initialData.clickTrackingParams,
video.watchPage.xsfrToken, video.watchPage!.xsfrToken,
video.watchPage.visitorInfoLive, video.watchPage!.visitorInfoLive,
video.watchPage.ysc); video.watchPage!.ysc);
} }
Stream<Comment> _getComments(String continuation, String clickTrackingParams, Stream<Comment> _getComments(String continuation, String clickTrackingParams,
@ -67,14 +67,14 @@ class CommentsClient {
['itemSectionContinuation']['contents'] ['itemSectionContinuation']['contents']
?.map((e) => e['commentThreadRenderer']) ?.map((e) => e['commentThreadRenderer'])
?.toList() ?.toList()
?.cast<Map<String, dynamic>>() as List<Map<String, dynamic>>; ?.cast<Map<String, dynamic>>() as List<Map<String, dynamic>>?;
if (contentRoot == null) { if (contentRoot == null) {
return; return;
} }
for (var content in contentRoot) { for (final content in contentRoot) {
var commentRaw = content['comment']['commentRenderer']; var commentRaw = content['comment']['commentRenderer'];
String continuation; String? continuation;
String clickTrackingParams; String? clickTrackingParams;
if (content['replies'] != null) { if (content['replies'] != null) {
continuation = content['replies']['commentRepliesRenderer'] continuation = content['replies']['commentRepliesRenderer']
['continuations'] ['continuations']
@ -96,12 +96,12 @@ class CommentsClient {
yield comment; yield comment;
} }
var continuationRoot = (data var continuationRoot = (data
?.get('response') .get('response')
?.get('continuationContents') ?.get('continuationContents')
?.get('itemSectionContinuation') ?.get('itemSectionContinuation')
?.getValue('continuations') ?.getValue('continuations')
?.first as Map<String, dynamic>) ?.first as Map<String, dynamic>)
?.get('nextContinuationData'); .get('nextContinuationData');
if (continuationRoot != null) { if (continuationRoot != null) {
yield* _getComments( yield* _getComments(
continuationRoot['continuation'], continuationRoot['continuation'],
@ -113,7 +113,7 @@ class CommentsClient {
} }
String _parseRuns(Map<dynamic, dynamic> runs) => String _parseRuns(Map<dynamic, dynamic> runs) =>
runs?.getValue('runs')?.map((e) => e['text'])?.join() ?? ''; runs.getValue('runs')?.map((e) => e['text'])?.join() ?? '';
//TODO: Implement replies //TODO: Implement replies
/* Stream<Comment> getReplies(Video video, Comment comment) async* { /* Stream<Comment> getReplies(Video video, Comment comment) async* {

View File

@ -26,7 +26,7 @@ class StreamsClient {
var signature = var signature =
DashManifest.getSignatureFromUrl(dashManifestUrl.toString()); DashManifest.getSignatureFromUrl(dashManifestUrl.toString());
if (!signature.isNullOrWhiteSpace) { if (!signature.isNullOrWhiteSpace) {
signature = cipherOperations.decipher(signature); signature = cipherOperations.decipher(signature!);
dashManifestUrl = dashManifestUrl.setQueryParam('signature', signature); dashManifestUrl = dashManifestUrl.setQueryParam('signature', signature);
} }
return DashManifest.get(_httpClient, dashManifestUrl); return DashManifest.get(_httpClient, dashManifestUrl);
@ -41,7 +41,7 @@ class StreamsClient {
var playerSource = await PlayerSource.get( var playerSource = await PlayerSource.get(
_httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl); _httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl);
var cipherOperations = playerSource.getCiperOperations(); var cipherOperations = playerSource.getCipherOperations();
var videoInfoResponse = await VideoInfoResponse.get( var videoInfoResponse = await VideoInfoResponse.get(
_httpClient, videoId.toString(), playerSource.sts); _httpClient, videoId.toString(), playerSource.sts);
@ -50,12 +50,12 @@ class StreamsClient {
var previewVideoId = playerResponse.previewVideoId; var previewVideoId = playerResponse.previewVideoId;
if (!previewVideoId.isNullOrWhiteSpace) { if (!previewVideoId.isNullOrWhiteSpace) {
throw VideoRequiresPurchaseException.preview( throw VideoRequiresPurchaseException.preview(
videoId, VideoId(previewVideoId)); videoId, VideoId(previewVideoId!));
} }
if (!playerResponse.isVideoPlayable) { if (!playerResponse.isVideoPlayable) {
throw VideoUnplayableException.unplayable(videoId, throw VideoUnplayableException.unplayable(videoId,
reason: playerResponse.getVideoPlayabilityError()); reason: playerResponse.videoPlayabilityError ?? '');
} }
if (playerResponse.isLive) { if (playerResponse.isLive) {
@ -70,7 +70,7 @@ class StreamsClient {
var dashManifestUrl = playerResponse.dashManifestUrl; var dashManifestUrl = playerResponse.dashManifestUrl;
if (!dashManifestUrl.isNullOrWhiteSpace) { if (!dashManifestUrl.isNullOrWhiteSpace) {
var dashManifest = var dashManifest =
await _getDashManifest(Uri.parse(dashManifestUrl), cipherOperations); await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations);
streamInfoProviders.addAll(dashManifest.streams); streamInfoProviders.addAll(dashManifest.streams);
} }
return StreamContext(streamInfoProviders, cipherOperations); return StreamContext(streamInfoProviders, cipherOperations);
@ -79,7 +79,7 @@ class StreamsClient {
Future<StreamContext> _getStreamContextFromWatchPage(VideoId videoId) async { Future<StreamContext> _getStreamContextFromWatchPage(VideoId videoId) async {
var watchPage = await WatchPage.get(_httpClient, videoId.toString()); var watchPage = await WatchPage.get(_httpClient, videoId.toString());
WatchPlayerConfig playerConfig; WatchPlayerConfig? playerConfig;
try { try {
playerConfig = watchPage.playerConfig; playerConfig = watchPage.playerConfig;
} on FormatException { } on FormatException {
@ -92,21 +92,21 @@ class StreamsClient {
} }
var previewVideoId = playerResponse.previewVideoId; var previewVideoId = playerResponse.previewVideoId;
if (!(previewVideoId.isNullOrWhiteSpace ?? true)) { if (!previewVideoId.isNullOrWhiteSpace) {
throw VideoRequiresPurchaseException.preview( throw VideoRequiresPurchaseException.preview(
videoId, VideoId(previewVideoId)); videoId, VideoId(previewVideoId!));
} }
var playerSourceUrl = watchPage.sourceUrl ?? playerConfig?.sourceUrl; var playerSourceUrl = watchPage.sourceUrl ?? playerConfig?.sourceUrl;
var playerSource = !playerSourceUrl.isNullOrWhiteSpace var playerSource = !playerSourceUrl.isNullOrWhiteSpace
? await PlayerSource.get(_httpClient, playerSourceUrl) ? await PlayerSource.get(_httpClient, playerSourceUrl!)
: null; : null;
var cipherOperations = var cipherOperations =
playerSource?.getCiperOperations() ?? const <CipherOperation>[]; playerSource?.getCipherOperations() ?? const <CipherOperation>[];
if (!playerResponse.isVideoPlayable) { if (!playerResponse.isVideoPlayable) {
throw VideoUnplayableException.unplayable(videoId, throw VideoUnplayableException.unplayable(videoId,
reason: playerResponse.getVideoPlayabilityError()); reason: playerResponse.videoPlayabilityError ?? '');
} }
if (playerResponse.isLive) { if (playerResponse.isLive) {
@ -120,7 +120,7 @@ class StreamsClient {
var dashManifestUrl = playerResponse.dashManifestUrl; var dashManifestUrl = playerResponse.dashManifestUrl;
if (!(dashManifestUrl?.isNullOrWhiteSpace ?? true)) { if (!(dashManifestUrl?.isNullOrWhiteSpace ?? true)) {
var dashManifest = var dashManifest =
await _getDashManifest(Uri.parse(dashManifestUrl), cipherOperations); await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations);
streamInfoProviders.addAll(dashManifest.streams); streamInfoProviders.addAll(dashManifest.streams);
} }
return StreamContext(streamInfoProviders, cipherOperations); return StreamContext(streamInfoProviders, cipherOperations);
@ -130,7 +130,7 @@ class StreamsClient {
// To make sure there are no duplicates streams, group them by tag // To make sure there are no duplicates streams, group them by tag
var streams = <int, StreamInfo>{}; var streams = <int, StreamInfo>{};
for (var streamInfo in streamContext.streamInfoProviders) { for (final streamInfo in streamContext.streamInfoProviders) {
var tag = streamInfo.tag; var tag = streamInfo.tag;
var url = Uri.parse(streamInfo.url); var url = Uri.parse(streamInfo.url);
@ -139,7 +139,7 @@ class StreamsClient {
var signatureParameter = streamInfo.signatureParameter ?? 'signature'; var signatureParameter = streamInfo.signatureParameter ?? 'signature';
if (!signature.isNullOrWhiteSpace) { if (!signature.isNullOrWhiteSpace) {
signature = streamContext.cipherOperations.decipher(signature); signature = streamContext.cipherOperations.decipher(signature!);
url = url.setQueryParam(signatureParameter, signature); url = url.setQueryParam(signatureParameter, signature);
} }
@ -153,9 +153,9 @@ class StreamsClient {
} }
// Common // Common
var container = StreamContainer.parse(streamInfo.container); var container = StreamContainer.parse(streamInfo.container!);
var fileSize = FileSize(contentLength); var fileSize = FileSize(contentLength);
var bitrate = Bitrate(streamInfo.bitrate); var bitrate = Bitrate(streamInfo.bitrate!);
var audioCodec = streamInfo.audioCodec; var audioCodec = streamInfo.audioCodec;
var videoCodec = streamInfo.videoCodec; var videoCodec = streamInfo.videoCodec;
@ -165,14 +165,14 @@ class StreamsClient {
var framerate = Framerate(streamInfo.framerate ?? 24); var framerate = Framerate(streamInfo.framerate ?? 24);
var videoQualityLabel = streamInfo.videoQualityLabel ?? var videoQualityLabel = streamInfo.videoQualityLabel ??
VideoQualityUtil.getLabelFromTagWithFramerate( VideoQualityUtil.getLabelFromTagWithFramerate(
tag, framerate.framesPerSecond); tag, framerate.framesPerSecond.toDouble());
var videoQuality = VideoQualityUtil.fromLabel(videoQualityLabel); var videoQuality = VideoQualityUtil.fromLabel(videoQualityLabel);
var videoWidth = streamInfo.videoWidth; var videoWidth = streamInfo.videoWidth;
var videoHeight = streamInfo.videoHeight; var videoHeight = streamInfo.videoHeight;
var videoResolution = videoWidth != -1 && videoHeight != -1 var videoResolution = videoWidth != -1 && videoHeight != -1
? VideoResolution(videoWidth, videoHeight) ? VideoResolution(videoWidth ?? 0, videoHeight ?? 0)
: videoQuality.toVideoResolution(); : videoQuality.toVideoResolution();
// Muxed // Muxed
@ -183,8 +183,8 @@ class StreamsClient {
container, container,
fileSize, fileSize,
bitrate, bitrate,
audioCodec, audioCodec!,
videoCodec, videoCodec!,
videoQualityLabel, videoQualityLabel,
videoQuality, videoQuality,
videoResolution, videoResolution,
@ -199,7 +199,7 @@ class StreamsClient {
container, container,
fileSize, fileSize,
bitrate, bitrate,
videoCodec, videoCodec!,
videoQualityLabel, videoQualityLabel,
videoQuality, videoQuality,
videoResolution, videoResolution,
@ -209,7 +209,7 @@ class StreamsClient {
// Audio-only // Audio-only
if (!audioCodec.isNullOrWhiteSpace) { if (!audioCodec.isNullOrWhiteSpace) {
streams[tag] = AudioOnlyStreamInfo( streams[tag] = AudioOnlyStreamInfo(
tag, url, container, fileSize, bitrate, audioCodec); tag, url, container, fileSize, bitrate, audioCodec!);
} }
// #if DEBUG // #if DEBUG
@ -244,7 +244,7 @@ class StreamsClient {
var playerResponse = videoInfoResponse.playerResponse; var playerResponse = videoInfoResponse.playerResponse;
if (!playerResponse.isVideoPlayable) { if (!playerResponse.isVideoPlayable) {
throw VideoUnplayableException.unplayable(videoId, throw VideoUnplayableException.unplayable(videoId,
reason: playerResponse.getVideoPlayabilityError()); reason: playerResponse.videoPlayabilityError ?? '');
} }
var hlsManifest = playerResponse.hlsManifestUrl; var hlsManifest = playerResponse.hlsManifestUrl;

View File

@ -23,7 +23,7 @@ class Video with EquatableMixin {
/// Video author Id. /// Video author Id.
/// Note: null if the video is from a search query. /// Note: null if the video is from a search query.
final ChannelId channelId; final ChannelId? channelId;
/// Video upload date. /// Video upload date.
/// Note: For search queries it is calculated with: /// Note: For search queries it is calculated with:
@ -34,7 +34,7 @@ class Video with EquatableMixin {
final String description; final String description;
/// Duration of the video. /// Duration of the video.
final Duration duration; final Duration? duration;
/// Available thumbnails for this video. /// Available thumbnails for this video.
final ThumbnailSet thumbnails; final ThumbnailSet thumbnails;
@ -77,33 +77,3 @@ class Video with EquatableMixin {
@override @override
List<Object> get props => [id]; List<Object> get props => [id];
} }
/// See [Video].
/// This class has no nullable values.
class SafeVideo extends Video {
@override
final DateTime uploadDate;
@override
final SafeEngagement engagement;
@override
final bool isLive;
///
SafeVideo(
VideoId id,
String title,
String author,
ChannelId channelId,
this.uploadDate,
String description,
Duration duration,
ThumbnailSet thumbnails,
Iterable<String>? keywords,
this.engagement,
this.isLive, // ignore: avoid_positional_boolean_parameters
[WatchPage? watchPage])
: super(id, title, author, channelId, uploadDate, description, duration,
thumbnails, keywords, engagement, isLive, watchPage);
}

View File

@ -42,7 +42,7 @@ class VideoClient {
playerResponse.videoDuration, playerResponse.videoDuration,
ThumbnailSet(videoId.value), ThumbnailSet(videoId.value),
playerResponse.videoKeywords, playerResponse.videoKeywords,
Engagement(playerResponse.videoViewCount ?? 0, watchPage.videoLikeCount, Engagement(playerResponse.videoViewCount, watchPage.videoLikeCount,
watchPage.videoDislikeCount), watchPage.videoDislikeCount),
playerResponse.isLive, playerResponse.isLive,
watchPage); watchPage);

View File

@ -11,31 +11,26 @@ class YoutubeExplode {
final YoutubeHttpClient _httpClient; final YoutubeHttpClient _httpClient;
/// Queries related to YouTube videos. /// Queries related to YouTube videos.
VideoClient get videos => _videos; late final VideoClient videos;
/// Queries related to YouTube playlists. /// Queries related to YouTube playlists.
PlaylistClient get playlists => _playlists; late final PlaylistClient playlists;
/// Queries related to YouTube channels. /// Queries related to YouTube channels.
ChannelClient get channels => _channels; late final ChannelClient channels;
/// YouTube search queries. /// YouTube search queries.
SearchClient get search => _search; late final SearchClient search;
/// Initializes an instance of [YoutubeClient]. /// Initializes an instance of [YoutubeClient].
YoutubeExplode([YoutubeHttpClient httpClient]) YoutubeExplode([YoutubeHttpClient? httpClient])
: _httpClient = httpClient ?? YoutubeHttpClient() { : _httpClient = httpClient ?? YoutubeHttpClient() {
_videos = VideoClient(_httpClient); videos = VideoClient(_httpClient);
_playlists = PlaylistClient(_httpClient); playlists = PlaylistClient(_httpClient);
_channels = ChannelClient(_httpClient); channels = ChannelClient(_httpClient);
_search = SearchClient(_httpClient); search = SearchClient(_httpClient);
} }
VideoClient _videos;
PlaylistClient _playlists;
ChannelClient _channels;
SearchClient _search;
/// 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();

View File

@ -1,26 +1,27 @@
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.8.0-nullsafety.1 version: 1.9.0-nullsafety.2
homepage: https://github.com/Hexer10/youtube_explode_dart homepage: https://github.com/Hexer10/youtube_explode_dart
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
collection: ^1.15.0
equatable: ^2.0.0
html: ^0.15.0 html: ^0.15.0
http: ^0.13.0 http: ^0.13.0
http_parser: ^4.0.0 http_parser: ^4.0.0
xml: ^5.0.2
equatable: ^2.0.0
meta: ^1.3.0
json_annotation: ^4.0.0 json_annotation: ^4.0.0
collection: ^1.15.0 meta: ^1.3.0
xml: ^5.0.2
dev_dependencies: dev_dependencies:
effective_dart: ^1.3.0 #TODO: Add build_runner when is nnbd
console: ^3.1.0 # build_runner: ^1.11.5
test: ^1.16.5 console: ^4.0.0
grinder: ^0.9.0-nullsafety.0 grinder: ^0.9.0-nullsafety.0
pedantic: ^1.11.0
json_serializable: ^4.0.2 json_serializable: ^4.0.2
build_runner: ^1.11.5 lint: ^1.5.3
pedantic: ^1.11.0
test: ^1.16.7

View File

@ -2,18 +2,18 @@ import 'package:test/test.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
YoutubeExplode yt; YoutubeExplode? yt;
setUpAll(() { setUpAll(() {
yt = YoutubeExplode(); yt = YoutubeExplode();
}); });
tearDownAll(() { tearDownAll(() {
yt.close(); yt?.close();
}); });
test('Get a channel about page', () async { test('Get a channel about page', () async {
var channelUrl = 'https://www.youtube.com/user/FavijTV'; var channelUrl = 'https://www.youtube.com/user/FavijTV';
var channel = await yt.channels.getAboutPageByUsername(channelUrl); var channel = await yt!.channels.getAboutPageByUsername(channelUrl);
expect(channel.country, 'Italy'); expect(channel.country, 'Italy');
expect(channel.thumbnails, isNotEmpty); expect(channel.thumbnails, isNotEmpty);
expect(channel.channelLinks, isNotEmpty); expect(channel.channelLinks, isNotEmpty);

View File

@ -3,7 +3,7 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
group('These are valid channel ids', () { group('These are valid channel ids', () {
for (var val in <dynamic>{ for (final val in <dynamic>{
[ChannelId('UCEnBXANsKmyj2r9xVyKoDiQ'), 'UCEnBXANsKmyj2r9xVyKoDiQ'], [ChannelId('UCEnBXANsKmyj2r9xVyKoDiQ'), 'UCEnBXANsKmyj2r9xVyKoDiQ'],
[ChannelId('UC46807r_RiRjH8IU-h_DrDQ'), 'UC46807r_RiRjH8IU-h_DrDQ'], [ChannelId('UC46807r_RiRjH8IU-h_DrDQ'), 'UC46807r_RiRjH8IU-h_DrDQ'],
}) { }) {
@ -13,7 +13,7 @@ void main() {
} }
}); });
group('These are valid channel urls', () { group('These are valid channel urls', () {
for (var val in <dynamic>{ for (final val in <dynamic>{
[ [
ChannelId('youtube.com/channel/UC3xnGqlcL3y-GXz5N3wiTJQ'), ChannelId('youtube.com/channel/UC3xnGqlcL3y-GXz5N3wiTJQ'),
'UC3xnGqlcL3y-GXz5N3wiTJQ' 'UC3xnGqlcL3y-GXz5N3wiTJQ'
@ -34,7 +34,7 @@ void main() {
}); });
group('These are not valid channel ids', () { group('These are not valid channel ids', () {
for (var val in { for (final val in {
'', '',
'UC3xnGqlcL3y-GXz5N3wiTJ', 'UC3xnGqlcL3y-GXz5N3wiTJ',
'UC3xnGqlcL y-GXz5N3wiTJQ' 'UC3xnGqlcL y-GXz5N3wiTJQ'
@ -46,7 +46,7 @@ void main() {
}); });
group('These are not valid channel urls', () { group('These are not valid channel urls', () {
for (var val in { for (final val in {
'youtube.com/?channel=UCUC3xnGqlcL3y-GXz5N3wiTJQ', 'youtube.com/?channel=UCUC3xnGqlcL3y-GXz5N3wiTJQ',
'youtube.com/channel/asd', 'youtube.com/channel/asd',
'youtube.com/' 'youtube.com/'

View File

@ -2,18 +2,18 @@ import 'package:test/test.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
YoutubeExplode yt; YoutubeExplode? yt;
setUpAll(() { setUpAll(() {
yt = YoutubeExplode(); yt = YoutubeExplode();
}); });
tearDownAll(() { tearDownAll(() {
yt.close(); yt?.close();
}); });
test('Get metadata of a channel', () async { test('Get metadata of a channel', () async {
var channelUrl = 'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ'; var channelUrl = 'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ';
var channel = await yt.channels.get(ChannelId(channelUrl)); var channel = await yt!.channels.get(ChannelId(channelUrl));
expect(channel.url, channelUrl); expect(channel.url, channelUrl);
expect(channel.title, 'Tyrrrz'); expect(channel.title, 'Tyrrrz');
expect(channel.logoUrl, isNotEmpty); expect(channel.logoUrl, isNotEmpty);
@ -21,31 +21,31 @@ void main() {
}); });
group('Get metadata of any channel', () { group('Get metadata of any channel', () {
for (var val in { for (final val in {
'UC46807r_RiRjH8IU-h_DrDQ', 'UC46807r_RiRjH8IU-h_DrDQ',
'UCJ6td3C9QlPO9O_J5dF4ZzA', 'UCJ6td3C9QlPO9O_J5dF4ZzA',
'UCiGm_E4ZwYSHV3bcW1pnSeQ' 'UCiGm_E4ZwYSHV3bcW1pnSeQ'
}) { }) {
test('Channel - $val', () async { test('Channel - $val', () async {
var channelId = ChannelId(val); var channelId = ChannelId(val);
var channel = await yt.channels.get(channelId); var channel = await yt!.channels.get(channelId);
expect(channel.id, channelId); expect(channel.id, channelId);
}); });
} }
}); });
test('Get metadata of a channel by username', () async { test('Get metadata of a channel by username', () async {
var channel = await yt.channels.getByUsername(Username('TheTyrrr')); var channel = await yt!.channels.getByUsername(Username('TheTyrrr'));
expect(channel.id.value, 'UCEnBXANsKmyj2r9xVyKoDiQ'); expect(channel.id.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
}); });
test('Get metadata of a channel by a video', () async { test('Get metadata of a channel by a video', () async {
var channel = await yt.channels.getByVideo(VideoId('5NmxuoNyDss')); var channel = await yt!.channels.getByVideo(VideoId('5NmxuoNyDss'));
expect(channel.id.value, 'UCEnBXANsKmyj2r9xVyKoDiQ'); expect(channel.id.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
}); });
test('Get the videos of a youtube channel', () async { test('Get the videos of a youtube channel', () async {
var videos = await yt.channels var videos = await yt!.channels
.getUploads(ChannelId( .getUploads(ChannelId(
'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ')) 'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ'))
.toList(); .toList();
@ -53,20 +53,20 @@ void main() {
}); });
group('Get the videos of any youtube channel', () { group('Get the videos of any youtube channel', () {
for (var val in { for (final val in {
'UC46807r_RiRjH8IU-h_DrDQ', 'UC46807r_RiRjH8IU-h_DrDQ',
'UCJ6td3C9QlPO9O_J5dF4ZzA', 'UCJ6td3C9QlPO9O_J5dF4ZzA',
'UCiGm_E4ZwYSHV3bcW1pnSeQ' 'UCiGm_E4ZwYSHV3bcW1pnSeQ'
}) { }) {
test('Channel - $val', () async { test('Channel - $val', () async {
var videos = await yt.channels.getUploads(ChannelId(val)).toList(); var videos = await yt!.channels.getUploads(ChannelId(val)).toList();
expect(videos, isNotEmpty); expect(videos, isNotEmpty);
}); });
} }
}); });
test('Get videos of a youtube channel from the uploads page', () async { test('Get videos of a youtube channel from the uploads page', () async {
var videos = await yt.channels var videos = await yt!.channels
.getUploadsFromPage('UCEnBXANsKmyj2r9xVyKoDiQ') .getUploadsFromPage('UCEnBXANsKmyj2r9xVyKoDiQ')
.take(30) .take(30)
.toList(); .toList();

View File

@ -2,59 +2,64 @@ import 'package:test/test.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
YoutubeExplode yt; YoutubeExplode? yt;
setUpAll(() { setUpAll(() {
yt = YoutubeExplode(); yt = YoutubeExplode();
}); });
tearDownAll(() { tearDownAll(() {
yt.close(); yt?.close();
}); });
test('Get closed captions of a video', () async { test('Get closed captions of a video', () async {
var manifest = await yt.videos.closedCaptions.getManifest('WOxr2dmLHLo'); var manifest = await yt!.videos.closedCaptions.getManifest('WOxr2dmLHLo');
expect(manifest.tracks, isNotEmpty); expect(manifest.tracks, isNotEmpty);
}); });
test('Get closed caption track of a video', () async { test('Get closed caption track of a video', () async {
var manifest = await yt.videos.closedCaptions.getManifest('WOxr2dmLHLo'); var manifest = await yt!.videos.closedCaptions.getManifest('WOxr2dmLHLo');
var trackInfo = manifest.tracks.first; var trackInfo = manifest.tracks.first;
var track = await yt.videos.closedCaptions.get(trackInfo); var track = await yt!.videos.closedCaptions.get(trackInfo);
expect(track.captions, isNotEmpty); expect(track.captions, isNotEmpty);
}); });
test('Get closed auto translated caption track file of a video', () async { test('Get closed auto translated caption track file of a video', () async {
var manifest = await yt.videos.closedCaptions.getManifest('WOxr2dmLHLo'); var manifest = await yt!.videos.closedCaptions.getManifest('WOxr2dmLHLo');
var trackInfo = manifest.tracks.first; var trackInfo = manifest.tracks.first;
var subtitles = await yt.videos.closedCaptions var subtitles = await yt!.videos.closedCaptions
.getSubTitles(trackInfo.autoTranslate('it')); .getSubTitles(trackInfo.autoTranslate('it'));
expect(subtitles, isNotEmpty); expect(subtitles, isNotEmpty);
}); });
test('Get closed caption track at a specific time', () async { test('Get closed caption track at a specific time', () async {
var manifest = await yt.videos.closedCaptions.getManifest('qfJthDvcZ08'); var manifest = await yt!.videos.closedCaptions.getManifest('qfJthDvcZ08');
var trackInfo = manifest.getByLanguage('en', autoGenerated: false); var trackInfo = manifest.getByLanguage('en',
var track = await yt.videos.closedCaptions.get(trackInfo.first); autoGenerated: false); // ignore: avoid_redundant_argument_values
var caption = var track = await yt!.videos.closedCaptions.get(trackInfo.first);
track.getByTime(const Duration(hours: 0, minutes: 1, seconds: 48)); var caption = track.getByTime(const Duration(
hours: 0, // ignore: avoid_redundant_argument_values
minutes: 1,
seconds: 48)); // ignore: avoid_redundant_argument_values
expect(caption, isNotNull); expect(caption, isNotNull);
expect(caption.parts, isEmpty); expect(caption?.parts, isEmpty);
expect(caption.text, 'But what if you don\'t have a captions file'); expect(caption?.text, 'But what if you don\'t have a captions file');
}); });
test('Get auto-generated closed caption track at a specific time', () async { test('Get auto-generated closed caption track at a specific time', () async {
var manifest = await yt.videos.closedCaptions.getManifest('ppJy5uGZLi4'); var manifest = await yt!.videos.closedCaptions.getManifest('ppJy5uGZLi4');
var trackInfo = manifest.getByLanguage('en', autoGenerated: true); var trackInfo = manifest.getByLanguage('en', autoGenerated: true);
var track = await yt.videos.closedCaptions.get(trackInfo.first); var track = await yt!.videos.closedCaptions.get(trackInfo.first);
var caption = var caption = track.getByTime(const Duration(
track.getByTime(const Duration(hours: 0, minutes: 13, seconds: 22)); hours: 0, // ignore: avoid_redundant_argument_values
var captionPart = caption.getPartByTime(const Duration(milliseconds: 200)); minutes: 13,
seconds: 22)); // ignore: avoid_redundant_argument_values
var captionPart = caption!.getPartByTime(const Duration(milliseconds: 200));
expect(caption, isNotNull); expect(caption, isNotNull);
expect(captionPart, isNotNull); expect(captionPart, isNotNull);
expect(caption.text, 'how about this black there are some'); expect(caption.text, 'how about this black there are some');
expect(captionPart.text, ' about'); expect(captionPart?.text, ' about');
}); });
} }

View File

@ -2,19 +2,19 @@ import 'package:test/test.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
YoutubeExplode yt; YoutubeExplode? yt;
setUpAll(() { setUpAll(() {
yt = YoutubeExplode(); yt = YoutubeExplode();
}); });
tearDownAll(() { tearDownAll(() {
yt.close(); yt?.close();
}); });
test('Get comments of a video', () async { test('Get comments of a video', () async {
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU'; var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
var video = await yt.videos.get(VideoId(videoUrl)); var video = await yt!.videos.get(VideoId(videoUrl));
var comments = await yt.videos.commentsClient.getComments(video).toList(); var comments = await yt!.videos.commentsClient.getComments(video).toList();
expect(comments.length, greaterThanOrEqualTo(1)); expect(comments.length, greaterThanOrEqualTo(1));
}, skip: 'This may fail on some environments'); }, skip: 'This may fail on some environments');
} }

View File

@ -3,7 +3,7 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
group('These are valid playlist ids', () { group('These are valid playlist ids', () {
for (var val in { for (final val in {
'PL601B2E69B03FAB9D', 'PL601B2E69B03FAB9D',
'PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e', 'PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e',
'PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk', 'PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk',
@ -23,7 +23,7 @@ void main() {
}); });
group('These are valid playlist urls', () { group('These are valid playlist urls', () {
for (var val in <dynamic>{ for (final val in <dynamic>{
[ [
PlaylistId( PlaylistId(
'youtube.com/playlist?list=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H'), 'youtube.com/playlist?list=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H'),
@ -62,7 +62,7 @@ void main() {
}); });
group('These are not valid playlist ids', () { group('These are not valid playlist ids', () {
for (var val in { for (final val in {
'PLm_3vnTS-pvmZFuF L1Pyhqf8kTTYVKjW', 'PLm_3vnTS-pvmZFuF L1Pyhqf8kTTYVKjW',
'PLm_3vnTS-pvmZFuF3L=Pyhqf8kTTYVKjW' 'PLm_3vnTS-pvmZFuF3L=Pyhqf8kTTYVKjW'
}) { }) {
@ -73,7 +73,7 @@ void main() {
}); });
group('These are not valid playlist urls', () { group('These are not valid playlist urls', () {
for (var val in { for (final val in {
'youtube.com/playlist?lisp=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H', 'youtube.com/playlist?lisp=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H',
'youtube.com/playlist?list=asd' 'youtube.com/playlist?list=asd'
'youtube.com/' 'youtube.com/'

View File

@ -2,19 +2,19 @@ import 'package:test/test.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
YoutubeExplode yt; YoutubeExplode? yt;
setUpAll(() { setUpAll(() {
yt = YoutubeExplode(); yt = YoutubeExplode();
}); });
tearDownAll(() { tearDownAll(() {
yt.close(); yt?.close();
}); });
test('Get metadata of a playlist', () async { test('Get metadata of a playlist', () async {
var playlistUrl = var playlistUrl =
'https://www.youtube.com/playlist?list=PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd'; 'https://www.youtube.com/playlist?list=PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd';
var playlist = await yt.playlists.get(PlaylistId(playlistUrl)); var playlist = await yt!.playlists.get(PlaylistId(playlistUrl));
expect(playlist.id.value, 'PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd'); expect(playlist.id.value, 'PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd');
expect(playlist.url, playlistUrl); expect(playlist.url, playlistUrl);
expect(playlist.title, 'osu! Highlights'); expect(playlist.title, 'osu! Highlights');
@ -30,7 +30,7 @@ void main() {
expect(playlist.thumbnails.maxResUrl, isNotEmpty); expect(playlist.thumbnails.maxResUrl, isNotEmpty);
}); });
group('Get metadata of any playlist', () { group('Get metadata of any playlist', () {
for (var val in { for (final val in {
PlaylistId('PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e'), PlaylistId('PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e'),
PlaylistId('RD1hu8-y6fKg0'), PlaylistId('RD1hu8-y6fKg0'),
PlaylistId('RDMMU-ty-2B02VY'), PlaylistId('RDMMU-ty-2B02VY'),
@ -38,14 +38,14 @@ void main() {
PlaylistId('PL601B2E69B03FAB9D') PlaylistId('PL601B2E69B03FAB9D')
}) { }) {
test('PlaylistID - ${val.value}', () async { test('PlaylistID - ${val.value}', () async {
var playlist = await yt.playlists.get(val); var playlist = await yt!.playlists.get(val);
expect(playlist.id.value, val.value); expect(playlist.id.value, val.value);
}); });
} }
}); });
test('Get videos in a playlist', () async { test('Get videos in a playlist', () async {
var videos = await yt.playlists var videos = await yt!.playlists
.getVideos(PlaylistId( .getVideos(PlaylistId(
'https://www.youtube.com/playlist?list=PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd')) 'https://www.youtube.com/playlist?list=PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd'))
.toList(); .toList();
@ -64,7 +64,7 @@ void main() {
}); });
group('Get videos in any playlist', () { group('Get videos in any playlist', () {
for (var val in { for (final val in {
PlaylistId('PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e'), PlaylistId('PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e'),
PlaylistId('PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk'), PlaylistId('PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk'),
PlaylistId('OLAK5uy_mtOdjCW76nDvf5yOzgcAVMYpJ5gcW5uKU'), PlaylistId('OLAK5uy_mtOdjCW76nDvf5yOzgcAVMYpJ5gcW5uKU'),
@ -74,7 +74,7 @@ void main() {
PlaylistId('PL601B2E69B03FAB9D'), PlaylistId('PL601B2E69B03FAB9D'),
}) { }) {
test('PlaylistID - ${val.value}', () async { test('PlaylistID - ${val.value}', () async {
expect(yt.playlists.getVideos(val), emits(isNotNull)); expect(yt!.playlists.getVideos(val), emits(isNotNull));
}); });
} }
}); });

View File

@ -2,22 +2,23 @@ import 'package:test/test.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
YoutubeExplode yt; YoutubeExplode? yt;
setUp(() { setUp(() {
yt = YoutubeExplode(); yt = YoutubeExplode();
}); });
tearDown(() { tearDown(() {
yt.close(); yt?.close();
}); });
test('Search a youtube video from the search page', () async { test('Search a youtube video from the search page', () async {
var videos = await yt.search.getVideos('undead corporation megalomania'); var videos = await yt!.search.getVideos('undead corporation megalomania');
expect(videos, isNotEmpty); expect(videos, isNotEmpty);
}); });
test('Search a youtube video from the search page-2', () async { test('Search a youtube video from the search page-2', () async {
var videos = await yt.search
var videos = await yt!.search
.getVideosFromPage('hello') .getVideosFromPage('hello')
.where((e) => e is SearchVideo) // Take only the videos. .where((e) => e is SearchVideo) // Take only the videos.
.cast<SearchVideo>() .cast<SearchVideo>()
@ -37,7 +38,7 @@ void main() {
test('Search a youtube videos from the search page - old', () async { test('Search a youtube videos from the search page - old', () async {
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package
var searchQuery = await yt.search.queryFromPage('hello'); var searchQuery = await yt!.search.queryFromPage('hello');
expect(searchQuery.content, isNotEmpty); expect(searchQuery.content, isNotEmpty);
expect(searchQuery.relatedVideos, isNotEmpty); expect(searchQuery.relatedVideos, isNotEmpty);
expect(searchQuery.relatedQueries, isNotEmpty); expect(searchQuery.relatedQueries, isNotEmpty);
@ -46,7 +47,7 @@ void main() {
test('Search with no results - old', () async { test('Search with no results - old', () async {
var query = var query =
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package
await yt.search.queryFromPage('g;jghEOGHJeguEPOUIhjegoUEHGOGHPSASG'); await yt!.search.queryFromPage('g;jghEOGHJeguEPOUIhjegoUEHGOGHPSASG');
expect(query.content, isEmpty); expect(query.content, isEmpty);
expect(query.relatedQueries, isEmpty); expect(query.relatedQueries, isEmpty);
expect(query.relatedVideos, isEmpty); expect(query.relatedVideos, isEmpty);
@ -56,7 +57,7 @@ void main() {
test('Search youtube videos have thumbnails - old', () async { test('Search youtube videos have thumbnails - old', () async {
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package
var searchQuery = await yt.search.queryFromPage('hello'); var searchQuery = await yt!.search.queryFromPage('hello');
expect(searchQuery.content.first, isA<SearchVideo>()); expect(searchQuery.content.first, isA<SearchVideo>());
var video = searchQuery.content.first as SearchVideo; var video = searchQuery.content.first as SearchVideo;
@ -64,7 +65,7 @@ void main() {
}); });
test('Search youtube videos from search page (stream) - old', () async { test('Search youtube videos from search page (stream) - old', () async {
var query = await yt.search.getVideosFromPage('hello').take(30).toList(); var query = await yt!.search.getVideosFromPage('hello').take(30).toList();
expect(query, hasLength(30)); expect(query, hasLength(30));
}); });
} }

View File

@ -2,19 +2,19 @@ import 'package:test/test.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
YoutubeExplode yt; YoutubeExplode? yt;
setUpAll(() { setUpAll(() {
yt = YoutubeExplode(); yt = YoutubeExplode();
}); });
tearDownAll(() { tearDownAll(() {
yt.close(); yt?.close();
}); });
group('Get streams manifest of any video', () { group('Get streams manifest of any video', () {
for (var val in { for (final val in {
VideoId('9bZkp7q19f0'), // very popular VideoId('9bZkp7q19f0'), // very popular
VideoId('SkRSXFQerZs'), // age restricted (embed allowed) // VideoId('SkRSXFQerZs'), // age restricted (embed allowed) - This is unplayable
VideoId('hySoCSoH-g8'), // age restricted (embed not allowed) VideoId('hySoCSoH-g8'), // age restricted (embed not allowed)
VideoId('_kmeFXjjGfk'), // embed not allowed (type 1) VideoId('_kmeFXjjGfk'), // embed not allowed (type 1)
VideoId('MeJVWBSsPAY'), // embed not allowed (type 2) VideoId('MeJVWBSsPAY'), // embed not allowed (type 2)
@ -25,30 +25,30 @@ void main() {
VideoId('-xNN-bJQ4vI'), // 360° video 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);
expect(manifest.streams, isNotEmpty); expect(manifest.streams, isNotEmpty);
}); });
} }
}); });
test('Stream of paid videos throw VideoRequiresPurchaseException', () { test('Stream of paid videos throw VideoRequiresPurchaseException', () {
expect(yt.videos.streamsClient.getManifest(VideoId('p3dDcKOFXQg')), expect(yt!.videos.streamsClient.getManifest(VideoId('p3dDcKOFXQg')),
throwsA(const TypeMatcher<VideoRequiresPurchaseException>())); throwsA(const TypeMatcher<VideoRequiresPurchaseException>()));
}); });
group('Stream of unavailable videos throws VideoUnavailableException', () { group('Stream of unavailable videos throws VideoUnavailableException', () {
for (var val in {VideoId('qld9w0b-1ao'), VideoId('pb_hHv3fByo')}) { for (final val in {VideoId('qld9w0b-1ao'), VideoId('pb_hHv3fByo')}) {
test('VideoId - ${val.value}', () { test('VideoId - ${val.value}', () {
expect(yt.videos.streamsClient.getManifest(val), expect(yt!.videos.streamsClient.getManifest(val),
throwsA(const TypeMatcher<VideoUnavailableException>())); throwsA(const TypeMatcher<VideoUnavailableException>()));
}); });
} }
}); });
group('Get specific stream of any playable video', () { group('Get specific stream of any playable video', () {
for (var val in { for (final val in {
VideoId('9bZkp7q19f0'), // very popular VideoId('9bZkp7q19f0'), // very popular
VideoId('SkRSXFQerZs'), // age restricted (embed allowed) // VideoId('SkRSXFQerZs'), // age restricted (embed allowed) - This is unplayable
VideoId('hySoCSoH-g8'), // age restricted (embed not allowed) VideoId('hySoCSoH-g8'), // age restricted (embed not allowed)
VideoId('_kmeFXjjGfk'), // embed not allowed (type 1) VideoId('_kmeFXjjGfk'), // embed not allowed (type 1)
VideoId('MeJVWBSsPAY'), // embed not allowed (type 2) VideoId('MeJVWBSsPAY'), // embed not allowed (type 2)
@ -59,9 +59,9 @@ void main() {
VideoId('-xNN-bJQ4vI'), // 360° video 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);
for (var streamInfo in manifest.streams) { for (final streamInfo in manifest.streams) {
expect(yt.videos.streamsClient.get(streamInfo), emits(isNotNull)); expect(yt!.videos.streamsClient.get(streamInfo), emits(isNotNull));
} }
}); });
} }

View File

@ -3,14 +3,14 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
group('These are valid usernames', () { group('These are valid usernames', () {
for (var val in {'TheTyrrr', 'KannibalenRecords', 'JClayton1994'}) { for (final val in {'TheTyrrr', 'KannibalenRecords', 'JClayton1994'}) {
test('Username - $val', () { test('Username - $val', () {
expect(Username(val).value, val); expect(Username(val).value, val);
}); });
} }
}); });
group('These are valid username urls', () { group('These are valid username urls', () {
for (var val in { for (final val in {
['youtube.com/user/ProZD', 'ProZD'], ['youtube.com/user/ProZD', 'ProZD'],
['youtube.com/user/TheTyrrr', 'TheTyrrr'], ['youtube.com/user/TheTyrrr', 'TheTyrrr'],
}) { }) {
@ -20,7 +20,7 @@ void main() {
} }
}); });
group('These are invalid usernames', () { group('These are invalid usernames', () {
for (var val in { for (final val in {
'The_Tyrrr', 'The_Tyrrr',
'0123456789ABCDEFGHIJK', '0123456789ABCDEFGHIJK',
'A1B2C3-', 'A1B2C3-',
@ -33,7 +33,7 @@ void main() {
}); });
group('These are not valid username urls', () { group('These are not valid username urls', () {
for (var val in { for (final val in {
'youtube.com/user/P_roZD', 'youtube.com/user/P_roZD',
'example.com/user/ProZD', 'example.com/user/ProZD',
}) { }) {

View File

@ -3,14 +3,14 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
group('These are valid video ids', () { group('These are valid video ids', () {
for (var val in {'9bZkp7q19f0', '_kmeFXjjGfk', 'AI7ULzgf8RU'}) { for (final val in {'9bZkp7q19f0', '_kmeFXjjGfk', 'AI7ULzgf8RU'}) {
test('VideoID - $val', () { test('VideoID - $val', () {
expect(VideoId(val).value, val); expect(VideoId(val).value, val);
}); });
} }
}); });
group('These are valid video urls', () { group('These are valid video urls', () {
for (var val in { for (final val in {
['youtube.com/watch?v=yIVRs6YSbOM', 'yIVRs6YSbOM'], ['youtube.com/watch?v=yIVRs6YSbOM', 'yIVRs6YSbOM'],
['youtu.be/yIVRs6YSbOM', 'yIVRs6YSbOM'], ['youtu.be/yIVRs6YSbOM', 'yIVRs6YSbOM'],
['youtube.com/embed/yIVRs6YSbOM', 'yIVRs6YSbOM'], ['youtube.com/embed/yIVRs6YSbOM', 'yIVRs6YSbOM'],
@ -21,14 +21,14 @@ void main() {
} }
}); });
group('These are not valid video ids', () { group('These are not valid video ids', () {
for (var val in {'', 'pI2I2zqzeK', 'pI2I2z zeKg'}) { for (final val in {'', 'pI2I2zqzeK', 'pI2I2z zeKg'}) {
test('VideoID - $val', () { test('VideoID - $val', () {
expect(() => VideoId(val), throwsArgumentError); expect(() => VideoId(val), throwsArgumentError);
}); });
} }
}); });
group('These are not valid video urls', () { group('These are not valid video urls', () {
for (var val in { for (final val in {
'youtube.com/xxx?v=pI2I2zqzeKg', 'youtube.com/xxx?v=pI2I2zqzeKg',
'youtu.be/watch?v=xxx', 'youtu.be/watch?v=xxx',
'youtube.com/embed' 'youtube.com/embed'

View File

@ -2,31 +2,31 @@ import 'package:test/test.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
void main() { void main() {
YoutubeExplode yt; YoutubeExplode? yt;
setUpAll(() { setUpAll(() {
yt = YoutubeExplode(); yt = YoutubeExplode();
}); });
tearDownAll(() { tearDownAll(() {
yt.close(); yt?.close();
}); });
test('Get metadata of a video', () async { test('Get metadata of a video', () async {
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU'; var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
var video = await yt.videos.get(VideoId(videoUrl)); var video = await yt!.videos.get(VideoId(videoUrl));
expect(video.id.value, 'AI7ULzgf8RU'); expect(video.id.value, 'AI7ULzgf8RU');
expect(video.url, videoUrl); expect(video.url, videoUrl);
expect(video.title, 'Aka no Ha [Another] +HDHR'); expect(video.title, 'Aka no Ha [Another] +HDHR');
expect(video.channelId.value, 'UCEnBXANsKmyj2r9xVyKoDiQ'); expect(video.channelId!.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
expect(video.author, 'Tyrrrz'); expect(video.author, 'Tyrrrz');
var rangeMs = DateTime(2017, 09, 30, 17, 15, 26).millisecondsSinceEpoch; var rangeMs = DateTime(2017, 09, 30, 17, 15, 26).millisecondsSinceEpoch;
// 1day margin since the uploadDate could differ from timezones // 1day margin since the uploadDate could differ from timezones
expect(video.uploadDate.millisecondsSinceEpoch, expect(video.uploadDate!.millisecondsSinceEpoch,
inInclusiveRange(rangeMs - 86400000, rangeMs + 86400000)); inInclusiveRange(rangeMs - 86400000, rangeMs + 86400000));
expect(video.description, contains('246pp')); expect(video.description, contains('246pp'));
// Should be 1:38 but sometimes it differs // Should be 1:38 but sometimes it differs
// so where using a 10 seconds range from it. // so where using a 10 seconds range from it.
expect(video.duration.inSeconds, inInclusiveRange(108, 118)); expect(video.duration!.inSeconds, inInclusiveRange(108, 118));
expect(video.thumbnails.lowResUrl, isNotEmpty); expect(video.thumbnails.lowResUrl, isNotEmpty);
expect(video.thumbnails.mediumResUrl, isNotEmpty); expect(video.thumbnails.mediumResUrl, isNotEmpty);
expect(video.thumbnails.highResUrl, isNotEmpty); expect(video.thumbnails.highResUrl, isNotEmpty);
@ -44,7 +44,7 @@ void main() {
}); });
group('Get metadata of any video', () { group('Get metadata of any video', () {
for (var val in { for (final val in {
VideoId('9bZkp7q19f0'), VideoId('9bZkp7q19f0'),
VideoId('SkRSXFQerZs'), VideoId('SkRSXFQerZs'),
VideoId('5VGm0dczmHc'), VideoId('5VGm0dczmHc'),
@ -52,16 +52,16 @@ void main() {
VideoId('5qap5aO4i9A') VideoId('5qap5aO4i9A')
}) { }) {
test('VideoId - ${val.value}', () async { test('VideoId - ${val.value}', () async {
var video = await yt.videos.get(val); var video = await yt!.videos.get(val);
expect(video.id.value, val.value); expect(video.id.value, val.value);
}); });
} }
}); });
group('Get metadata of invalid videos throws VideoUnplayableException', () { group('Get metadata of invalid videos throws VideoUnplayableException', () {
for (var val in {VideoId('qld9w0b-1ao'), VideoId('pb_hHv3fByo')}) { for (final val in {VideoId('qld9w0b-1ao'), VideoId('pb_hHv3fByo')}) {
test('VideoId - $val', () { test('VideoId - $val', () {
expect(() async => yt.videos.get(val), expect(() async => yt!.videos.get(val),
throwsA(const TypeMatcher<VideoUnplayableException>())); throwsA(const TypeMatcher<VideoUnplayableException>()));
}); });
} }

View File

@ -1,7 +1,7 @@
import 'package:grinder/grinder.dart'; import 'package:grinder/grinder.dart';
final pub = sdkBin('pub'); final pub = sdkBin('pub');
void main(args) => grind(args); void main(List<String> args) => grind(args);
@Task('Run tests') @Task('Run tests')
void test() => TestRunner().testAsync(); void test() => TestRunner().testAsync();
@ -11,11 +11,11 @@ void analysis() {}
@DefaultTask() @DefaultTask()
@Depends(test) @Depends(test)
build() { void build() {
Pub.build(); Pub.build();
Pub.upgrade(); Pub.upgrade();
Pub.version(); Pub.version();
} }
@Task() @Task()
clean() => defaultClean(); void clean() => defaultClean();