Fix all linter info
This commit is contained in:
parent
e107c60581
commit
ab8edbe7bd
|
@ -13,6 +13,6 @@ doc/api/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.iml
|
*.iml
|
||||||
/tool/
|
/bin/
|
||||||
|
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 '../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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'));
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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}) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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