Fix all linter info
This commit is contained in:
parent
e107c60581
commit
ab8edbe7bd
|
@ -13,6 +13,6 @@ doc/api/
|
|||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
/tool/
|
||||
/bin/
|
||||
|
||||
.flutter-plugins-dependencies
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# Analysis options to run test for ginder
|
||||
include: package:pedantic/analysis_options.yaml
|
|
@ -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();
|
Loading…
Reference in New Issue