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/
.vscode/
*.iml
/tool/
/bin/
.flutter-plugins-dependencies

View File

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

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
# 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:

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 '../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 {

View File

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

View File

@ -6,7 +6,7 @@ import 'exceptions/exceptions.dart';
/// Run the [function] each time an exception is thrown until the retryCount
/// is 0.
Future<T> retry<T>(FutureOr<T> function()) async {
Future<T> retry<T>(FutureOr<T> Function() function) async {
var retryCount = 5;
// ignore: literal_only_boolean_expressions
@ -27,7 +27,6 @@ Future<T> retry<T>(FutureOr<T> 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) {

View File

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

View File

@ -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<ChannelPage> get(YoutubeHttpClient httpClient, String id) {
var url = 'https://www.youtube.com/channel/$id?hl=en';
@ -40,6 +49,7 @@ class ChannelPage {
});
}
///
static Future<ChannelPage> getByUsername(
YoutubeHttpClient httpClient, String username) {
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/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<ChannelUploadPage> nextPage(YoutubeHttpClient httpClient) {
if (initialData.continuation.isEmpty) {
return Future.value(null);
@ -64,6 +69,7 @@ class ChannelUploadPage {
});
}
///
static Future<ChannelUploadPage> 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);
}

View File

@ -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<ClosedCaption> _closedCaptions;
///
Iterable<ClosedCaption> 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<ClosedCaptionTrackResponse> 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<ClosedCaptionPart> _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<ClosedCaptionPart> 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'));

View File

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

View File

@ -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<EmbedPage> get(YoutubeHttpClient httpClient, String videoId) {
var url = 'https://youtube.com/embed/$videoId?hl=en';
return retry(() async {

View File

@ -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<String, dynamic> _root;
@ -15,31 +16,43 @@ class PlayerResponse {
Iterable<ClosedCaptionTrack> _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<String> get videoKeywords =>
_root['videoDetails']['keywords']?.cast<String>() ?? 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<StreamInfoProvider> get muxedStreams => _muxedStreams ??= _root
?.get('streamingData')
?.getValue('formats')
@ -72,6 +89,7 @@ class PlayerResponse {
?.cast<StreamInfoProvider>() ??
const <StreamInfoProvider>[];
///
Iterable<StreamInfoProvider> get adaptiveStreams => _adaptiveStreams ??= _root
?.get('streamingData')
?.getValue('adaptiveFormats')
@ -79,9 +97,11 @@ class PlayerResponse {
?.cast<StreamInfoProvider>() ??
const <StreamInfoProvider>[];
///
List<StreamInfoProvider> get streams =>
_streams ??= [...muxedStreams, ...adaptiveStreams];
///
Iterable<ClosedCaptionTrack> get closedCaptionTrack =>
_closedCaptionTrack ??= _root
.get('captions')
@ -91,26 +111,35 @@ class PlayerResponse {
?.cast<ClosedCaptionTrack>() ??
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<String, dynamic> _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);
}

View File

@ -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<CipherOperation> 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<PlayerSource> get(
YoutubeHttpClient httpClient, String url) async {
if (_cache[url] == null) {

View File

@ -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<String, dynamic> _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<PlaylistResponse> get(YoutubeHttpClient httpClient, String id,
{int index = 0}) {
var url =
@ -48,6 +60,7 @@ class PlaylistResponse {
});
}
///
static Future<PlaylistResponse> searchResults(
YoutubeHttpClient httpClient, String query,
{int page = 0}) {

View File

@ -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<SearchPage> nextPage(YoutubeHttpClient httpClient) {
Future<SearchPage> nextPage(YoutubeHttpClient httpClient) async {
if (initialData.continuation == '') {
return null;
}
@ -77,6 +83,7 @@ class SearchPage {
xsrfToken: xsfrToken);
}
///
static Future<SearchPage> 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

View File

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

View File

@ -6,6 +6,7 @@ import '../youtube_http_client.dart';
import 'player_response.dart';
import 'stream_info_provider.dart';
///
class VideoInfoResponse {
final Map<String, String> _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<VideoInfoResponse> 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;

View File

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

View File

@ -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<String> getString(dynamic url,
{Map<String, String> headers, bool validate = true}) async {
var response = await get(url, headers: headers);
@ -64,6 +66,7 @@ class YoutubeHttpClient extends http.BaseClient {
return response;
}
///
Future<String> postString(dynamic url,
{Map<String, String> body,
Map<String, String> headers,
@ -77,6 +80,7 @@ class YoutubeHttpClient extends http.BaseClient {
return response.body;
}
///
// TODO: Check why isRateLimited is not working.
Stream<List<int>> getStream(StreamInfo streamInfo,
{Map<String, String> headers,
@ -126,6 +130,7 @@ class YoutubeHttpClient extends http.BaseClient {
// }
}
///
Future<int> getContentLength(dynamic url,
{Map<String, String> headers, bool validate = true}) async {
var response = await head(url, headers: headers);

View File

@ -46,6 +46,7 @@ class ClosedCaptionClient {
return ClosedCaptionTrack(captions);
}
///
Future<String> 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';
}
}

View File

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

View File

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

View File

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

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