Fix all linter info

This commit is contained in:
Mattia 2020-07-16 20:02:54 +02:00
parent e107c60581
commit ab8edbe7bd
27 changed files with 221 additions and 29 deletions

2
.gitignore vendored
View File

@ -13,6 +13,6 @@ doc/api/
.idea/ .idea/
.vscode/ .vscode/
*.iml *.iml
/tool/ /bin/
.flutter-plugins-dependencies .flutter-plugins-dependencies

View File

@ -1,6 +1,7 @@
## 1.4.2 ## 1.4.2
- Implement `getSrt` a video closed captions in srt format. - Implement `getSrt` a video closed captions in srt format.
- Only throw custom exceptions from the library. - Only throw custom exceptions from the library.
- `getUploadsFromPage` no longer throws.
## 1.4.1+1 ## 1.4.1+1
- Bug fixes - Bug fixes

View File

@ -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 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: linter:
rules: rules:
- valid_regexps - valid_regexps
@ -59,6 +54,8 @@ linter:
- use_string_buffers - use_string_buffers
- void_checks - void_checks
- package_names - package_names
- prefer_single_quotes
- use_function_type_syntax_for_parameters
analyzer: analyzer:
exclude: exclude:

View File

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

View File

@ -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 '../extensions/helpers_extension.dart';
import '../playlists/playlists.dart'; import '../playlists/playlists.dart';
import '../reverse_engineering/responses/channel_upload_page.dart';
import '../reverse_engineering/responses/responses.dart'; import '../reverse_engineering/responses/responses.dart';
import '../reverse_engineering/youtube_http_client.dart'; import '../reverse_engineering/youtube_http_client.dart';
import '../videos/video.dart'; import '../videos/video.dart';
import '../videos/video_id.dart'; import '../videos/video_id.dart';
import 'channel.dart'; import 'channel.dart';
import 'channel_id.dart'; import 'channel_id.dart';
import 'channel_video.dart';
import 'username.dart'; import 'username.dart';
import 'video_sorting.dart';
/// Queries related to YouTube channels. /// Queries related to YouTube channels.
class ChannelClient { class ChannelClient {

View File

@ -1,5 +1,5 @@
import 'package:equatable/equatable.dart'; 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) /// Metadata related to a search query result (playlist)
class ChannelVideo with EquatableMixin { class ChannelVideo with EquatableMixin {

View File

@ -6,7 +6,7 @@ import 'exceptions/exceptions.dart';
/// Run the [function] each time an exception is thrown until the retryCount /// Run the [function] each time an exception is thrown until the retryCount
/// is 0. /// is 0.
Future<T> retry<T>(FutureOr<T> function()) async { Future<T> retry<T>(FutureOr<T> Function() function) async {
var retryCount = 5; var retryCount = 5;
// ignore: literal_only_boolean_expressions // ignore: literal_only_boolean_expressions
@ -27,7 +27,6 @@ Future<T> retry<T>(FutureOr<T> function()) async {
/// Get "retry" cost of each YoutubeExplode exception. /// Get "retry" cost of each YoutubeExplode exception.
int getExceptionCost(Exception e) { int getExceptionCost(Exception e) {
if (e is TransientFailureException || e is FormatException) { if (e is TransientFailureException || e is FormatException) {
print('Ripperoni!');
return 1; return 1;
} }
if (e is RequestLimitExceededException) { if (e is RequestLimitExceededException) {

View File

@ -168,8 +168,10 @@ extension VideoQualityUtil on VideoQuality {
label, 'label', 'Unrecognized video quality label'); label, 'label', 'Unrecognized video quality label');
} }
///
String getLabel() => '${toString().stripNonDigits()}p'; String getLabel() => '${toString().stripNonDigits()}p';
///
String getLabelWithFramerate(double framerate) { String getLabelWithFramerate(double framerate) {
// Framerate appears only if it's above 30 // Framerate appears only if it's above 30
if (framerate <= 30) { if (framerate <= 30) {
@ -180,6 +182,7 @@ extension VideoQualityUtil on VideoQuality {
return '${getLabel()}$framerateRounded'; return '${getLabel()}$framerateRounded';
} }
///
static String getLabelFromTagWithFramerate(int itag, double framerate) { static String getLabelFromTagWithFramerate(int itag, double framerate) {
var videoQuality = fromTag(itag); var videoQuality = fromTag(itag);
return videoQuality.getLabelWithFramerate(framerate); return videoQuality.getLabelWithFramerate(framerate);

View File

@ -6,26 +6,35 @@ import '../../extensions/helpers_extension.dart';
import '../../retry.dart'; import '../../retry.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
///
class ChannelPage { class ChannelPage {
final Document _root; final Document _root;
///
bool get isOk => _root.querySelector('meta[property="og:url"]') != null; bool get isOk => _root.querySelector('meta[property="og:url"]') != null;
///
String get channelUrl => String get channelUrl =>
_root.querySelector('meta[property="og:url"]')?.attributes['content']; _root.querySelector('meta[property="og:url"]')?.attributes['content'];
///
String get channelId => channelUrl.substringAfter('channel/'); String get channelId => channelUrl.substringAfter('channel/');
///
String get channelTitle => String get channelTitle =>
_root.querySelector('meta[property="og:title"]')?.attributes['content']; _root.querySelector('meta[property="og:title"]')?.attributes['content'];
///
String get channelLogoUrl => String get channelLogoUrl =>
_root.querySelector('meta[property="og:image"]')?.attributes['content']; _root.querySelector('meta[property="og:image"]')?.attributes['content'];
///
ChannelPage(this._root); ChannelPage(this._root);
///
ChannelPage.parse(String raw) : _root = parser.parse(raw); ChannelPage.parse(String raw) : _root = parser.parse(raw);
///
static Future<ChannelPage> get(YoutubeHttpClient httpClient, String id) { static Future<ChannelPage> get(YoutubeHttpClient httpClient, String id) {
var url = 'https://www.youtube.com/channel/$id?hl=en'; var url = 'https://www.youtube.com/channel/$id?hl=en';
@ -40,6 +49,7 @@ class ChannelPage {
}); });
} }
///
static Future<ChannelPage> getByUsername( static Future<ChannelPage> getByUsername(
YoutubeHttpClient httpClient, String username) { YoutubeHttpClient httpClient, String username) {
var url = 'https://www.youtube.com/user/$username?hl=en'; var url = 'https://www.youtube.com/user/$username?hl=en';

View File

@ -2,20 +2,23 @@ import 'dart:convert';
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 'package:youtube_explode_dart/src/exceptions/exceptions.dart';
import '../../channels/channel_video.dart'; import '../../channels/channel_video.dart';
import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.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';
///
class ChannelUploadPage { class ChannelUploadPage {
///
final String channelId; final String channelId;
final Document _root; final Document _root;
_InitialData _initialData; _InitialData _initialData;
///
_InitialData get initialData => _InitialData get initialData =>
_initialData ??= _InitialData(json.decode(_matchJson(_extractJson( _initialData ??= _InitialData(json.decode(_matchJson(_extractJson(
_root _root
@ -48,9 +51,11 @@ class ChannelUploadPage {
return str.substring(0, lastI + 1); return str.substring(0, lastI + 1);
} }
///
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);
@ -64,6 +69,7 @@ class ChannelUploadPage {
}); });
} }
///
static Future<ChannelUploadPage> get( static Future<ChannelUploadPage> get(
YoutubeHttpClient httpClient, String channelId, String sorting) { YoutubeHttpClient httpClient, String channelId, String sorting) {
assert(sorting != null); assert(sorting != null);
@ -75,6 +81,7 @@ class ChannelUploadPage {
}); });
} }
///
ChannelUploadPage.parse(String raw, this.channelId) ChannelUploadPage.parse(String raw, this.channelId)
: _root = parser.parse(raw); : _root = parser.parse(raw);
} }

View File

@ -3,18 +3,24 @@ import 'package:xml/xml.dart' as xml;
import '../../retry.dart'; import '../../retry.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
///
class ClosedCaptionTrackResponse { class ClosedCaptionTrackResponse {
final xml.XmlDocument _root; final xml.XmlDocument _root;
Iterable<ClosedCaption> _closedCaptions; Iterable<ClosedCaption> _closedCaptions;
///
Iterable<ClosedCaption> get closedCaptions => _closedCaptions ??= Iterable<ClosedCaption> get closedCaptions => _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
ClosedCaptionTrackResponse.parse(String raw) : _root = xml.parse(raw); ClosedCaptionTrackResponse.parse(String raw) : _root = xml.parse(raw);
///
static Future<ClosedCaptionTrackResponse> get( static Future<ClosedCaptionTrackResponse> get(
YoutubeHttpClient httpClient, String url) { YoutubeHttpClient httpClient, String url) {
var formatUrl = _setQueryParameters(url, {'format': '3'}); var formatUrl = _setQueryParameters(url, {'format': '3'});
@ -34,6 +40,7 @@ class ClosedCaptionTrackResponse {
} }
} }
///
class ClosedCaption { class ClosedCaption {
final xml.XmlElement _root; final xml.XmlElement _root;
@ -42,29 +49,37 @@ class ClosedCaption {
Duration _end; Duration _end;
Iterable<ClosedCaptionPart> _parts; Iterable<ClosedCaptionPart> _parts;
///
String get text => _root.text; String get text => _root.text;
///
Duration get offset => _offset ??= Duration get offset => _offset ??=
Duration(milliseconds: int.parse(_root.getAttribute('t') ?? 0)); Duration(milliseconds: int.parse(_root.getAttribute('t') ?? 0));
///
Duration get duration => _duration ??= Duration get duration => _duration ??=
Duration(milliseconds: int.parse(_root.getAttribute('d') ?? 0)); Duration(milliseconds: int.parse(_root.getAttribute('d') ?? 0));
///
Duration get end => _end ??= offset + duration; Duration get end => _end ??= offset + duration;
///
Iterable<ClosedCaptionPart> getParts() => Iterable<ClosedCaptionPart> getParts() =>
_parts ??= _root.findAllElements('s').map((e) => ClosedCaptionPart._(e)); _parts ??= _root.findAllElements('s').map((e) => ClosedCaptionPart._(e));
ClosedCaption._(this._root); ClosedCaption._(this._root);
} }
///
class ClosedCaptionPart { class ClosedCaptionPart {
final xml.XmlElement _root; final xml.XmlElement _root;
Duration _offset; Duration _offset;
///
String get text => _root.text; String get text => _root.text;
///
Duration get offset => _offset ??= Duration get offset => _offset ??=
Duration(milliseconds: int.parse(_root.getAttribute('t') ?? '0')); Duration(milliseconds: int.parse(_root.getAttribute('t') ?? '0'));

View File

@ -4,12 +4,15 @@ import '../../retry.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
import 'stream_info_provider.dart'; import 'stream_info_provider.dart';
///
class DashManifest { 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> _streams;
///
Iterable<_StreamInfo> get streams => _streams ??= _root Iterable<_StreamInfo> get streams => _streams ??= _root
.findElements('Representation') .findElements('Representation')
.where((e) => e .where((e) => e
@ -19,11 +22,14 @@ class DashManifest {
.contains('sq/')) .contains('sq/'))
.map((e) => _StreamInfo(e)); .map((e) => _StreamInfo(e));
///
DashManifest(this._root); DashManifest(this._root);
///
// ignore: deprecated_member_use // ignore: deprecated_member_use
DashManifest.parse(String raw) : _root = xml.parse(raw); DashManifest.parse(String raw) : _root = xml.parse(raw);
///
static Future<DashManifest> get(YoutubeHttpClient httpClient, dynamic url) { static Future<DashManifest> get(YoutubeHttpClient httpClient, dynamic url) {
return retry(() async { return retry(() async {
var raw = await httpClient.getString(url); var raw = await httpClient.getString(url);
@ -31,6 +37,7 @@ class DashManifest {
}); });
} }
///
static String getSignatureFromUrl(String url) => static String getSignatureFromUrl(String url) =>
_urlSignatureExp.firstMatch(url)?.group(1); _urlSignatureExp.firstMatch(url)?.group(1);
} }

View File

@ -7,6 +7,7 @@ import '../../extensions/helpers_extension.dart';
import '../../retry.dart'; import '../../retry.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
///
class EmbedPage { class EmbedPage {
static final _playerConfigExp = static final _playerConfigExp =
RegExp(r"yt\.setConfig\({'PLAYER_CONFIG':(.*)}\);"); RegExp(r"yt\.setConfig\({'PLAYER_CONFIG':(.*)}\);");
@ -15,6 +16,7 @@ class EmbedPage {
_PlayerConfig _playerConfig; _PlayerConfig _playerConfig;
String __playerConfigJson; String __playerConfigJson;
///
_PlayerConfig get playerconfig { _PlayerConfig get playerconfig {
if (_playerConfig != null) { if (_playerConfig != null) {
return _playerConfig; return _playerConfig;
@ -32,10 +34,13 @@ class EmbedPage {
.map((e) => _playerConfigExp.firstMatch(e)?.group(1)) .map((e) => _playerConfigExp.firstMatch(e)?.group(1))
.firstWhere((e) => !e.isNullOrWhiteSpace, orElse: () => null); .firstWhere((e) => !e.isNullOrWhiteSpace, orElse: () => null);
///
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) {
var url = 'https://youtube.com/embed/$videoId?hl=en'; var url = 'https://youtube.com/embed/$videoId?hl=en';
return retry(() async { return retry(() async {

View File

@ -5,6 +5,7 @@ import 'package:http_parser/http_parser.dart';
import '../../extensions/helpers_extension.dart'; import '../../extensions/helpers_extension.dart';
import 'stream_info_provider.dart'; import 'stream_info_provider.dart';
///
class PlayerResponse { class PlayerResponse {
// Json parsed map // Json parsed map
final Map<String, dynamic> _root; final Map<String, dynamic> _root;
@ -15,31 +16,43 @@ class PlayerResponse {
Iterable<ClosedCaptionTrack> _closedCaptionTrack; Iterable<ClosedCaptionTrack> _closedCaptionTrack;
String _videoPlayabilityError; String _videoPlayabilityError;
///
String get playabilityStatus => _root['playabilityStatus']['status']; String get playabilityStatus => _root['playabilityStatus']['status'];
///
bool get isVideoAvailable => playabilityStatus.toLowerCase() != 'error'; bool get isVideoAvailable => playabilityStatus.toLowerCase() != 'error';
///
bool get isVideoPlayable => playabilityStatus.toLowerCase() == 'ok'; bool get isVideoPlayable => playabilityStatus.toLowerCase() == 'ok';
///
String get videoTitle => _root['videoDetails']['title']; String get videoTitle => _root['videoDetails']['title'];
///
String get videoAuthor => _root['videoDetails']['author']; String get videoAuthor => _root['videoDetails']['author'];
///
DateTime get videoUploadDate => DateTime.parse( DateTime get videoUploadDate => DateTime.parse(
_root['microformat']['playerMicroformatRenderer']['uploadDate']); _root['microformat']['playerMicroformatRenderer']['uploadDate']);
///
String get videoChannelId => _root['videoDetails']['channelId']; String get videoChannelId => _root['videoDetails']['channelId'];
///
Duration get videoDuration => Duration get videoDuration =>
Duration(seconds: int.parse(_root['videoDetails']['lengthSeconds'])); Duration(seconds: int.parse(_root['videoDetails']['lengthSeconds']));
///
Iterable<String> get videoKeywords => Iterable<String> get videoKeywords =>
_root['videoDetails']['keywords']?.cast<String>() ?? const []; _root['videoDetails']['keywords']?.cast<String>() ?? const [];
///
String get videoDescription => _root['videoDetails']['shortDescription']; String get videoDescription => _root['videoDetails']['shortDescription'];
///
int get videoViewCount => int.parse(_root['videoDetails']['viewCount']); int get videoViewCount => int.parse(_root['videoDetails']['viewCount']);
///
// Can be null // Can be null
String get previewVideoId => String get previewVideoId =>
_root _root
@ -55,16 +68,20 @@ class PlayerResponse {
?.getValue('playerVars') ?? ?.getValue('playerVars') ??
'')['video_id']; '')['video_id'];
///
bool get isLive => _root.get('videoDetails')?.getValue('isLive') ?? false; bool get isLive => _root.get('videoDetails')?.getValue('isLive') ?? false;
///
// Can be null // Can be null
String get hlsManifestUrl => String get hlsManifestUrl =>
_root.get('streamingData')?.getValue('hlsManifestUrl'); _root.get('streamingData')?.getValue('hlsManifestUrl');
///
// Can be null // Can be null
String get dashManifestUrl => String get dashManifestUrl =>
_root.get('streamingData')?.getValue('dashManifestUrl'); _root.get('streamingData')?.getValue('dashManifestUrl');
///
Iterable<StreamInfoProvider> get muxedStreams => _muxedStreams ??= _root Iterable<StreamInfoProvider> get muxedStreams => _muxedStreams ??= _root
?.get('streamingData') ?.get('streamingData')
?.getValue('formats') ?.getValue('formats')
@ -72,6 +89,7 @@ class PlayerResponse {
?.cast<StreamInfoProvider>() ?? ?.cast<StreamInfoProvider>() ??
const <StreamInfoProvider>[]; const <StreamInfoProvider>[];
///
Iterable<StreamInfoProvider> get adaptiveStreams => _adaptiveStreams ??= _root Iterable<StreamInfoProvider> get adaptiveStreams => _adaptiveStreams ??= _root
?.get('streamingData') ?.get('streamingData')
?.getValue('adaptiveFormats') ?.getValue('adaptiveFormats')
@ -79,9 +97,11 @@ class PlayerResponse {
?.cast<StreamInfoProvider>() ?? ?.cast<StreamInfoProvider>() ??
const <StreamInfoProvider>[]; const <StreamInfoProvider>[];
///
List<StreamInfoProvider> get streams => List<StreamInfoProvider> get streams =>
_streams ??= [...muxedStreams, ...adaptiveStreams]; _streams ??= [...muxedStreams, ...adaptiveStreams];
///
Iterable<ClosedCaptionTrack> get closedCaptionTrack => Iterable<ClosedCaptionTrack> get closedCaptionTrack =>
_closedCaptionTrack ??= _root _closedCaptionTrack ??= _root
.get('captions') .get('captions')
@ -91,26 +111,35 @@ class PlayerResponse {
?.cast<ClosedCaptionTrack>() ?? ?.cast<ClosedCaptionTrack>() ??
const []; const [];
///
PlayerResponse(this._root); PlayerResponse(this._root);
///
String getVideoPlayabilityError() => _videoPlayabilityError ??= String getVideoPlayabilityError() => _videoPlayabilityError ??=
_root.get('playabilityStatus')?.getValue('reason'); _root.get('playabilityStatus')?.getValue('reason');
///
PlayerResponse.parse(String raw) : _root = json.decode(raw); PlayerResponse.parse(String raw) : _root = json.decode(raw);
} }
///
class ClosedCaptionTrack { class ClosedCaptionTrack {
// Json parsed map // Json parsed map
final Map<String, dynamic> _root; final Map<String, dynamic> _root;
///
String get url => _root['baseUrl']; String get url => _root['baseUrl'];
///
String get languageCode => _root['languageCode']; String get languageCode => _root['languageCode'];
///
String get languageName => _root['name']['simpleText']; 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); ClosedCaptionTrack(this._root);
} }

View File

@ -5,6 +5,7 @@ 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+)\)'); final RegExp _statIndexExp = RegExp(r'\(\w+,(\d+)\)');
@ -20,6 +21,7 @@ class PlayerSource {
String _sts; String _sts;
String _deciphererDefinitionBody; String _deciphererDefinitionBody;
///
String get sts { String get sts {
if (_sts != null) { if (_sts != null) {
return _sts; return _sts;
@ -33,6 +35,7 @@ class PlayerSource {
return _sts ??= val; return _sts ??= val;
} }
///
Iterable<CipherOperation> getCiperOperations() sync* { Iterable<CipherOperation> getCiperOperations() sync* {
var funcBody = _getDeciphererFuncBody(); var funcBody = _getDeciphererFuncBody();
@ -102,11 +105,14 @@ class PlayerSource {
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(
YoutubeHttpClient httpClient, String url) async { YoutubeHttpClient httpClient, String url) async {
if (_cache[url] == null) { if (_cache[url] == null) {

View File

@ -7,37 +7,49 @@ import '../../extensions/helpers_extension.dart';
import '../../retry.dart'; import '../../retry.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
///
class PlaylistResponse { class PlaylistResponse {
Iterable<_Video> _videos; Iterable<_Video> _videos;
// Json parsed map // Json parsed map
final Map<String, dynamic> _root; final Map<String, dynamic> _root;
///
String get title => _root['title']; String get title => _root['title'];
///
String get author => _root['author']; String get author => _root['author'];
///
String get description => _root['description']; String get description => _root['description'];
///
ThumbnailSet get thumbnails => ThumbnailSet(videos.firstOrNull.id); ThumbnailSet get thumbnails => ThumbnailSet(videos.firstOrNull.id);
///
int get viewCount => _root['views']; int get viewCount => _root['views'];
///
int get likeCount => _root['likes']; int get likeCount => _root['likes'];
///
int get dislikeCount => _root['dislikes']; int get dislikeCount => _root['dislikes'];
///
Iterable<_Video> get videos => _videos ??= Iterable<_Video> get videos => _videos ??=
_root['video']?.map((e) => _Video(e))?.cast<_Video>() ?? const <_Video>[]; _root['video']?.map((e) => _Video(e))?.cast<_Video>() ?? const <_Video>[];
///
PlaylistResponse(this._root); PlaylistResponse(this._root);
///
PlaylistResponse.parse(String raw) : _root = json.tryDecode(raw) { PlaylistResponse.parse(String raw) : _root = json.tryDecode(raw) {
if (_root == null) { if (_root == null) {
throw TransientFailureException('Playerlist response is broken.'); throw TransientFailureException('Playerlist response is broken.');
} }
} }
///
static Future<PlaylistResponse> get(YoutubeHttpClient httpClient, String id, static Future<PlaylistResponse> get(YoutubeHttpClient httpClient, String id,
{int index = 0}) { {int index = 0}) {
var url = var url =
@ -48,6 +60,7 @@ class PlaylistResponse {
}); });
} }
///
static Future<PlaylistResponse> searchResults( static Future<PlaylistResponse> searchResults(
YoutubeHttpClient httpClient, String query, YoutubeHttpClient httpClient, String query,
{int page = 0}) { {int page = 0}) {

View File

@ -13,15 +13,18 @@ import '../../search/search_video.dart';
import '../../videos/videos.dart'; import '../../videos/videos.dart';
import '../youtube_http_client.dart'; import '../youtube_http_client.dart';
///
class SearchPage { class SearchPage {
static final _xsfrTokenExp = RegExp('"XSRF_TOKEN":"(.+?)"'); static final _xsfrTokenExp = RegExp('"XSRF_TOKEN":"(.+?)"');
///
final String queryString; final String queryString;
final Document _root; final Document _root;
_InitialData _initialData; _InitialData _initialData;
String _xsrfToken; String _xsrfToken;
///
_InitialData get initialData => _InitialData get initialData =>
_initialData ??= _InitialData(json.decode(_matchJson(_extractJson( _initialData ??= _InitialData(json.decode(_matchJson(_extractJson(
_root _root
@ -31,6 +34,7 @@ class SearchPage {
.firstWhere((e) => e.contains('window["ytInitialData"] =')), .firstWhere((e) => e.contains('window["ytInitialData"] =')),
'window["ytInitialData"] =')))); 'window["ytInitialData"] ='))));
///
String get xsfrToken => _xsrfToken ??= _xsfrTokenExp String get xsfrToken => _xsrfToken ??= _xsfrTokenExp
.firstMatch(_root .firstMatch(_root
.querySelectorAll('script') .querySelectorAll('script')
@ -61,13 +65,15 @@ class SearchPage {
return str.substring(0, lastI + 1); return str.substring(0, lastI + 1);
} }
///
SearchPage(this._root, this.queryString, SearchPage(this._root, this.queryString,
[_InitialData initalData, String xsfrToken]) [_InitialData initalData, String xsfrToken])
: _initialData = initalData, : _initialData = initalData,
_xsrfToken = xsfrToken; _xsrfToken = xsfrToken;
///
// TODO: Replace this in favour of async* when quering; // TODO: Replace this in favour of async* when quering;
Future<SearchPage> nextPage(YoutubeHttpClient httpClient) { Future<SearchPage> nextPage(YoutubeHttpClient httpClient) async {
if (initialData.continuation == '') { if (initialData.continuation == '') {
return null; return null;
} }
@ -77,6 +83,7 @@ class SearchPage {
xsrfToken: xsfrToken); xsrfToken: xsfrToken);
} }
///
static Future<SearchPage> get( static Future<SearchPage> get(
YoutubeHttpClient httpClient, String queryString, YoutubeHttpClient httpClient, String queryString,
{String ctoken, String itct, String xsrfToken}) { {String ctoken, String itct, String xsrfToken}) {
@ -103,6 +110,7 @@ class SearchPage {
}); });
} }
///
SearchPage.parse(String raw, this.queryString) : _root = parser.parse(raw); SearchPage.parse(String raw, this.queryString) : _root = parser.parse(raw);
} }
@ -229,7 +237,10 @@ class _InitialData {
runs?.getValue('runs')?.map((e) => e['text'])?.join() ?? ''; 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 // ['contents'] -> @See ContentsList
// ['continuations'] -> Data to see more // ['continuations'] -> Data to see more
@ -237,10 +248,12 @@ class _InitialData {
// Key -> 'videoRenderer' // Key -> 'videoRenderer'
// videoId --> VideoId // videoId --> VideoId
// title['runs'].loop -> ['text'] -> concatenate --> "Video Title" // 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" // ownerText['runs'].first -> ['text'] --> "Video Author"
// lengthText['simpleText'] -> Parse format H:M:S -> "Video Duration" // 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' // Key -> 'radioRenderer'
// playlistId -> PlaylistId // playlistId -> PlaylistId
@ -248,8 +261,10 @@ class _InitialData {
// //
// Key -> 'horizontalCardListRenderer' // Queries related to this search // Key -> 'horizontalCardListRenderer' // Queries related to this search
// cards --> List of Maps -> loop -> ['searchRefinementCardRenderer'].first // cards --> List of Maps -> loop -> ['searchRefinementCardRenderer'].first
// thumbnail -> ['thumbnails'].first -> ['url'] --> "Thumbnail url" -> Find video id from id. // thumbnail -> ['thumbnails'].first -> ['url']
// searchEndpoint -> ['searchEndpoint'] -> ['query'] -> "Related query string" // --> "Thumbnail url" -> Find video id from id.
// searchEndpoint -> ['searchEndpoint']
// -> ['query'] -> "Related query string"
// //
// Key -> 'shelfRenderer' // Videos related to this search // Key -> 'shelfRenderer' // Videos related to this search
// contents -> ['verticalListRenderer']['items'] -> loop -> parseContent // contents -> ['verticalListRenderer']['items'] -> loop -> parseContent

View File

@ -1,38 +1,66 @@
///
abstract class StreamInfoProvider { abstract class StreamInfoProvider {
///
static final RegExp contentLenExp = RegExp(r'clen=(\d+)'); static final RegExp contentLenExp = RegExp(r'clen=(\d+)');
///
int get tag; int get tag;
///
String get url; String get url;
///
// Can be null // Can be null
// ignore: avoid_returning_null
String get signature => null; String get signature => null;
///
// Can be null // Can be null
// ignore: avoid_returning_null
String get signatureParameter => null; String get signatureParameter => null;
///
// Can be null // Can be null
// ignore: avoid_returning_null
int get contentLength => null; int get contentLength => null;
///
// Can be null
// ignore: avoid_returning_null
int get bitrate; int get bitrate;
///
// Can be null
// ignore: avoid_returning_null
String get container; String get container;
///
// Can be null // Can be null
// ignore: avoid_returning_null
String get audioCodec => null; String get audioCodec => null;
///
// Can be null // Can be null
// ignore: avoid_returning_null
String get videoCodec => null; String get videoCodec => null;
///
// Can be null // Can be null
// ignore: avoid_returning_null
String get videoQualityLabel => null; String get videoQualityLabel => null;
///
// Can be null // Can be null
// ignore: avoid_returning_null
int get videoWidth => null; int get videoWidth => null;
///
// Can be null // Can be null
// ignore: avoid_returning_null
int get videoHeight => null; int get videoHeight => null;
///
// Can be null // Can be null
// ignore: avoid_returning_null
int get framerate => null; int get framerate => null;
} }

View File

@ -6,6 +6,7 @@ import '../youtube_http_client.dart';
import 'player_response.dart'; import 'player_response.dart';
import 'stream_info_provider.dart'; import 'stream_info_provider.dart';
///
class VideoInfoResponse { class VideoInfoResponse {
final Map<String, String> _root; final Map<String, String> _root;
@ -16,14 +17,18 @@ class VideoInfoResponse {
Iterable<_StreamInfo> _adaptiveStreams; Iterable<_StreamInfo> _adaptiveStreams;
Iterable<_StreamInfo> _streams; Iterable<_StreamInfo> _streams;
///
String get status => _status ??= _root['status']; String get status => _status ??= _root['status'];
///
bool get isVideoAvailable => bool get isVideoAvailable =>
_isVideoAvailable ??= status.toLowerCase() != 'fail'; _isVideoAvailable ??= status.toLowerCase() != 'fail';
///
PlayerResponse get playerResponse => PlayerResponse get playerResponse =>
_playerResponse ??= PlayerResponse.parse(_root['player_response']); _playerResponse ??= PlayerResponse.parse(_root['player_response']);
///
Iterable<_StreamInfo> get muxedStreams => Iterable<_StreamInfo> get muxedStreams =>
_muxedStreams ??= _root['url_encoded_fmt_stream_map'] _muxedStreams ??= _root['url_encoded_fmt_stream_map']
?.split(',') ?.split(',')
@ -31,6 +36,7 @@ class VideoInfoResponse {
?.map((e) => _StreamInfo(e)) ?? ?.map((e) => _StreamInfo(e)) ??
const []; const [];
///
Iterable<_StreamInfo> get adaptiveStreams => Iterable<_StreamInfo> get adaptiveStreams =>
_adaptiveStreams ??= _root['adaptive_fmts'] _adaptiveStreams ??= _root['adaptive_fmts']
?.split(',') ?.split(',')
@ -38,13 +44,17 @@ class VideoInfoResponse {
?.map((e) => _StreamInfo(e)) ?? ?.map((e) => _StreamInfo(e)) ??
const []; const [];
///
Iterable<_StreamInfo> get streams => Iterable<_StreamInfo> get streams =>
_streams ??= [...muxedStreams, ...adaptiveStreams]; _streams ??= [...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]) {
@ -103,7 +113,7 @@ class _StreamInfo extends StreamInfoProvider {
@override @override
int get bitrate => _bitrate ??= int.parse(_root['bitrate']); 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 @override
String get container => _container ??= mimeType.subtype; String get container => _container ??= mimeType.subtype;

View File

@ -12,6 +12,7 @@ import '../youtube_http_client.dart';
import 'player_response.dart'; import 'player_response.dart';
import 'stream_info_provider.dart'; import 'stream_info_provider.dart';
///
class WatchPage { class WatchPage {
static final RegExp _videoLikeExp = static final RegExp _videoLikeExp =
RegExp(r'"label"\s*:\s*"([\d,\.]+) likes"'); RegExp(r'"label"\s*:\s*"([\d,\.]+) likes"');
@ -23,13 +24,18 @@ 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;
///
final String ysc; final String ysc;
_InitialData _initialData; _InitialData _initialData;
String _xsfrToken; String _xsfrToken;
_PlayerConfig _playerConfig; _PlayerConfig _playerConfig;
///
_InitialData get initialData => _InitialData get initialData =>
_initialData ??= _InitialData(json.decode(_matchJson(_extractJson( _initialData ??= _InitialData(json.decode(_matchJson(_extractJson(
_root _root
@ -39,6 +45,7 @@ class WatchPage {
.firstWhere((e) => e.contains('window["ytInitialData"] =')), .firstWhere((e) => e.contains('window["ytInitialData"] =')),
'window["ytInitialData"] =')))); 'window["ytInitialData"] ='))));
///
String get xsfrToken => _xsfrToken ??= _xsfrTokenExp String get xsfrToken => _xsfrToken ??= _xsfrTokenExp
.firstMatch(_root .firstMatch(_root
.querySelectorAll('script') .querySelectorAll('script')
@ -46,11 +53,14 @@ class WatchPage {
.text) .text)
.group(1); .group(1);
///
bool get isOk => _root.body.querySelector('#player') != null; 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)
@ -63,6 +73,7 @@ class WatchPage {
?.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)
@ -75,6 +86,7 @@ class WatchPage {
?.nullIfWhitespace ?? ?.nullIfWhitespace ??
'0'); '0');
///
_PlayerConfig get playerConfig => _PlayerConfig get playerConfig =>
_playerConfig ??= _PlayerConfig(json.decode(_matchJson(_extractJson( _playerConfig ??= _PlayerConfig(json.decode(_matchJson(_extractJson(
_root.getElementsByTagName('html').first.text, _root.getElementsByTagName('html').first.text,
@ -103,11 +115,14 @@ class WatchPage {
return str.substring(0, lastI + 1); return str.substring(0, lastI + 1);
} }
///
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) {
final url = 'https://youtube.com/watch?v=$videoId&bpctr=9999999999&hl=en'; final url = 'https://youtube.com/watch?v=$videoId&bpctr=9999999999&hl=en';
return retry(() async { return retry(() async {
@ -119,7 +134,7 @@ class WatchPage {
var result = WatchPage.parse(req.body, visitorInfoLive, ysc); var result = WatchPage.parse(req.body, visitorInfoLive, ysc);
if (!result.isOk) { if (!result.isOk) {
throw TransientFailureException("Video watch page is broken."); throw TransientFailureException('Video watch page is broken.');
} }
if (!result.isVideoAvailable) { if (!result.isVideoAvailable) {

View File

@ -5,6 +5,7 @@ import 'package:http/http.dart' as http;
import '../exceptions/exceptions.dart'; import '../exceptions/exceptions.dart';
import '../videos/streams/streams.dart'; import '../videos/streams/streams.dart';
///
class YoutubeHttpClient extends http.BaseClient { class YoutubeHttpClient extends http.BaseClient {
final http.Client _httpClient = http.Client(); final http.Client _httpClient = http.Client();
@ -43,6 +44,7 @@ class YoutubeHttpClient extends http.BaseClient {
} }
} }
///
Future<String> getString(dynamic url, Future<String> getString(dynamic url,
{Map<String, String> headers, bool validate = true}) async { {Map<String, String> headers, bool validate = true}) async {
var response = await get(url, headers: headers); var response = await get(url, headers: headers);
@ -64,6 +66,7 @@ class YoutubeHttpClient extends http.BaseClient {
return response; return response;
} }
///
Future<String> postString(dynamic url, Future<String> postString(dynamic url,
{Map<String, String> body, {Map<String, String> body,
Map<String, String> headers, Map<String, String> headers,
@ -77,6 +80,7 @@ class YoutubeHttpClient extends http.BaseClient {
return response.body; return response.body;
} }
///
// TODO: Check why isRateLimited is not working. // TODO: Check why isRateLimited is not working.
Stream<List<int>> getStream(StreamInfo streamInfo, Stream<List<int>> getStream(StreamInfo streamInfo,
{Map<String, String> headers, {Map<String, String> headers,
@ -126,6 +130,7 @@ class YoutubeHttpClient extends http.BaseClient {
// } // }
} }
///
Future<int> getContentLength(dynamic url, Future<int> getContentLength(dynamic url,
{Map<String, String> headers, bool validate = true}) async { {Map<String, String> headers, bool validate = true}) async {
var response = await head(url, headers: headers); var response = await head(url, headers: headers);

View File

@ -46,6 +46,7 @@ class ClosedCaptionClient {
return ClosedCaptionTrack(captions); return ClosedCaptionTrack(captions);
} }
///
Future<String> getSrt(ClosedCaptionTrackInfo trackInfo) async { Future<String> getSrt(ClosedCaptionTrackInfo trackInfo) async {
var track = await get(trackInfo); var track = await get(trackInfo);
@ -93,7 +94,7 @@ extension on Duration {
} }
if (inMicroseconds < 0) { if (inMicroseconds < 0) {
return "-${-this}"; return '-${-this}';
} }
var twoDigitHours = twoDigits(inHours); var twoDigitHours = twoDigits(inHours);
var twoDigitMinutes = var twoDigitMinutes =
@ -102,6 +103,6 @@ extension on Duration {
twoDigits(inSeconds.remainder(Duration.secondsPerMinute)); twoDigits(inSeconds.remainder(Duration.secondsPerMinute));
var fourDigitsUs = var fourDigitsUs =
threeDigits(inMilliseconds.remainder(1000)); threeDigits(inMilliseconds.remainder(1000));
return "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds,$fourDigitsUs"; return '$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds,$fourDigitsUs';
} }
} }

View File

@ -128,7 +128,7 @@ class StreamsClient {
// Signature // Signature
var signature = streamInfo.signature; var signature = streamInfo.signature;
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);
@ -163,7 +163,7 @@ class StreamsClient {
var videoWidth = streamInfo.videoWidth; var videoWidth = streamInfo.videoWidth;
var videoHeight = streamInfo.videoHeight; var videoHeight = streamInfo.videoHeight;
var videoResolution = videoWidth != null && videoHeight != null var videoResolution = videoWidth != -1 && videoHeight != -1
? VideoResolution(videoWidth, videoHeight) ? VideoResolution(videoWidth, videoHeight)
: videoQuality.toVideoResolution(); : videoQuality.toVideoResolution();

View File

@ -19,3 +19,5 @@ dev_dependencies:
effective_dart: ^1.2.3 effective_dart: ^1.2.3
console: ^3.1.0 console: ^3.1.0
test: ^1.12.0 test: ^1.12.0
grinder: ^0.8.5
pedantic: ^1.9.2

View File

@ -27,7 +27,7 @@ void main() {
expect(video.thumbnails.highResUrl, isNotNull); expect(video.thumbnails.highResUrl, isNotNull);
expect(video.thumbnails.standardResUrl, isNotNull); expect(video.thumbnails.standardResUrl, isNotNull);
expect(video.thumbnails.maxResUrl, 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.viewCount, greaterThanOrEqualTo(134));
expect(video.engagement.likeCount, greaterThanOrEqualTo(5)); expect(video.engagement.likeCount, greaterThanOrEqualTo(5));
expect(video.engagement.dislikeCount, greaterThanOrEqualTo(0)); expect(video.engagement.dislikeCount, greaterThanOrEqualTo(0));

View File

@ -0,0 +1,2 @@
# Analysis options to run test for ginder
include: package:pedantic/analysis_options.yaml

24
tool/grind.dart Normal file
View File

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