diff --git a/.gitignore b/.gitignore index 522bf7b..371e8c5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,6 @@ doc/api/ .idea/ .vscode/ *.iml -/tool/ +/bin/ .flutter-plugins-dependencies \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f29abd..9a94330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 1.4.2 - Implement `getSrt` a video closed captions in srt format. - Only throw custom exceptions from the library. +- `getUploadsFromPage` no longer throws. ## 1.4.1+1 - Bug fixes diff --git a/analysis_options.yaml b/analysis_options.yaml index 3464177..9d860e8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,10 +1,5 @@ -# Defines a default set of lint rules enforced for -# projects at Google. For details and rationale, -# see https://github.com/dart-lang/pedantic#enabled-lints. include: package:effective_dart/analysis_options.yaml -# For lint rules and documentation, see http://dart-lang.github.io/linter/lints. -# Uncomment to specify additional rules. linter: rules: - valid_regexps @@ -59,6 +54,8 @@ linter: - use_string_buffers - void_checks - package_names + - prefer_single_quotes + - use_function_type_syntax_for_parameters analyzer: exclude: diff --git a/example/video_download_flutter/.flutter-plugins-dependencies b/example/video_download_flutter/.flutter-plugins-dependencies deleted file mode 100644 index 9130528..0000000 --- a/example/video_download_flutter/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"downloads_path_provider","path":"D:\\\\Tools\\\\flutter-sdk-stable\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\downloads_path_provider-0.1.0\\\\","dependencies":[]},{"name":"permission_handler","path":"D:\\\\Tools\\\\flutter-sdk-stable\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-5.0.1\\\\","dependencies":[]}],"android":[{"name":"downloads_path_provider","path":"D:\\\\Tools\\\\flutter-sdk-stable\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\downloads_path_provider-0.1.0\\\\","dependencies":[]},{"name":"permission_handler","path":"D:\\\\Tools\\\\flutter-sdk-stable\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-5.0.1\\\\","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"downloads_path_provider","dependencies":[]},{"name":"permission_handler","dependencies":[]}],"date_created":"2020-06-14 15:48:21.493261","version":"1.17.3"} \ No newline at end of file diff --git a/lib/src/channels/channel_client.dart b/lib/src/channels/channel_client.dart index 351328d..8ad79eb 100644 --- a/lib/src/channels/channel_client.dart +++ b/lib/src/channels/channel_client.dart @@ -1,16 +1,15 @@ -import 'channel_video.dart'; -import 'video_sorting.dart'; -import '../reverse_engineering/responses/channel_upload_page.dart'; - import '../extensions/helpers_extension.dart'; import '../playlists/playlists.dart'; +import '../reverse_engineering/responses/channel_upload_page.dart'; import '../reverse_engineering/responses/responses.dart'; import '../reverse_engineering/youtube_http_client.dart'; import '../videos/video.dart'; import '../videos/video_id.dart'; import 'channel.dart'; import 'channel_id.dart'; +import 'channel_video.dart'; import 'username.dart'; +import 'video_sorting.dart'; /// Queries related to YouTube channels. class ChannelClient { diff --git a/lib/src/channels/channel_video.dart b/lib/src/channels/channel_video.dart index 3e8ab73..1721d61 100644 --- a/lib/src/channels/channel_video.dart +++ b/lib/src/channels/channel_video.dart @@ -1,5 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:youtube_explode_dart/src/videos/video_id.dart'; +import '../videos/video_id.dart'; /// Metadata related to a search query result (playlist) class ChannelVideo with EquatableMixin { diff --git a/lib/src/retry.dart b/lib/src/retry.dart index ee49cb8..562ea47 100644 --- a/lib/src/retry.dart +++ b/lib/src/retry.dart @@ -6,7 +6,7 @@ import 'exceptions/exceptions.dart'; /// Run the [function] each time an exception is thrown until the retryCount /// is 0. -Future retry(FutureOr function()) async { +Future retry(FutureOr Function() function) async { var retryCount = 5; // ignore: literal_only_boolean_expressions @@ -27,7 +27,6 @@ Future retry(FutureOr function()) async { /// Get "retry" cost of each YoutubeExplode exception. int getExceptionCost(Exception e) { if (e is TransientFailureException || e is FormatException) { - print('Ripperoni!'); return 1; } if (e is RequestLimitExceededException) { diff --git a/lib/src/reverse_engineering/heuristics.dart b/lib/src/reverse_engineering/heuristics.dart index 80118cc..d0c7395 100644 --- a/lib/src/reverse_engineering/heuristics.dart +++ b/lib/src/reverse_engineering/heuristics.dart @@ -168,8 +168,10 @@ extension VideoQualityUtil on VideoQuality { label, 'label', 'Unrecognized video quality label'); } + /// String getLabel() => '${toString().stripNonDigits()}p'; + /// String getLabelWithFramerate(double framerate) { // Framerate appears only if it's above 30 if (framerate <= 30) { @@ -180,6 +182,7 @@ extension VideoQualityUtil on VideoQuality { return '${getLabel()}$framerateRounded'; } + /// static String getLabelFromTagWithFramerate(int itag, double framerate) { var videoQuality = fromTag(itag); return videoQuality.getLabelWithFramerate(framerate); diff --git a/lib/src/reverse_engineering/responses/channel_page.dart b/lib/src/reverse_engineering/responses/channel_page.dart index 7e6fd69..990efbf 100644 --- a/lib/src/reverse_engineering/responses/channel_page.dart +++ b/lib/src/reverse_engineering/responses/channel_page.dart @@ -6,26 +6,35 @@ import '../../extensions/helpers_extension.dart'; import '../../retry.dart'; import '../youtube_http_client.dart'; +/// class ChannelPage { final Document _root; + /// bool get isOk => _root.querySelector('meta[property="og:url"]') != null; + /// String get channelUrl => _root.querySelector('meta[property="og:url"]')?.attributes['content']; + /// String get channelId => channelUrl.substringAfter('channel/'); + /// String get channelTitle => _root.querySelector('meta[property="og:title"]')?.attributes['content']; + /// String get channelLogoUrl => _root.querySelector('meta[property="og:image"]')?.attributes['content']; + /// ChannelPage(this._root); + /// ChannelPage.parse(String raw) : _root = parser.parse(raw); + /// static Future get(YoutubeHttpClient httpClient, String id) { var url = 'https://www.youtube.com/channel/$id?hl=en'; @@ -40,6 +49,7 @@ class ChannelPage { }); } + /// static Future getByUsername( YoutubeHttpClient httpClient, String username) { var url = 'https://www.youtube.com/user/$username?hl=en'; diff --git a/lib/src/reverse_engineering/responses/channel_upload_page.dart b/lib/src/reverse_engineering/responses/channel_upload_page.dart index 6af906a..7c8c6b5 100644 --- a/lib/src/reverse_engineering/responses/channel_upload_page.dart +++ b/lib/src/reverse_engineering/responses/channel_upload_page.dart @@ -2,20 +2,23 @@ import 'dart:convert'; import 'package:html/dom.dart'; import 'package:html/parser.dart' as parser; -import 'package:youtube_explode_dart/src/exceptions/exceptions.dart'; import '../../channels/channel_video.dart'; +import '../../exceptions/exceptions.dart'; import '../../extensions/helpers_extension.dart'; import '../../retry.dart'; import '../../videos/videos.dart'; import '../youtube_http_client.dart'; +/// class ChannelUploadPage { + /// final String channelId; final Document _root; _InitialData _initialData; + /// _InitialData get initialData => _initialData ??= _InitialData(json.decode(_matchJson(_extractJson( _root @@ -48,9 +51,11 @@ class ChannelUploadPage { return str.substring(0, lastI + 1); } + /// ChannelUploadPage(this._root, this.channelId, [_InitialData initialData]) : _initialData = initialData; + /// Future nextPage(YoutubeHttpClient httpClient) { if (initialData.continuation.isEmpty) { return Future.value(null); @@ -64,6 +69,7 @@ class ChannelUploadPage { }); } + /// static Future get( YoutubeHttpClient httpClient, String channelId, String sorting) { assert(sorting != null); @@ -75,6 +81,7 @@ class ChannelUploadPage { }); } + /// ChannelUploadPage.parse(String raw, this.channelId) : _root = parser.parse(raw); } diff --git a/lib/src/reverse_engineering/responses/closed_caption_track_response.dart b/lib/src/reverse_engineering/responses/closed_caption_track_response.dart index 5f7325e..8937baf 100644 --- a/lib/src/reverse_engineering/responses/closed_caption_track_response.dart +++ b/lib/src/reverse_engineering/responses/closed_caption_track_response.dart @@ -3,18 +3,24 @@ import 'package:xml/xml.dart' as xml; import '../../retry.dart'; import '../youtube_http_client.dart'; +/// class ClosedCaptionTrackResponse { final xml.XmlDocument _root; Iterable _closedCaptions; + /// Iterable get closedCaptions => _closedCaptions ??= _root.findAllElements('p').map((e) => ClosedCaption._(e)); + /// ClosedCaptionTrackResponse(this._root); + /// + // ignore: deprecated_member_use ClosedCaptionTrackResponse.parse(String raw) : _root = xml.parse(raw); + /// static Future get( YoutubeHttpClient httpClient, String url) { var formatUrl = _setQueryParameters(url, {'format': '3'}); @@ -34,6 +40,7 @@ class ClosedCaptionTrackResponse { } } +/// class ClosedCaption { final xml.XmlElement _root; @@ -42,29 +49,37 @@ class ClosedCaption { Duration _end; Iterable _parts; + /// String get text => _root.text; + /// Duration get offset => _offset ??= Duration(milliseconds: int.parse(_root.getAttribute('t') ?? 0)); + /// Duration get duration => _duration ??= Duration(milliseconds: int.parse(_root.getAttribute('d') ?? 0)); + /// Duration get end => _end ??= offset + duration; + /// Iterable getParts() => _parts ??= _root.findAllElements('s').map((e) => ClosedCaptionPart._(e)); ClosedCaption._(this._root); } +/// class ClosedCaptionPart { final xml.XmlElement _root; Duration _offset; + /// String get text => _root.text; + /// Duration get offset => _offset ??= Duration(milliseconds: int.parse(_root.getAttribute('t') ?? '0')); diff --git a/lib/src/reverse_engineering/responses/dash_manifest.dart b/lib/src/reverse_engineering/responses/dash_manifest.dart index e3bfb74..a40519b 100644 --- a/lib/src/reverse_engineering/responses/dash_manifest.dart +++ b/lib/src/reverse_engineering/responses/dash_manifest.dart @@ -4,12 +4,15 @@ import '../../retry.dart'; import '../youtube_http_client.dart'; import 'stream_info_provider.dart'; +/// class DashManifest { static final _urlSignatureExp = RegExp(r'/s/(.*?)(?:/|$)'); final xml.XmlDocument _root; Iterable<_StreamInfo> _streams; + + /// Iterable<_StreamInfo> get streams => _streams ??= _root .findElements('Representation') .where((e) => e @@ -19,11 +22,14 @@ class DashManifest { .contains('sq/')) .map((e) => _StreamInfo(e)); + /// DashManifest(this._root); + /// // ignore: deprecated_member_use DashManifest.parse(String raw) : _root = xml.parse(raw); + /// static Future get(YoutubeHttpClient httpClient, dynamic url) { return retry(() async { var raw = await httpClient.getString(url); @@ -31,6 +37,7 @@ class DashManifest { }); } + /// static String getSignatureFromUrl(String url) => _urlSignatureExp.firstMatch(url)?.group(1); } diff --git a/lib/src/reverse_engineering/responses/embed_page.dart b/lib/src/reverse_engineering/responses/embed_page.dart index 6f483c2..f404247 100644 --- a/lib/src/reverse_engineering/responses/embed_page.dart +++ b/lib/src/reverse_engineering/responses/embed_page.dart @@ -7,6 +7,7 @@ import '../../extensions/helpers_extension.dart'; import '../../retry.dart'; import '../youtube_http_client.dart'; +/// class EmbedPage { static final _playerConfigExp = RegExp(r"yt\.setConfig\({'PLAYER_CONFIG':(.*)}\);"); @@ -15,6 +16,7 @@ class EmbedPage { _PlayerConfig _playerConfig; String __playerConfigJson; + /// _PlayerConfig get playerconfig { if (_playerConfig != null) { return _playerConfig; @@ -32,10 +34,13 @@ class EmbedPage { .map((e) => _playerConfigExp.firstMatch(e)?.group(1)) .firstWhere((e) => !e.isNullOrWhiteSpace, orElse: () => null); + /// EmbedPage(this._root); + /// EmbedPage.parse(String raw) : _root = parser.parse(raw); + /// static Future get(YoutubeHttpClient httpClient, String videoId) { var url = 'https://youtube.com/embed/$videoId?hl=en'; return retry(() async { diff --git a/lib/src/reverse_engineering/responses/player_response.dart b/lib/src/reverse_engineering/responses/player_response.dart index b4ecd67..29015c3 100644 --- a/lib/src/reverse_engineering/responses/player_response.dart +++ b/lib/src/reverse_engineering/responses/player_response.dart @@ -5,6 +5,7 @@ import 'package:http_parser/http_parser.dart'; import '../../extensions/helpers_extension.dart'; import 'stream_info_provider.dart'; +/// class PlayerResponse { // Json parsed map final Map _root; @@ -15,31 +16,43 @@ class PlayerResponse { Iterable _closedCaptionTrack; String _videoPlayabilityError; + /// String get playabilityStatus => _root['playabilityStatus']['status']; + /// bool get isVideoAvailable => playabilityStatus.toLowerCase() != 'error'; + /// bool get isVideoPlayable => playabilityStatus.toLowerCase() == 'ok'; + /// String get videoTitle => _root['videoDetails']['title']; + /// String get videoAuthor => _root['videoDetails']['author']; + /// DateTime get videoUploadDate => DateTime.parse( _root['microformat']['playerMicroformatRenderer']['uploadDate']); + /// String get videoChannelId => _root['videoDetails']['channelId']; + /// Duration get videoDuration => Duration(seconds: int.parse(_root['videoDetails']['lengthSeconds'])); + /// Iterable get videoKeywords => _root['videoDetails']['keywords']?.cast() ?? const []; + /// String get videoDescription => _root['videoDetails']['shortDescription']; + /// int get videoViewCount => int.parse(_root['videoDetails']['viewCount']); + /// // Can be null String get previewVideoId => _root @@ -55,16 +68,20 @@ class PlayerResponse { ?.getValue('playerVars') ?? '')['video_id']; + /// bool get isLive => _root.get('videoDetails')?.getValue('isLive') ?? false; + /// // Can be null String get hlsManifestUrl => _root.get('streamingData')?.getValue('hlsManifestUrl'); + /// // Can be null String get dashManifestUrl => _root.get('streamingData')?.getValue('dashManifestUrl'); + /// Iterable get muxedStreams => _muxedStreams ??= _root ?.get('streamingData') ?.getValue('formats') @@ -72,6 +89,7 @@ class PlayerResponse { ?.cast() ?? const []; + /// Iterable get adaptiveStreams => _adaptiveStreams ??= _root ?.get('streamingData') ?.getValue('adaptiveFormats') @@ -79,9 +97,11 @@ class PlayerResponse { ?.cast() ?? const []; + /// List get streams => _streams ??= [...muxedStreams, ...adaptiveStreams]; + /// Iterable get closedCaptionTrack => _closedCaptionTrack ??= _root .get('captions') @@ -91,26 +111,35 @@ class PlayerResponse { ?.cast() ?? const []; + /// PlayerResponse(this._root); + /// String getVideoPlayabilityError() => _videoPlayabilityError ??= _root.get('playabilityStatus')?.getValue('reason'); + /// PlayerResponse.parse(String raw) : _root = json.decode(raw); } +/// class ClosedCaptionTrack { // Json parsed map final Map _root; + /// String get url => _root['baseUrl']; + /// String get languageCode => _root['languageCode']; + /// String get languageName => _root['name']['simpleText']; - bool get autoGenerated => _root['vssId'].toLowerCase().startsWith("a."); + /// + bool get autoGenerated => _root['vssId'].toLowerCase().startsWith('a.'); + /// ClosedCaptionTrack(this._root); } diff --git a/lib/src/reverse_engineering/responses/player_source.dart b/lib/src/reverse_engineering/responses/player_source.dart index 2aa51ab..2129ecf 100644 --- a/lib/src/reverse_engineering/responses/player_source.dart +++ b/lib/src/reverse_engineering/responses/player_source.dart @@ -5,6 +5,7 @@ import '../../retry.dart'; import '../cipher/cipher_operations.dart'; import '../youtube_http_client.dart'; +/// class PlayerSource { final RegExp _statIndexExp = RegExp(r'\(\w+,(\d+)\)'); @@ -20,6 +21,7 @@ class PlayerSource { String _sts; String _deciphererDefinitionBody; + /// String get sts { if (_sts != null) { return _sts; @@ -33,6 +35,7 @@ class PlayerSource { return _sts ??= val; } + /// Iterable getCiperOperations() sync* { var funcBody = _getDeciphererFuncBody(); @@ -102,11 +105,14 @@ class PlayerSource { return exp.firstMatch(_root).group(0).nullIfWhitespace; } + /// PlayerSource(this._root); + /// // Same as default constructor PlayerSource.parse(this._root); + /// static Future get( YoutubeHttpClient httpClient, String url) async { if (_cache[url] == null) { diff --git a/lib/src/reverse_engineering/responses/playlist_response.dart b/lib/src/reverse_engineering/responses/playlist_response.dart index 4298948..7a41ac6 100644 --- a/lib/src/reverse_engineering/responses/playlist_response.dart +++ b/lib/src/reverse_engineering/responses/playlist_response.dart @@ -7,37 +7,49 @@ import '../../extensions/helpers_extension.dart'; import '../../retry.dart'; import '../youtube_http_client.dart'; +/// class PlaylistResponse { Iterable<_Video> _videos; // Json parsed map final Map _root; + /// String get title => _root['title']; + /// String get author => _root['author']; + /// String get description => _root['description']; + /// ThumbnailSet get thumbnails => ThumbnailSet(videos.firstOrNull.id); + /// int get viewCount => _root['views']; + /// int get likeCount => _root['likes']; + /// int get dislikeCount => _root['dislikes']; + /// Iterable<_Video> get videos => _videos ??= _root['video']?.map((e) => _Video(e))?.cast<_Video>() ?? const <_Video>[]; + /// PlaylistResponse(this._root); + /// PlaylistResponse.parse(String raw) : _root = json.tryDecode(raw) { if (_root == null) { throw TransientFailureException('Playerlist response is broken.'); } } + /// static Future get(YoutubeHttpClient httpClient, String id, {int index = 0}) { var url = @@ -48,6 +60,7 @@ class PlaylistResponse { }); } + /// static Future searchResults( YoutubeHttpClient httpClient, String query, {int page = 0}) { diff --git a/lib/src/reverse_engineering/responses/search_page.dart b/lib/src/reverse_engineering/responses/search_page.dart index 767b8b5..84043e2 100644 --- a/lib/src/reverse_engineering/responses/search_page.dart +++ b/lib/src/reverse_engineering/responses/search_page.dart @@ -13,15 +13,18 @@ import '../../search/search_video.dart'; import '../../videos/videos.dart'; import '../youtube_http_client.dart'; +/// class SearchPage { static final _xsfrTokenExp = RegExp('"XSRF_TOKEN":"(.+?)"'); + /// final String queryString; final Document _root; _InitialData _initialData; String _xsrfToken; + /// _InitialData get initialData => _initialData ??= _InitialData(json.decode(_matchJson(_extractJson( _root @@ -31,6 +34,7 @@ class SearchPage { .firstWhere((e) => e.contains('window["ytInitialData"] =')), 'window["ytInitialData"] =')))); + /// String get xsfrToken => _xsrfToken ??= _xsfrTokenExp .firstMatch(_root .querySelectorAll('script') @@ -61,13 +65,15 @@ class SearchPage { return str.substring(0, lastI + 1); } + /// SearchPage(this._root, this.queryString, [_InitialData initalData, String xsfrToken]) : _initialData = initalData, _xsrfToken = xsfrToken; + /// // TODO: Replace this in favour of async* when quering; - Future nextPage(YoutubeHttpClient httpClient) { + Future nextPage(YoutubeHttpClient httpClient) async { if (initialData.continuation == '') { return null; } @@ -77,6 +83,7 @@ class SearchPage { xsrfToken: xsfrToken); } + /// static Future get( YoutubeHttpClient httpClient, String queryString, {String ctoken, String itct, String xsrfToken}) { @@ -103,6 +110,7 @@ class SearchPage { }); } + /// SearchPage.parse(String raw, this.queryString) : _root = parser.parse(raw); } @@ -229,7 +237,10 @@ class _InitialData { runs?.getValue('runs')?.map((e) => e['text'])?.join() ?? ''; } -// ['contents']['twoColumnSearchResultsRenderer']['primaryContents']['sectionListRenderer']['contents'].first['itemSectionRenderer'] +// ['contents']['twoColumnSearchResultsRenderer']['primaryContents'] +// ['sectionListRenderer']['contents'].first['itemSectionRenderer'] +// +// // ['contents'] -> @See ContentsList // ['continuations'] -> Data to see more @@ -237,10 +248,12 @@ class _InitialData { // Key -> 'videoRenderer' // videoId --> VideoId // title['runs'].loop -> ['text'] -> concatenate --> "Video Title" -// descriptionSnippet['runs'].loop -> ['text'] -> concatenate --> "Video Description snippet" +// descriptionSnippet['runs'].loop -> ['text'] -> concatenate +// --> "Video Description snippet" // ownerText['runs'].first -> ['text'] --> "Video Author" // lengthText['simpleText'] -> Parse format H:M:S -> "Video Duration" -// viewCountText['simpleText'] -> Strip non digit -> int.parse --> "Video View Count" +// viewCountText['simpleText'] -> Strip non digit -> int.parse +// --> "Video View Count" // // Key -> 'radioRenderer' // playlistId -> PlaylistId @@ -248,8 +261,10 @@ class _InitialData { // // Key -> 'horizontalCardListRenderer' // Queries related to this search // cards --> List of Maps -> loop -> ['searchRefinementCardRenderer'].first -// thumbnail -> ['thumbnails'].first -> ['url'] --> "Thumbnail url" -> Find video id from id. -// searchEndpoint -> ['searchEndpoint'] -> ['query'] -> "Related query string" +// thumbnail -> ['thumbnails'].first -> ['url'] +// --> "Thumbnail url" -> Find video id from id. +// searchEndpoint -> ['searchEndpoint'] +// -> ['query'] -> "Related query string" // // Key -> 'shelfRenderer' // Videos related to this search // contents -> ['verticalListRenderer']['items'] -> loop -> parseContent diff --git a/lib/src/reverse_engineering/responses/stream_info_provider.dart b/lib/src/reverse_engineering/responses/stream_info_provider.dart index 04db88d..655448f 100644 --- a/lib/src/reverse_engineering/responses/stream_info_provider.dart +++ b/lib/src/reverse_engineering/responses/stream_info_provider.dart @@ -1,38 +1,66 @@ +/// abstract class StreamInfoProvider { + /// static final RegExp contentLenExp = RegExp(r'clen=(\d+)'); + /// int get tag; + /// String get url; + /// // Can be null + // ignore: avoid_returning_null String get signature => null; + /// // Can be null + // ignore: avoid_returning_null String get signatureParameter => null; + /// // Can be null + // ignore: avoid_returning_null int get contentLength => null; + /// + // Can be null + // ignore: avoid_returning_null int get bitrate; + /// + // Can be null + // ignore: avoid_returning_null String get container; + /// // Can be null + // ignore: avoid_returning_null String get audioCodec => null; + /// // Can be null + // ignore: avoid_returning_null String get videoCodec => null; + /// // Can be null + // ignore: avoid_returning_null String get videoQualityLabel => null; + /// // Can be null + // ignore: avoid_returning_null int get videoWidth => null; + /// // Can be null + // ignore: avoid_returning_null int get videoHeight => null; + /// // Can be null + // ignore: avoid_returning_null int get framerate => null; } diff --git a/lib/src/reverse_engineering/responses/video_info_response.dart b/lib/src/reverse_engineering/responses/video_info_response.dart index c888442..0ded1a3 100644 --- a/lib/src/reverse_engineering/responses/video_info_response.dart +++ b/lib/src/reverse_engineering/responses/video_info_response.dart @@ -6,6 +6,7 @@ import '../youtube_http_client.dart'; import 'player_response.dart'; import 'stream_info_provider.dart'; +/// class VideoInfoResponse { final Map _root; @@ -16,14 +17,18 @@ class VideoInfoResponse { Iterable<_StreamInfo> _adaptiveStreams; Iterable<_StreamInfo> _streams; + /// String get status => _status ??= _root['status']; + /// bool get isVideoAvailable => _isVideoAvailable ??= status.toLowerCase() != 'fail'; + /// PlayerResponse get playerResponse => _playerResponse ??= PlayerResponse.parse(_root['player_response']); + /// Iterable<_StreamInfo> get muxedStreams => _muxedStreams ??= _root['url_encoded_fmt_stream_map'] ?.split(',') @@ -31,6 +36,7 @@ class VideoInfoResponse { ?.map((e) => _StreamInfo(e)) ?? const []; + /// Iterable<_StreamInfo> get adaptiveStreams => _adaptiveStreams ??= _root['adaptive_fmts'] ?.split(',') @@ -38,13 +44,17 @@ class VideoInfoResponse { ?.map((e) => _StreamInfo(e)) ?? const []; + /// Iterable<_StreamInfo> get streams => _streams ??= [...muxedStreams, ...adaptiveStreams]; + /// VideoInfoResponse(this._root); + /// VideoInfoResponse.parse(String raw) : _root = Uri.splitQueryString(raw); + /// static Future get( YoutubeHttpClient httpClient, String videoId, [String sts]) { @@ -103,7 +113,7 @@ class _StreamInfo extends StreamInfoProvider { @override int get bitrate => _bitrate ??= int.parse(_root['bitrate']); - MediaType get mimeType => _mimeType ??= MediaType.parse(_root["type"]); + MediaType get mimeType => _mimeType ??= MediaType.parse(_root['type']); @override String get container => _container ??= mimeType.subtype; diff --git a/lib/src/reverse_engineering/responses/watch_page.dart b/lib/src/reverse_engineering/responses/watch_page.dart index 4e260d9..ea8f56a 100644 --- a/lib/src/reverse_engineering/responses/watch_page.dart +++ b/lib/src/reverse_engineering/responses/watch_page.dart @@ -12,6 +12,7 @@ import '../youtube_http_client.dart'; import 'player_response.dart'; import 'stream_info_provider.dart'; +/// class WatchPage { static final RegExp _videoLikeExp = RegExp(r'"label"\s*:\s*"([\d,\.]+) likes"'); @@ -23,13 +24,18 @@ class WatchPage { static final _xsfrTokenExp = RegExp(r'"XSRF_TOKEN"\s*:\s*"(.+?)"'); final Document _root; + + /// final String visitorInfoLive; + + /// final String ysc; _InitialData _initialData; String _xsfrToken; _PlayerConfig _playerConfig; + /// _InitialData get initialData => _initialData ??= _InitialData(json.decode(_matchJson(_extractJson( _root @@ -39,6 +45,7 @@ class WatchPage { .firstWhere((e) => e.contains('window["ytInitialData"] =')), 'window["ytInitialData"] =')))); + /// String get xsfrToken => _xsfrToken ??= _xsfrTokenExp .firstMatch(_root .querySelectorAll('script') @@ -46,11 +53,14 @@ class WatchPage { .text) .group(1); + /// bool get isOk => _root.body.querySelector('#player') != null; + /// bool get isVideoAvailable => _root.querySelector('meta[property="og:url"]') != null; + /// int get videoLikeCount => int.parse(_videoLikeExp .firstMatch(_root.outerHtml) ?.group(1) @@ -63,6 +73,7 @@ class WatchPage { ?.nullIfWhitespace ?? '0'); + /// int get videoDislikeCount => int.parse(_videoDislikeExp .firstMatch(_root.outerHtml) ?.group(1) @@ -75,6 +86,7 @@ class WatchPage { ?.nullIfWhitespace ?? '0'); + /// _PlayerConfig get playerConfig => _playerConfig ??= _PlayerConfig(json.decode(_matchJson(_extractJson( _root.getElementsByTagName('html').first.text, @@ -103,11 +115,14 @@ class WatchPage { return str.substring(0, lastI + 1); } + /// WatchPage(this._root, this.visitorInfoLive, this.ysc); + /// WatchPage.parse(String raw, this.visitorInfoLive, this.ysc) : _root = parser.parse(raw); + /// static Future get(YoutubeHttpClient httpClient, String videoId) { final url = 'https://youtube.com/watch?v=$videoId&bpctr=9999999999&hl=en'; return retry(() async { @@ -119,7 +134,7 @@ class WatchPage { var result = WatchPage.parse(req.body, visitorInfoLive, ysc); if (!result.isOk) { - throw TransientFailureException("Video watch page is broken."); + throw TransientFailureException('Video watch page is broken.'); } if (!result.isVideoAvailable) { diff --git a/lib/src/reverse_engineering/youtube_http_client.dart b/lib/src/reverse_engineering/youtube_http_client.dart index 5bb3ab6..3bc1711 100644 --- a/lib/src/reverse_engineering/youtube_http_client.dart +++ b/lib/src/reverse_engineering/youtube_http_client.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import '../exceptions/exceptions.dart'; import '../videos/streams/streams.dart'; +/// class YoutubeHttpClient extends http.BaseClient { final http.Client _httpClient = http.Client(); @@ -43,6 +44,7 @@ class YoutubeHttpClient extends http.BaseClient { } } + /// Future getString(dynamic url, {Map headers, bool validate = true}) async { var response = await get(url, headers: headers); @@ -64,6 +66,7 @@ class YoutubeHttpClient extends http.BaseClient { return response; } + /// Future postString(dynamic url, {Map body, Map headers, @@ -77,6 +80,7 @@ class YoutubeHttpClient extends http.BaseClient { return response.body; } + /// // TODO: Check why isRateLimited is not working. Stream> getStream(StreamInfo streamInfo, {Map headers, @@ -126,6 +130,7 @@ class YoutubeHttpClient extends http.BaseClient { // } } + /// Future getContentLength(dynamic url, {Map headers, bool validate = true}) async { var response = await head(url, headers: headers); diff --git a/lib/src/videos/closed_captions/closed_caption_client.dart b/lib/src/videos/closed_captions/closed_caption_client.dart index 783f766..b178caa 100644 --- a/lib/src/videos/closed_captions/closed_caption_client.dart +++ b/lib/src/videos/closed_captions/closed_caption_client.dart @@ -46,6 +46,7 @@ class ClosedCaptionClient { return ClosedCaptionTrack(captions); } + /// Future getSrt(ClosedCaptionTrackInfo trackInfo) async { var track = await get(trackInfo); @@ -93,7 +94,7 @@ extension on Duration { } if (inMicroseconds < 0) { - return "-${-this}"; + return '-${-this}'; } var twoDigitHours = twoDigits(inHours); var twoDigitMinutes = @@ -102,6 +103,6 @@ extension on Duration { twoDigits(inSeconds.remainder(Duration.secondsPerMinute)); var fourDigitsUs = threeDigits(inMilliseconds.remainder(1000)); - return "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds,$fourDigitsUs"; + return '$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds,$fourDigitsUs'; } } diff --git a/lib/src/videos/streams/streams_client.dart b/lib/src/videos/streams/streams_client.dart index 5d4a632..9a77821 100644 --- a/lib/src/videos/streams/streams_client.dart +++ b/lib/src/videos/streams/streams_client.dart @@ -128,7 +128,7 @@ class StreamsClient { // Signature var signature = streamInfo.signature; - var signatureParameter = streamInfo.signatureParameter ?? "signature"; + var signatureParameter = streamInfo.signatureParameter ?? 'signature'; if (!signature.isNullOrWhiteSpace) { signature = streamContext.cipherOperations.decipher(signature); @@ -163,7 +163,7 @@ class StreamsClient { var videoWidth = streamInfo.videoWidth; var videoHeight = streamInfo.videoHeight; - var videoResolution = videoWidth != null && videoHeight != null + var videoResolution = videoWidth != -1 && videoHeight != -1 ? VideoResolution(videoWidth, videoHeight) : videoQuality.toVideoResolution(); diff --git a/pubspec.yaml b/pubspec.yaml index 7268ca5..cd45641 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,3 +19,5 @@ dev_dependencies: effective_dart: ^1.2.3 console: ^3.1.0 test: ^1.12.0 + grinder: ^0.8.5 + pedantic: ^1.9.2 diff --git a/test/video_test.dart b/test/video_test.dart index 3157ff1..5effcc1 100644 --- a/test/video_test.dart +++ b/test/video_test.dart @@ -27,7 +27,7 @@ void main() { expect(video.thumbnails.highResUrl, isNotNull); expect(video.thumbnails.standardResUrl, isNotNull); expect(video.thumbnails.maxResUrl, isNotNull); - expect(video.keywords, orderedEquals(["osu", "mouse", "rhythm game"])); + expect(video.keywords, orderedEquals(['osu', 'mouse', 'rhythm game'])); expect(video.engagement.viewCount, greaterThanOrEqualTo(134)); expect(video.engagement.likeCount, greaterThanOrEqualTo(5)); expect(video.engagement.dislikeCount, greaterThanOrEqualTo(0)); diff --git a/tool/analysis_options.yaml b/tool/analysis_options.yaml new file mode 100644 index 0000000..22604fe --- /dev/null +++ b/tool/analysis_options.yaml @@ -0,0 +1,2 @@ +# Analysis options to run test for ginder +include: package:pedantic/analysis_options.yaml diff --git a/tool/grind.dart b/tool/grind.dart new file mode 100644 index 0000000..255546f --- /dev/null +++ b/tool/grind.dart @@ -0,0 +1,24 @@ +import 'package:grinder/grinder.dart'; + + +final pub = sdkBin('pub'); +void main(args) => grind(args); + +@Task('Run tests') +void test() => TestRunner().testAsync(); + +@Task('Dart analysis') +void analysis() { + +} + +@DefaultTask() +@Depends(test) +build() { + Pub.build(); + Pub.upgrade(); + Pub.version() +} + +@Task() +clean() => defaultClean();