diff --git a/lib/src/extensions/helpers_extension.dart b/lib/src/extensions/helpers_extension.dart index c233429..e355ce2 100644 --- a/lib/src/extensions/helpers_extension.dart +++ b/lib/src/extensions/helpers_extension.dart @@ -1,5 +1,8 @@ library _youtube_explode.extensions; +import 'dart:convert'; +import 'package:collection/collection.dart'; + import '../reverse_engineering/cipher/cipher_operations.dart'; /// Utility for Strings. @@ -11,36 +14,35 @@ extension StringUtility on String { String substringUntil(String separator) => substring(0, indexOf(separator)); /// - String substringAfter(String separator) => - substring(indexOf(separator) + separator.length); + String substringAfter(String separator) => substring(indexOf(separator) + separator.length); static final _exp = RegExp(r'\D'); /// Strips out all non digit characters. String stripNonDigits() => replaceAll(_exp, ''); - /// - String extractJson() { - var buffer = StringBuffer(); - var depth = 0; + /// Extract and decode json from a string + Map? extractJson([String separator = '']) { + final index = indexOf(separator) + separator.length; + if (index > length) { + return null; + } - for (var i = 0; i < length; i++) { - var ch = this[i]; - var chPrv = i > 0 ? this[i - 1] : ''; + final str = substring(index); - buffer.write(ch); + final startIdx = str.indexOf('{'); + var endIdx = str.lastIndexOf('}'); - if (ch == '{' && chPrv != '\\') { - depth++; - } else if (ch == '}' && chPrv != '\\') { - depth--; - } - - if (depth == 0) { - break; + while (true) { + try { + return json.decode(str.substring(startIdx, endIdx + 1)) as Map; + } on FormatException { + endIdx = str.lastIndexOf(str.substring(0, endIdx)); + if (endIdx == 0) { + return null; + } } } - return buffer.toString(); } DateTime parseDateTime() => DateTime.parse(this); @@ -166,3 +168,16 @@ extension RunsParser on List { /// String parseRuns() => map((e) => e['text']).join(); } + +extension GenericExtract on List { + /// Used to extract initial data that start with `var ytInitialData = ` or 'window["ytInitialData"] ='. + T extractGenericData(T Function(Map) builder, Exception Function() orThrow) { + var initialData = firstWhereOrNull((e) => e.contains('var ytInitialData = '))?.extractJson('var ytInitialData = '); + initialData ??= firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='))?.extractJson('window["ytInitialData"] ='); + + if (initialData != null) { + return builder(initialData); + } + throw orThrow(); + } +} diff --git a/lib/src/reverse_engineering/responses/channel_about_page.dart b/lib/src/reverse_engineering/responses/channel_about_page.dart index ea4fd66..57a43c4 100644 --- a/lib/src/reverse_engineering/responses/channel_about_page.dart +++ b/lib/src/reverse_engineering/responses/channel_about_page.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:collection/collection.dart'; import 'package:html/dom.dart'; import 'package:html/parser.dart' as parser; @@ -18,57 +16,16 @@ class ChannelAboutPage { late final _InitialData initialData = _getInitialData(); _InitialData _getInitialData() { - final scriptText = _root - .querySelectorAll('script') - .map((e) => e.text) - .toList(growable: false); - - var initialDataText = scriptText.firstWhere( - (e) => e.contains('window["ytInitialData"] ='), - orElse: () => ''); - if (initialDataText.isNotEmpty) { - return _InitialData(json - .decode(_extractJson(initialDataText, 'window["ytInitialData"] ='))); - } - - initialDataText = scriptText.firstWhere( - (e) => e.contains('var ytInitialData = '), - orElse: () => ''); - if (initialDataText.isNotEmpty) { - return _InitialData( - json.decode(_extractJson(initialDataText, 'var ytInitialData = '))); - } - - throw TransientFailureException( - 'Failed to retrieve initial data from the channel about page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars + final scriptText = _root.querySelectorAll('script').map((e) => e.text).toList(growable: false); + return scriptText.extractGenericData( + (obj) => _InitialData(obj), + () => TransientFailureException( + 'Failed to retrieve initial data from the channel about page, please report this to the project GitHub page.')); } /// String get description => initialData.description; - String _extractJson(String html, String separator) { - return _matchJson( - html.substring(html.indexOf(separator) + separator.length)); - } - - String _matchJson(String str) { - var bracketCount = 0; - late int lastI; - for (var i = 0; i < str.length; i++) { - lastI = i; - if (str[i] == '{') { - bracketCount++; - } else if (str[i] == '}') { - bracketCount--; - } else if (str[i] == ';') { - if (bracketCount == 0) { - return str.substring(0, i); - } - } - } - return str.substring(0, lastI + 1); - } - /// ChannelAboutPage(this._root); @@ -88,8 +45,7 @@ class ChannelAboutPage { } /// - static Future getByUsername( - YoutubeHttpClient httpClient, String username) { + static Future getByUsername(YoutubeHttpClient httpClient, String username) { var url = 'https://www.youtube.com/user/$username/about?hl=en'; return retry(() async { @@ -128,49 +84,29 @@ class _InitialData { .get('channelAboutFullMetadataRenderer')!; } - late final String description = - content.get('description')!.getT('simpleText')!; + late final String description = content.get('description')!.getT('simpleText')!; late final List channelLinks = content .getList('primaryLinks')! .map((e) => ChannelLink( e.get('title')?.getT('simpleText') ?? '', - extractUrl(e - .get('navigationEndpoint') - ?.get('commandMetadata') - ?.get('webCommandMetadata') - ?.getT('url') ?? - e - .get('navigationEndpoint') - ?.get('urlEndpoint') - ?.getT('url') ?? + extractUrl(e.get('navigationEndpoint')?.get('commandMetadata')?.get('webCommandMetadata')?.getT('url') ?? + e.get('navigationEndpoint')?.get('urlEndpoint')?.getT('url') ?? ''), - Uri.parse(e - .get('icon') - ?.getList('thumbnails') - ?.firstOrNull - ?.getT('url') ?? - ''))) + Uri.parse(e.get('icon')?.getList('thumbnails')?.firstOrNull?.getT('url') ?? ''))) .toList(); - late final int viewCount = int.parse(content - .get('viewCountText')! - .getT('simpleText')! - .stripNonDigits()); + late final int viewCount = int.parse(content.get('viewCountText')!.getT('simpleText')!.stripNonDigits()); - late final String joinDate = - content.get('joinedDateText')!.getList('runs')![1].getT('text')!; + late final String joinDate = content.get('joinedDateText')!.getList('runs')![1].getT('text')!; late final String title = content.get('title')!.getT('simpleText')!; - late final List> avatar = - content.get('avatar')!.getList('thumbnails')!; + late final List> avatar = content.get('avatar')!.getList('thumbnails')!; String get country => content.get('country')!.getT('simpleText')!; - String parseRuns(List? runs) => - runs?.map((e) => e.text).join() ?? ''; + String parseRuns(List? runs) => runs?.map((e) => e.text).join() ?? ''; - Uri extractUrl(String text) => - Uri.parse(Uri.decodeFull(_urlExp.firstMatch(text)?.group(1) ?? '')); + Uri extractUrl(String text) => Uri.parse(Uri.decodeFull(_urlExp.firstMatch(text)?.group(1) ?? '')); } diff --git a/lib/src/reverse_engineering/responses/channel_upload_page.dart b/lib/src/reverse_engineering/responses/channel_upload_page.dart index 5e3286f..25fa9d5 100644 --- a/lib/src/reverse_engineering/responses/channel_upload_page.dart +++ b/lib/src/reverse_engineering/responses/channel_upload_page.dart @@ -30,179 +30,140 @@ class ChannelUploadPage { .map((e) => e.text) .toList(growable: false); - var initialDataText = scriptText.firstWhere( - (e) => e.contains('window["ytInitialData"] ='), - orElse: () => ''); - if (initialDataText.isNotEmpty) { - return _InitialData(json - .decode(_extractJson(initialDataText, 'window["ytInitialData"] ='))); - } - - initialDataText = scriptText.firstWhere( - (e) => e.contains('var ytInitialData = '), - orElse: () => ''); - if (initialDataText.isNotEmpty) { - return _InitialData( - json.decode(_extractJson(initialDataText, 'var ytInitialData = '))); - } - - throw TransientFailureException( - 'Failed to retrieve initial data from the channel upload page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars + return scriptText.extractGenericData((obj) => _InitialData(obj), () => + TransientFailureException( + 'Failed to retrieve initial data from the channel upload page, please report this to the project GitHub page.')); } - String _extractJson(String html, String separator) { - return _matchJson( - html.substring(html.indexOf(separator) + separator.length)); - } + /// + ChannelUploadPage(this._root, this.channelId, [_InitialData ? initialData]): _initialData = initialData; - String _matchJson(String str) { - var bracketCount = 0; - late int lastI; - for (var i = 0; i < str.length; i++) { - lastI = i; - if (str[i] == '{') { - bracketCount++; - } else if (str[i] == '}') { - bracketCount--; - } else if (str[i] == ';') { - if (bracketCount == 0) { - return str.substring(0, i); - } - } - } - return str.substring(0, lastI + 1); - } - - /// - ChannelUploadPage(this._root, this.channelId, [_InitialData? initialData]) - : _initialData = initialData; - - /// - Future nextPage(YoutubeHttpClient httpClient) { + /// + Future nextPage(YoutubeHttpClient httpClient) { if (initialData.continuation.isEmpty) { - return Future.value(null); + return Future.value(null); } var url = - '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 { - var raw = await httpClient.getString(url); - return ChannelUploadPage( - null, channelId, _InitialData(json.decode(raw)[1])); + var raw = await httpClient.getString(url); + return ChannelUploadPage( + null, channelId, _InitialData(json.decode(raw)[1])); }); - } + } - /// - static Future get( - YoutubeHttpClient httpClient, String channelId, String sorting) { + /// + static Future get( + YoutubeHttpClient httpClient, String channelId, String sorting) { 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 { - var raw = await httpClient.getString(url); - return ChannelUploadPage.parse(raw, channelId); + var raw = await httpClient.getString(url); + return ChannelUploadPage.parse(raw, channelId); }); + } + + /// + ChannelUploadPage.parse(String raw, this.channelId) + : _root = parser.parse(raw); } - /// - ChannelUploadPage.parse(String raw, this.channelId) - : _root = parser.parse(raw); -} - -class _InitialData { + class _InitialData { // Json parsed map final Map root; _InitialData(this.root); late final Map? continuationContext = - getContinuationContext(); + getContinuationContext(); late final String clickTrackingParams = - continuationContext?.getT('continuationContext') ?? ''; + continuationContext?.getT('continuationContext') ?? ''; late final List uploads = - getContentContext().map(_parseContent).whereNotNull().toList(); + getContentContext().map(_parseContent).whereNotNull().toList(); late final String continuation = - continuationContext?.getT('continuation') ?? ''; + continuationContext?.getT('continuation') ?? ''; List> getContentContext() { - List>? context; - if (root.containsKey('contents')) { - context = root - .get('contents') - ?.get('twoColumnBrowseResultsRenderer') - ?.getList('tabs') - ?.map((e) => e['tabRenderer']) - .cast>() - .firstWhereOrNull((e) => e['selected'] as bool) - ?.get('content') - ?.get('sectionListRenderer') - ?.getList('contents') - ?.firstOrNull - ?.get('itemSectionRenderer') - ?.getList('contents') - ?.firstOrNull - ?.get('gridRenderer') - ?.getList('items') - ?.cast>(); - } - if (context == null && root.containsKey('response')) { - context = root - .get('response') - ?.get('continuationContents') - ?.get('gridContinuation') - ?.getList('items') - ?.cast>(); - } - if (context == null) { - throw FatalFailureException('Failed to get initial data context.'); - } - return context; + List>? context; + if (root.containsKey('contents')) { + context = root + .get('contents') + ?.get('twoColumnBrowseResultsRenderer') + ?.getList('tabs') + ?.map((e) => e['tabRenderer']) + .cast>() + .firstWhereOrNull((e) => e['selected'] as bool) + ?.get('content') + ?.get('sectionListRenderer') + ?.getList('contents') + ?.firstOrNull + ?.get('itemSectionRenderer') + ?.getList('contents') + ?.firstOrNull + ?.get('gridRenderer') + ?.getList('items') + ?.cast>(); + } + if (context == null && root.containsKey('response')) { + context = root + .get('response') + ?.get('continuationContents') + ?.get('gridContinuation') + ?.getList('items') + ?.cast>(); + } + if (context == null) { + throw FatalFailureException('Failed to get initial data context.'); + } + return context; } Map? getContinuationContext() { - if (root.containsKey('contents')) { - return root - .get('contents') - ?.get('twoColumnBrowseResultsRenderer') - ?.getList('tabs') - ?.map((e) => e['tabRenderer']) - .cast>() - .firstWhereOrNull((e) => e['selected'] as bool) - ?.get('content') - ?.get('sectionListRenderer') - ?.getList('contents') - ?.firstOrNull - ?.get('itemSectionRenderer') - ?.getList('contents') - ?.firstOrNull - ?.get('gridRenderer') - ?.getList('continuations') - ?.firstOrNull - ?.get('nextContinuationData'); - } - if (root.containsKey('response')) { - return root - .get('response') - ?.get('continuationContents') - ?.get('gridContinuation') - ?.getList('continuations') - ?.firstOrNull - ?.get('nextContinuationData'); - } - return null; + if (root.containsKey('contents')) { + return root + .get('contents') + ?.get('twoColumnBrowseResultsRenderer') + ?.getList('tabs') + ?.map((e) => e['tabRenderer']) + .cast>() + .firstWhereOrNull((e) => e['selected'] as bool) + ?.get('content') + ?.get('sectionListRenderer') + ?.getList('contents') + ?.firstOrNull + ?.get('itemSectionRenderer') + ?.getList('contents') + ?.firstOrNull + ?.get('gridRenderer') + ?.getList('continuations') + ?.firstOrNull + ?.get('nextContinuationData'); + } + if (root.containsKey('response')) { + return root + .get('response') + ?.get('continuationContents') + ?.get('gridContinuation') + ?.getList('continuations') + ?.firstOrNull + ?.get('nextContinuationData'); + } + return null; } ChannelVideo? _parseContent(Map? content) { - if (content == null || !content.containsKey('gridVideoRenderer')) { - return null; - } - - var video = content.get('gridVideoRenderer')!; - return ChannelVideo( - VideoId(video.getT('videoId')!), - video.get('title')?.getT('simpleText') ?? - video.get('title')?.getList('runs')?.map((e) => e['text']).join() ?? - ''); + if (content == null || !content.containsKey('gridVideoRenderer')) { + return null; + } + + var video = content.get('gridVideoRenderer')!; + return ChannelVideo( + VideoId(video.getT('videoId')!), + video.get('title')?.getT('simpleText') ?? + video.get('title')?.getList('runs')?.map((e) => e['text']).join() ?? + ''); + } } -} diff --git a/lib/src/reverse_engineering/responses/embed_page.dart b/lib/src/reverse_engineering/responses/embed_page.dart index 75b4113..d7fec61 100644 --- a/lib/src/reverse_engineering/responses/embed_page.dart +++ b/lib/src/reverse_engineering/responses/embed_page.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:collection/collection.dart'; import 'package:html/dom.dart'; import 'package:html/parser.dart' as parser; @@ -11,8 +9,7 @@ import 'player_config_base.dart'; /// class EmbedPage { - static final _playerConfigExp = - RegExp('[\'""]PLAYER_CONFIG[\'""]\\s*:\\s*(\\{.*\\})'); + static final _playerConfigExp = RegExp('[\'""]PLAYER_CONFIG[\'""]\\s*:\\s*(\\{.*\\})'); static final _playerConfigExp2 = RegExp(r'yt.setConfig\((\{.*\})'); final Document root; @@ -24,8 +21,7 @@ class EmbedPage { .querySelectorAll('*[name="player_ias/base"]') .map((e) => e.attributes['src']) .where((e) => !e.isNullOrWhiteSpace) - .firstWhere((e) => e!.contains('player_ias') && e.endsWith('.js'), - orElse: () => null); + .firstWhere((e) => e!.contains('player_ias') && e.endsWith('.js'), orElse: () => null); // _root.querySelector('*[name="player_ias/base"]').attributes['src']; if (url == null) { return null; @@ -35,11 +31,11 @@ class EmbedPage { /// EmbedPlayerConfig? getPlayerConfig() { - var playerConfigJson = _playerConfigJson ?? _playerConfigJson2; + var playerConfigJson = (_playerConfigJson ?? _playerConfigJson2)?.extractJson(); if (playerConfigJson == null) { return null; } - return EmbedPlayerConfig(json.decode(playerConfigJson.extractJson())); + return EmbedPlayerConfig(playerConfigJson); } String? get _playerConfigJson => root diff --git a/lib/src/reverse_engineering/responses/player_response.dart b/lib/src/reverse_engineering/responses/player_response.dart index d93a0ad..d35c43f 100644 --- a/lib/src/reverse_engineering/responses/player_response.dart +++ b/lib/src/reverse_engineering/responses/player_response.dart @@ -123,6 +123,8 @@ class PlayerResponse { late final String? videoPlayabilityError = root.get('playabilityStatus')?.getT('reason'); + PlayerResponse(this.root); + /// PlayerResponse.parse(String raw) : root = json.decode(raw); } diff --git a/lib/src/reverse_engineering/responses/playlist_page.dart b/lib/src/reverse_engineering/responses/playlist_page.dart index 7ea1d37..47fb58a 100644 --- a/lib/src/reverse_engineering/responses/playlist_page.dart +++ b/lib/src/reverse_engineering/responses/playlist_page.dart @@ -24,59 +24,16 @@ class PlaylistPage { return _initialData!; } - final scriptText = root! - .querySelectorAll('script') - .map((e) => e.text) - .toList(growable: false); + final scriptText = root!.querySelectorAll('script').map((e) => e.text).toList(growable: false); - var initialDataText = scriptText - .firstWhereOrNull((e) => e.contains('window["ytInitialData"] =')); - if (initialDataText != null) { - return _InitialData(json - .decode(_extractJson(initialDataText, 'window["ytInitialData"] ='))); - } - - initialDataText = - scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = ')); - if (initialDataText != null) { - return _InitialData( - json.decode(_extractJson(initialDataText, 'var ytInitialData = '))); - } - - throw TransientFailureException( - 'Failed to retrieve initial data from the search page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars - } - - String _extractJson(String html, String separator) { - var index = html.indexOf(separator) + separator.length; - if (index > html.length) { - 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)); - } - - String _matchJson(String str) { - var bracketCount = 0; - var lastI = 0; - for (var i = 0; i < str.length; i++) { - lastI = i; - if (str[i] == '{') { - bracketCount++; - } else if (str[i] == '}') { - bracketCount--; - } else if (str[i] == ';') { - if (bracketCount == 0) { - return str.substring(0, i); - } - } - } - return str.substring(0, lastI + 1); + return scriptText.extractGenericData( + (obj) => _InitialData(obj), + () => TransientFailureException( + 'Failed to retrieve initial data from the search page, please report this to the project GitHub page.')); } /// - PlaylistPage(this.root, this.playlistId, [_InitialData? initialData]) - : _initialData = initialData; + PlaylistPage(this.root, this.playlistId, [_InitialData? initialData]) : _initialData = initialData; /// Future nextPage(YoutubeHttpClient httpClient) async { @@ -87,26 +44,19 @@ class PlaylistPage { } /// - static Future get(YoutubeHttpClient httpClient, String id, - {String? token}) { + static Future get(YoutubeHttpClient httpClient, String id, {String? token}) { if (token != null && token.isNotEmpty) { - var url = - 'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; + var url = 'https://www.youtube.com/youtubei/v1/guide?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; return retry(() async { var body = { 'context': const { - 'client': { - 'hl': 'en', - 'clientName': 'WEB', - 'clientVersion': '2.20200911.04.00' - } + 'client': {'hl': 'en', 'clientName': 'WEB', 'clientVersion': '2.20200911.04.00'} }, 'continuation': token }; - var raw = - await httpClient.post(Uri.parse(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))); }); // Ask for next page, @@ -130,10 +80,7 @@ class _InitialData { _InitialData(this.root); - late final String? title = root - .get('metadata') - ?.get('playlistMetadataRenderer') - ?.getT('title'); + late final String? title = root.get('metadata')?.get('playlistMetadataRenderer')?.getT('title'); late final String? author = root .get('sidebar') @@ -147,10 +94,7 @@ class _InitialData { ?.getT>('runs') ?.parseRuns(); - late final String? description = root - .get('metadata') - ?.get('playlistMetadataRenderer') - ?.getT('description'); + late final String? description = root.get('metadata')?.get('playlistMetadataRenderer')?.getT('description'); late final int? viewCount = root .get('sidebar') @@ -163,13 +107,12 @@ class _InitialData { ?.getT('simpleText') ?.parseInt(); - late final String? continuationToken = - (videosContent ?? playlistVideosContent) - ?.firstWhereOrNull((e) => e['continuationItemRenderer'] != null) - ?.get('continuationItemRenderer') - ?.get('continuationEndpoint') - ?.get('continuationCommand') - ?.getT('token'); + late final String? continuationToken = (videosContent ?? playlistVideosContent) + ?.firstWhereOrNull((e) => e['continuationItemRenderer'] != null) + ?.get('continuationItemRenderer') + ?.get('continuationEndpoint') + ?.get('continuationCommand') + ?.getT('token'); List>? get playlistVideosContent => root @@ -187,11 +130,7 @@ class _InitialData { ?.firstOrNull ?.get('playlistVideoListRenderer') ?.getList('contents') ?? - root - .getList('onResponseReceivedActions') - ?.firstOrNull - ?.get('appendContinuationItemsAction') - ?.getList('continuationItems'); + root.getList('onResponseReceivedActions')?.firstOrNull?.get('appendContinuationItemsAction')?.getList('continuationItems'); late final List>? videosContent = root .get('contents') @@ -199,17 +138,10 @@ class _InitialData { ?.get('primaryContents') ?.get('sectionListRenderer') ?.getList('contents') ?? - root - .getList('onResponseReceivedCommands') - ?.firstOrNull - ?.get('appendContinuationItemsAction') - ?.getList('continuationItems'); + root.getList('onResponseReceivedCommands')?.firstOrNull?.get('appendContinuationItemsAction')?.getList('continuationItems'); List<_Video> get playlistVideos => - playlistVideosContent - ?.where((e) => e['playlistVideoRenderer'] != null) - .map((e) => _Video(e['playlistVideoRenderer'])) - .toList() ?? + playlistVideosContent?.where((e) => e['playlistVideoRenderer'] != null).map((e) => _Video(e['playlistVideoRenderer'])).toList() ?? const []; List<_Video> get videos => @@ -236,13 +168,7 @@ class _Video { ''; String get channelId => - root - .get('ownerText') - ?.getList('runs') - ?.firstOrNull - ?.get('navigationEndpoint') - ?.get('browseEndpoint') - ?.getT('browseId') ?? + root.get('ownerText')?.getList('runs')?.firstOrNull?.get('navigationEndpoint')?.get('browseEndpoint')?.getT('browseId') ?? root .get('shortBylineText') ?.getList('runs') @@ -254,14 +180,11 @@ class _Video { String get title => root.get('title')?.getList('runs')?.parseRuns() ?? ''; - String get description => - root.getList('descriptionSnippet')?.parseRuns() ?? ''; + String get description => root.getList('descriptionSnippet')?.parseRuns() ?? ''; - Duration? get duration => - _stringToDuration(root.get('lengthText')?.getT('simpleText')); + Duration? get duration => _stringToDuration(root.get('lengthText')?.getT('simpleText')); - int get viewCount => - root.get('viewCountText')?.getT('simpleText')?.parseInt() ?? 0; + int get viewCount => root.get('viewCountText')?.getT('simpleText')?.parseInt() ?? 0; /// Format: HH:MM:SS static Duration? _stringToDuration(String? string) { @@ -276,14 +199,10 @@ class _Video { return Duration(seconds: int.parse(parts.first)); } if (parts.length == 2) { - return Duration( - minutes: int.parse(parts.first), seconds: int.parse(parts[1])); + return Duration(minutes: int.parse(parts.first), seconds: int.parse(parts[1])); } if (parts.length == 3) { - return Duration( - hours: int.parse(parts[0]), - minutes: int.parse(parts[1]), - seconds: int.parse(parts[2])); + return Duration(hours: int.parse(parts[0]), minutes: int.parse(parts[1]), seconds: int.parse(parts[2])); } throw Error(); } diff --git a/lib/src/reverse_engineering/responses/search_page.dart b/lib/src/reverse_engineering/responses/search_page.dart index 5ee50e7..08fc39c 100644 --- a/lib/src/reverse_engineering/responses/search_page.dart +++ b/lib/src/reverse_engineering/responses/search_page.dart @@ -28,98 +28,43 @@ class SearchPage { return _initialData!; } - final scriptText = root! - .querySelectorAll('script') - .map((e) => e.text) - .toList(growable: false); - - var initialDataText = scriptText - .firstWhereOrNull((e) => e.contains('window["ytInitialData"] =')); - if (initialDataText != null) { - return _initialData = _InitialData(json - .decode(_extractJson(initialDataText, 'window["ytInitialData"] ='))); - } - - initialDataText = - scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = ')); - if (initialDataText != null) { - return _initialData = _InitialData( - json.decode(_extractJson(initialDataText, 'var ytInitialData = '))); - } - - throw TransientFailureException( - 'Failed to retrieve initial data from the search page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars - } - - String _extractJson(String html, String separator) { - var index = html.indexOf(separator) + separator.length; - if (index > html.length) { - 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)); - } - - String _matchJson(String str) { - var bracketCount = 0; - var lastI = 0; - for (var i = 0; i < str.length; i++) { - lastI = i; - if (str[i] == '{') { - bracketCount++; - } else if (str[i] == '}') { - bracketCount--; - } else if (str[i] == ';') { - if (bracketCount == 0) { - return str.substring(0, i); - } - } - } - return str.substring(0, lastI + 1); + final scriptText = root!.querySelectorAll('script').map((e) => e.text).toList(growable: false); + return scriptText.extractGenericData( + (obj) => _InitialData(obj), + () => TransientFailureException( + 'Failed to retrieve initial data from the search page, please report this to the project GitHub page.')); } /// - SearchPage(this.root, this.queryString, [_InitialData? initialData]) - : _initialData = initialData; + SearchPage(this.root, this.queryString, [_InitialData? initialData]) : _initialData = initialData; Future nextPage(YoutubeHttpClient httpClient) async { - if (initialData.continuationToken == '' || - initialData.estimatedResults == 0) { + if (initialData.continuationToken == '' || initialData.estimatedResults == 0) { return null; } return get(httpClient, queryString, token: initialData.continuationToken); } /// - static Future get( - YoutubeHttpClient httpClient, String queryString, - {String? token}) { + static Future get(YoutubeHttpClient httpClient, String queryString, {String? token}) { if (token != null) { - var url = - 'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; + var url = 'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; return retry(() async { var body = { 'context': const { - 'client': { - 'hl': 'en', - 'clientName': 'WEB', - 'clientVersion': '2.20200911.04.00' - } + 'client': {'hl': 'en', 'clientName': 'WEB', 'clientVersion': '2.20200911.04.00'} }, 'continuation': token }; - var raw = - await httpClient.post(Uri.parse(url), body: json.encode(body)); - return SearchPage( - null, queryString, _InitialData(json.decode(raw.body))); + var raw = await httpClient.post(Uri.parse(url), body: json.encode(body)); + return SearchPage(null, queryString, _InitialData(json.decode(raw.body))); }); // Ask for next page, } - var url = - 'https://www.youtube.com/results?search_query=${Uri.encodeQueryComponent(queryString)}'; + var url = 'https://www.youtube.com/results?search_query=${Uri.encodeQueryComponent(queryString)}'; return retry(() async { var raw = await httpClient.getString(url); return SearchPage.parse(raw, queryString); @@ -197,9 +142,7 @@ class _InitialData { } // Contains only [SearchVideo] or [SearchPlaylist] - late final List searchContent = - getContentContext()?.map(_parseContent).whereNotNull().toList() ?? - const []; + late final List searchContent = getContentContext()?.map(_parseContent).whereNotNull().toList() ?? const []; List get relatedQueries => getContentContext() @@ -207,10 +150,8 @@ class _InitialData { .map((e) => e.get('horizontalCardListRenderer')?.getList('cards')) .firstOrNull ?.map((e) => e['searchRefinementCardRenderer']) - .map((e) => RelatedQuery( - e.searchEndpoint.searchEndpoint.query, - VideoId( - Uri.parse(e.thumbnail.thumbnails.first.url).pathSegments[1]))) + .map((e) => + RelatedQuery(e.searchEndpoint.searchEndpoint.query, VideoId(Uri.parse(e.thumbnail.thumbnails.first.url).pathSegments[1]))) .toList() .cast() ?? const []; @@ -218,11 +159,7 @@ class _InitialData { List get relatedVideos => getContentContext() ?.where((e) => e['shelfRenderer'] != null) - .map((e) => e - .get('shelfRenderer') - ?.get('content') - ?.get('verticalListRenderer') - ?.getList('items')) + .map((e) => e.get('shelfRenderer')?.get('content')?.get('verticalListRenderer')?.getList('items')) .firstOrNull ?.map(_parseContent) .whereNotNull() @@ -231,8 +168,7 @@ class _InitialData { late final String? continuationToken = _getContinuationToken(); - late final int estimatedResults = - int.parse(root.getT('estimatedResults') ?? '0'); + late final int estimatedResults = int.parse(root.getT('estimatedResults') ?? '0'); BaseSearchContent? _parseContent(Map? content) { if (content == null) { @@ -247,47 +183,24 @@ class _InitialData { _parseRuns(renderer.get('ownerText')?.getList('runs')), _parseRuns(renderer.get('descriptionSnippet')?.getList('runs')), renderer.get('lengthText')?.getT('simpleText') ?? '', - int.parse(renderer - .get('viewCountText') - ?.getT('simpleText') - ?.stripNonDigits() - .nullIfWhitespace ?? - renderer - .get('viewCountText') - ?.getList('runs') - ?.firstOrNull - ?.getT('text') - ?.stripNonDigits() - .nullIfWhitespace ?? + int.parse(renderer.get('viewCountText')?.getT('simpleText')?.stripNonDigits().nullIfWhitespace ?? + renderer.get('viewCountText')?.getList('runs')?.firstOrNull?.getT('text')?.stripNonDigits().nullIfWhitespace ?? '0'), (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(), renderer.get('publishedTimeText')?.getT('simpleText'), - renderer - .get('viewCountText') - ?.getList('runs') - ?.elementAtSafe(1) - ?.getT('text') - ?.trim() == - 'watching'); + renderer.get('viewCountText')?.getList('runs')?.elementAtSafe(1)?.getT('text')?.trim() == 'watching'); } if (content['radioRenderer'] != null) { var renderer = content.get('radioRenderer')!; - return SearchPlaylist( - PlaylistId(renderer.getT('playlistId')!), - renderer.get('title')!.getT('simpleText')!, - int.parse(_parseRuns(renderer.get('videoCountText')?.getList('runs')) - .stripNonDigits() - .nullIfWhitespace ?? - '0')); + return SearchPlaylist(PlaylistId(renderer.getT('playlistId')!), renderer.get('title')!.getT('simpleText')!, + int.parse(_parseRuns(renderer.get('videoCountText')?.getList('runs')).stripNonDigits().nullIfWhitespace ?? '0')); } // Here ignore 'horizontalCardListRenderer' & 'shelfRenderer' return null; } - String _parseRuns(List? runs) => - runs?.map((e) => e['text']).join() ?? ''; + String _parseRuns(List? runs) => runs?.map((e) => e['text']).join() ?? ''; } diff --git a/lib/src/reverse_engineering/responses/watch_page.dart b/lib/src/reverse_engineering/responses/watch_page.dart index 4a2e094..3cbf28a 100644 --- a/lib/src/reverse_engineering/responses/watch_page.dart +++ b/lib/src/reverse_engineering/responses/watch_page.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:collection/collection.dart'; import 'package:html/dom.dart'; import 'package:html/parser.dart' as parser; @@ -14,15 +12,11 @@ import 'player_response.dart'; /// class WatchPage { - static final RegExp _videoLikeExp = - RegExp(r'"label"\s*:\s*"([\d,\.]+) likes"'); - static final RegExp _videoDislikeExp = - RegExp(r'"label"\s*:\s*"([\d,\.]+) dislikes"'); - static final RegExp _visitorInfoLiveExp = - RegExp('VISITOR_INFO1_LIVE=([^;]+)'); + static final RegExp _videoLikeExp = RegExp(r'"label"\s*:\s*"([\d,\.]+) likes"'); + static final RegExp _videoDislikeExp = RegExp(r'"label"\s*:\s*"([\d,\.]+) dislikes"'); + static final RegExp _visitorInfoLiveExp = RegExp('VISITOR_INFO1_LIVE=([^;]+)'); static final RegExp _yscExp = RegExp('YSC=([^;]+)'); - static final RegExp _playerResponseExp = - RegExp(r'var\s+ytInitialPlayerResponse\s*=\s*(\{.*\})'); + static final RegExp _playerResponseExp = RegExp(r'var\s+ytInitialPlayerResponse\s*=\s*(\{.*\})'); static final _xsfrTokenExp = RegExp(r'"XSRF_TOKEN"\s*:\s*"(.+?)"'); @@ -57,85 +51,51 @@ class WatchPage { return _initialData!; } - final scriptText = root - .querySelectorAll('script') - .map((e) => e.text) - .toList(growable: false); - - var initialDataText = scriptText - .firstWhereOrNull((e) => e.contains('window["ytInitialData"] =')); - if (initialDataText != null) { - return _initialData = _InitialData(json - .decode(_extractJson(initialDataText, 'window["ytInitialData"] ='))); - } - - initialDataText = - scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = ')); - if (initialDataText != null) { - return _initialData = _InitialData( - json.decode(_extractJson(initialDataText, 'var ytInitialData = '))); - } - - 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 + final scriptText = root.querySelectorAll('script').map((e) => e.text).toList(growable: false); + return scriptText.extractGenericData( + (obj) => _InitialData(obj), + () => TransientFailureException( + 'Failed to retrieve initial data from the watch page, please report this to the project GitHub page.')); } late final String xsfrToken = getXsfrToken()!; /// String? getXsfrToken() { - return _xsfrTokenExp - .firstMatch(root - .querySelectorAll('script') - .firstWhere((e) => _xsfrTokenExp.hasMatch(e.text)) - .text) - ?.group(1); + 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 => - root.querySelector('meta[property="og:url"]') != null; + bool get isVideoAvailable => root.querySelector('meta[property="og:url"]') != null; /// - int get videoLikeCount => int.parse(_videoLikeExp - .firstMatch(root.outerHtml) - ?.group(1) - ?.stripNonDigits() - .nullIfWhitespace ?? - root - .querySelector('.like-button-renderer-like-button') - ?.text - .stripNonDigits() - .nullIfWhitespace ?? + int get videoLikeCount => int.parse(_videoLikeExp.firstMatch(root.outerHtml)?.group(1)?.stripNonDigits().nullIfWhitespace ?? + root.querySelector('.like-button-renderer-like-button')?.text.stripNonDigits().nullIfWhitespace ?? '0'); /// - int get videoDislikeCount => int.parse(_videoDislikeExp - .firstMatch(root.outerHtml) - ?.group(1) - ?.stripNonDigits() - .nullIfWhitespace ?? - root - .querySelector('.like-button-renderer-dislike-button') - ?.text - .stripNonDigits() - .nullIfWhitespace ?? + int get videoDislikeCount => int.parse(_videoDislikeExp.firstMatch(root.outerHtml)?.group(1)?.stripNonDigits().nullIfWhitespace ?? + root.querySelector('.like-button-renderer-dislike-button')?.text.stripNonDigits().nullIfWhitespace ?? '0'); static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\})'); - late final WatchPlayerConfig playerConfig = WatchPlayerConfig(json.decode( - _playerConfigExp - .firstMatch(root.getElementsByTagName('html').first.text) - ?.group(1) - ?.extractJson() ?? - 'a')); + late final WatchPlayerConfig? playerConfig = getPlayerConfig(); late final PlayerResponse? playerResponse = getPlayerResponse(); + /// + WatchPlayerConfig? getPlayerConfig() { + final jsonMap = _playerConfigExp.firstMatch(root.getElementsByTagName('html').first.text)?.group(1)?.extractJson(); + if (jsonMap == null) { + return null; + } + return WatchPlayerConfig(jsonMap); + } + /// PlayerResponse? getPlayerResponse() { final val = root @@ -147,18 +107,14 @@ class WatchPage { if (val == null) { return null; } - return PlayerResponse.parse(val); + return PlayerResponse(val); } - String _extractJson(String html, String separator) => - html.substring(html.indexOf(separator) + separator.length).extractJson(); - /// WatchPage(this.root, this.visitorInfoLive, this.ysc); /// - WatchPage.parse(String raw, this.visitorInfoLive, this.ysc) - : root = parser.parse(raw); + WatchPage.parse(String raw, this.visitorInfoLive, this.ysc) : root = parser.parse(raw); /// static Future get(YoutubeHttpClient httpClient, String videoId) { @@ -167,9 +123,9 @@ class WatchPage { var req = await httpClient.get(url, validate: true); 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 result = WatchPage.parse(req.body, visitorInfoLive, ysc); + var result = WatchPage.parse(req.body, visitorInfoLive ?? '', ysc); if (!result.isOk) { throw TransientFailureException('Video watch page is broken.'); @@ -192,12 +148,10 @@ class WatchPlayerConfig implements PlayerConfigBase> { WatchPlayerConfig(this.root); @override - late final String sourceUrl = - 'https://youtube.com${root.get('assets')!.getT('js')}'; + late final String sourceUrl = 'https://youtube.com${root.get('assets')!.getT('js')}'; /// - late final PlayerResponse playerResponse = - PlayerResponse.parse(root.get('args')!.getT('playerResponse')!); + late final PlayerResponse playerResponse = PlayerResponse.parse(root.get('args')!.getT('playerResponse')!); } class _InitialData { @@ -223,9 +177,7 @@ class _InitialData { return null; } - late final String continuation = - getContinuationContext()?.getT('continuation') ?? ''; + late final String continuation = getContinuationContext()?.getT('continuation') ?? ''; - late final String clickTrackingParams = - getContinuationContext()?.getT('clickTrackingParams') ?? ''; + late final String clickTrackingParams = getContinuationContext()?.getT('clickTrackingParams') ?? ''; } diff --git a/lib/src/videos/streams/streams_client.dart b/lib/src/videos/streams/streams_client.dart index c852ecf..1fd92b9 100644 --- a/lib/src/videos/streams/streams_client.dart +++ b/lib/src/videos/streams/streams_client.dart @@ -21,10 +21,8 @@ class StreamsClient { /// Initializes an instance of [StreamsClient] StreamsClient(this._httpClient); - Future _getDashManifest( - Uri dashManifestUrl, Iterable cipherOperations) { - var signature = - DashManifest.getSignatureFromUrl(dashManifestUrl.toString()); + Future _getDashManifest(Uri dashManifestUrl, Iterable cipherOperations) { + var signature = DashManifest.getSignatureFromUrl(dashManifestUrl.toString()); if (!signature.isNullOrWhiteSpace) { signature = cipherOperations.decipher(signature!); dashManifestUrl = dashManifestUrl.setQueryParam('signature', signature); @@ -39,74 +37,56 @@ class StreamsClient { throw VideoUnplayableException.unplayable(videoId); } - var playerSource = await PlayerSource.get( - _httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl); + var playerSource = await PlayerSource.get(_httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl); var cipherOperations = playerSource.getCipherOperations(); - var videoInfoResponse = await VideoInfoResponse.get( - _httpClient, videoId.toString(), playerSource.sts); + var videoInfoResponse = await VideoInfoResponse.get(_httpClient, videoId.toString(), playerSource.sts); var playerResponse = videoInfoResponse.playerResponse; var previewVideoId = playerResponse.previewVideoId; if (!previewVideoId.isNullOrWhiteSpace) { - throw VideoRequiresPurchaseException.preview( - videoId, VideoId(previewVideoId!)); + throw VideoRequiresPurchaseException.preview(videoId, VideoId(previewVideoId!)); } if (!playerResponse.isVideoPlayable) { - throw VideoUnplayableException.unplayable(videoId, - reason: playerResponse.videoPlayabilityError ?? ''); + throw VideoUnplayableException.unplayable(videoId, reason: playerResponse.videoPlayabilityError ?? ''); } if (playerResponse.isLive) { throw VideoUnplayableException.liveStream(videoId); } - var streamInfoProviders = [ - ...videoInfoResponse.streams, - ...playerResponse.streams - ]; + var streamInfoProviders = [...videoInfoResponse.streams, ...playerResponse.streams]; var dashManifestUrl = playerResponse.dashManifestUrl; if (!dashManifestUrl.isNullOrWhiteSpace) { - var dashManifest = - await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations); + var dashManifest = await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations); streamInfoProviders.addAll(dashManifest.streams); } return StreamContext(streamInfoProviders, cipherOperations); } Future _getStreamContextFromWatchPage(VideoId videoId) async { - var watchPage = await WatchPage.get(_httpClient, videoId.toString()); + final watchPage = await WatchPage.get(_httpClient, videoId.toString()); - WatchPlayerConfig? playerConfig; - try { - playerConfig = watchPage.playerConfig; - } on FormatException { - playerConfig = null; - } - var playerResponse = - playerConfig?.playerResponse ?? watchPage.playerResponse; + final playerConfig = watchPage.playerConfig; + + var playerResponse = playerConfig?.playerResponse ?? watchPage.playerResponse; if (playerResponse == null) { throw VideoUnplayableException.unplayable(videoId); } var previewVideoId = playerResponse.previewVideoId; if (!previewVideoId.isNullOrWhiteSpace) { - throw VideoRequiresPurchaseException.preview( - videoId, VideoId(previewVideoId!)); + throw VideoRequiresPurchaseException.preview(videoId, VideoId(previewVideoId!)); } var playerSourceUrl = watchPage.sourceUrl ?? playerConfig?.sourceUrl; - var playerSource = !playerSourceUrl.isNullOrWhiteSpace - ? await PlayerSource.get(_httpClient, playerSourceUrl!) - : null; - var cipherOperations = - playerSource?.getCipherOperations() ?? const []; + var playerSource = !playerSourceUrl.isNullOrWhiteSpace ? await PlayerSource.get(_httpClient, playerSourceUrl!) : null; + var cipherOperations = playerSource?.getCipherOperations() ?? const []; if (!playerResponse.isVideoPlayable) { - throw VideoUnplayableException.unplayable(videoId, - reason: playerResponse.videoPlayabilityError ?? ''); + throw VideoUnplayableException.unplayable(videoId, reason: playerResponse.videoPlayabilityError ?? ''); } if (playerResponse.isLive) { @@ -119,8 +99,7 @@ class StreamsClient { var dashManifestUrl = playerResponse.dashManifestUrl; if (!(dashManifestUrl?.isNullOrWhiteSpace ?? true)) { - var dashManifest = - await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations); + var dashManifest = await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations); streamInfoProviders.addAll(dashManifest.streams); } return StreamContext(streamInfoProviders, cipherOperations); @@ -144,9 +123,7 @@ class StreamsClient { } // Content length - var contentLength = streamInfo.contentLength ?? - await _httpClient.getContentLength(url, validate: false) ?? - 0; + var contentLength = streamInfo.contentLength ?? await _httpClient.getContentLength(url, validate: false) ?? 0; if (contentLength <= 0) { continue; @@ -163,53 +140,31 @@ class StreamsClient { // Muxed or Video-only if (!videoCodec.isNullOrWhiteSpace) { var framerate = Framerate(streamInfo.framerate ?? 24); - var videoQualityLabel = streamInfo.videoQualityLabel ?? - VideoQualityUtil.getLabelFromTagWithFramerate( - tag, framerate.framesPerSecond.toDouble()); + var videoQualityLabel = + streamInfo.videoQualityLabel ?? VideoQualityUtil.getLabelFromTagWithFramerate(tag, framerate.framesPerSecond.toDouble()); var videoQuality = VideoQualityUtil.fromLabel(videoQualityLabel); var videoWidth = streamInfo.videoWidth; var videoHeight = streamInfo.videoHeight; - var videoResolution = videoWidth != -1 && videoHeight != -1 - ? VideoResolution(videoWidth ?? 0, videoHeight ?? 0) - : videoQuality.toVideoResolution(); + var videoResolution = + videoWidth != -1 && videoHeight != -1 ? VideoResolution(videoWidth ?? 0, videoHeight ?? 0) : videoQuality.toVideoResolution(); // Muxed if (!audioCodec.isNullOrWhiteSpace) { - streams[tag] = MuxedStreamInfo( - tag, - url, - container, - fileSize, - bitrate, - audioCodec!, - videoCodec!, - videoQualityLabel, - videoQuality, - videoResolution, - framerate); + streams[tag] = MuxedStreamInfo(tag, url, container, fileSize, bitrate, audioCodec!, videoCodec!, videoQualityLabel, videoQuality, + videoResolution, framerate); continue; } // Video only streams[tag] = VideoOnlyStreamInfo( - tag, - url, - container, - fileSize, - bitrate, - videoCodec!, - videoQualityLabel, - videoQuality, - videoResolution, - framerate); + tag, url, container, fileSize, bitrate, videoCodec!, videoQualityLabel, videoQuality, videoResolution, framerate); continue; } // Audio-only if (!audioCodec.isNullOrWhiteSpace) { - streams[tag] = AudioOnlyStreamInfo( - tag, url, container, fileSize, bitrate, audioCodec!); + streams[tag] = AudioOnlyStreamInfo(tag, url, container, fileSize, bitrate, audioCodec!); } // #if DEBUG @@ -239,12 +194,10 @@ class StreamsClient { /// Gets the HTTP Live Stream (HLS) manifest URL /// for the specified video (if it's a live video stream). Future getHttpLiveStreamUrl(VideoId videoId) async { - var videoInfoResponse = - await VideoInfoResponse.get(_httpClient, videoId.toString()); + var videoInfoResponse = await VideoInfoResponse.get(_httpClient, videoId.toString()); var playerResponse = videoInfoResponse.playerResponse; if (!playerResponse.isVideoPlayable) { - throw VideoUnplayableException.unplayable(videoId, - reason: playerResponse.videoPlayabilityError ?? ''); + throw VideoUnplayableException.unplayable(videoId, reason: playerResponse.videoPlayabilityError ?? ''); } var hlsManifest = playerResponse.hlsManifestUrl; @@ -255,6 +208,5 @@ class StreamsClient { } /// Gets the actual stream which is identified by the specified metadata. - Stream> get(StreamInfo streamInfo) => - _httpClient.getStream(streamInfo); + Stream> get(StreamInfo streamInfo) => _httpClient.getStream(streamInfo); } diff --git a/pubspec.yaml b/pubspec.yaml index f1ab391..1f1e16e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: youtube_explode_dart description: A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. -version: 1.9.0-nullsafety.3 +version: 1.9.0-nullsafety.4 homepage: https://github.com/Hexer10/youtube_explode_dart diff --git a/test/video_test.dart b/test/video_test.dart index fbb7027..2f35e80 100644 --- a/test/video_test.dart +++ b/test/video_test.dart @@ -25,7 +25,7 @@ void main() { inInclusiveRange(rangeMs - 86400000, rangeMs + 86400000)); expect(video.description, contains('246pp')); // Should be 1:38 but sometimes it differs - // so where using a 10 seconds range from it. + // so we're using a 10 seconds range from it. expect(video.duration!.inSeconds, inInclusiveRange(108, 118)); expect(video.thumbnails.lowResUrl, isNotEmpty); expect(video.thumbnails.mediumResUrl, isNotEmpty);