Implement caching of some results
This commit is contained in:
parent
7f093fa79f
commit
6f0f3601cc
|
@ -1,3 +1,6 @@
|
|||
## 1.3.1
|
||||
- Implement caching of some results.
|
||||
|
||||
## 1.3.0
|
||||
- Added api get youtube comments of a video.
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ YoutubeExplode is a library that provides an interface to query metadata of YouT
|
|||
|
||||
Add the dependency to the pubspec.yaml (Check for the latest version)
|
||||
```yaml
|
||||
youtube_explode_dart: ^1.2.0
|
||||
youtube_explode_dart: ^1.3.0
|
||||
```
|
||||
|
||||
Import the library
|
||||
|
|
|
@ -6,11 +6,13 @@ import '../youtube_http_client.dart';
|
|||
class ClosedCaptionTrackResponse {
|
||||
final xml.XmlDocument _root;
|
||||
|
||||
ClosedCaptionTrackResponse(this._root);
|
||||
Iterable<ClosedCaption> _closedCaptions;
|
||||
|
||||
Iterable<ClosedCaption> get closedCaptions =>
|
||||
Iterable<ClosedCaption> get closedCaptions => _closedCaptions ??=
|
||||
_root.findAllElements('p').map((e) => ClosedCaption._(e));
|
||||
|
||||
ClosedCaptionTrackResponse(this._root);
|
||||
|
||||
ClosedCaptionTrackResponse.parse(String raw) : _root = xml.parse(raw);
|
||||
|
||||
static Future<ClosedCaptionTrackResponse> get(
|
||||
|
@ -35,29 +37,36 @@ class ClosedCaptionTrackResponse {
|
|||
class ClosedCaption {
|
||||
final xml.XmlElement _root;
|
||||
|
||||
ClosedCaption._(this._root);
|
||||
Duration _offset;
|
||||
Duration _duration;
|
||||
Duration _end;
|
||||
Iterable<ClosedCaptionPart> _parts;
|
||||
|
||||
String get text => _root.text;
|
||||
|
||||
Duration get offset =>
|
||||
Duration get offset => _offset ??=
|
||||
Duration(milliseconds: int.parse(_root.getAttribute('t') ?? 0));
|
||||
|
||||
Duration get duration =>
|
||||
Duration get duration => _duration ??=
|
||||
Duration(milliseconds: int.parse(_root.getAttribute('d') ?? 0));
|
||||
|
||||
Duration get end => offset + duration;
|
||||
Duration get end => _end ??= offset + duration;
|
||||
|
||||
Iterable<ClosedCaptionPart> getParts() =>
|
||||
_root.findAllElements('s').map((e) => ClosedCaptionPart._(e));
|
||||
_parts ??= _root.findAllElements('s').map((e) => ClosedCaptionPart._(e));
|
||||
|
||||
ClosedCaption._(this._root);
|
||||
}
|
||||
|
||||
class ClosedCaptionPart {
|
||||
final xml.XmlElement _root;
|
||||
|
||||
ClosedCaptionPart._(this._root);
|
||||
Duration _offset;
|
||||
|
||||
String get text => _root.text;
|
||||
|
||||
Duration get offset =>
|
||||
Duration get offset => _offset ??=
|
||||
Duration(milliseconds: int.parse(_root.getAttribute('t') ?? '0'));
|
||||
|
||||
ClosedCaptionPart._(this._root);
|
||||
}
|
||||
|
|
|
@ -8,10 +8,9 @@ class DashManifest {
|
|||
static final _urlSignatureExp = RegExp(r'/s/(.*?)(?:/|$)');
|
||||
|
||||
final xml.XmlDocument _root;
|
||||
Iterable<_StreamInfo> _streams;
|
||||
|
||||
DashManifest(this._root);
|
||||
|
||||
Iterable<_StreamInfo> get streams => _root
|
||||
Iterable<_StreamInfo> get streams => _streams ??= _root
|
||||
.findElements('Representation')
|
||||
.where((e) => e
|
||||
.findElements('Initialization')
|
||||
|
@ -20,6 +19,9 @@ 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) {
|
||||
|
|
|
@ -12,23 +12,28 @@ class EmbedPage {
|
|||
RegExp(r"yt\.setConfig\({'PLAYER_CONFIG':(.*)}\);");
|
||||
|
||||
final Document _root;
|
||||
|
||||
EmbedPage(this._root);
|
||||
_PlayerConfig _playerConfig;
|
||||
String __playerConfigJson;
|
||||
|
||||
_PlayerConfig get playerconfig {
|
||||
if (_playerConfig != null) {
|
||||
return _playerConfig;
|
||||
}
|
||||
var playerConfigJson = _playerConfigJson;
|
||||
if (playerConfigJson == null) {
|
||||
return null;
|
||||
}
|
||||
return _PlayerConfig(json.decode(playerConfigJson));
|
||||
return _playerConfig = _PlayerConfig(json.decode(playerConfigJson));
|
||||
}
|
||||
|
||||
String get _playerConfigJson => _root
|
||||
String get _playerConfigJson => __playerConfigJson ??= _root
|
||||
.getElementsByTagName('script')
|
||||
.map((e) => e.text)
|
||||
.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) {
|
||||
|
|
|
@ -9,36 +9,60 @@ class PlayerResponse {
|
|||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
|
||||
PlayerResponse(this._root);
|
||||
String _playerabilityStatus;
|
||||
bool _isVideoAvailable;
|
||||
bool _isVideoPlayable;
|
||||
String _videoTitle;
|
||||
String _videoAuthor;
|
||||
DateTime _videoUploadDate;
|
||||
String _videoChannelId;
|
||||
Duration _videoDuration;
|
||||
Iterable<String> _videoKeywords;
|
||||
String _videoDescription;
|
||||
int _videoViewCount;
|
||||
String _previewVideoId;
|
||||
bool _isLive;
|
||||
String _hlsManifestUrl;
|
||||
String _dashManifestUrl;
|
||||
Iterable<StreamInfoProvider> _muxedStreams;
|
||||
Iterable<StreamInfoProvider> _adaptiveStreams;
|
||||
List<StreamInfoProvider> _streams;
|
||||
Iterable<ClosedCaptionTrack> _closedCaptionTrack;
|
||||
String _videoPlayabilityError;
|
||||
|
||||
String get playabilityStatus => _root['playabilityStatus']['status'];
|
||||
String get playabilityStatus =>
|
||||
_playerabilityStatus ??= _root['playabilityStatus']['status'];
|
||||
|
||||
bool get isVideoAvailable => playabilityStatus.toLowerCase() != 'error';
|
||||
bool get isVideoAvailable =>
|
||||
_isVideoAvailable ??= playabilityStatus.toLowerCase() != 'error';
|
||||
|
||||
bool get isVideoPlayable => playabilityStatus.toLowerCase() == 'ok';
|
||||
bool get isVideoPlayable =>
|
||||
_isVideoAvailable ??= playabilityStatus.toLowerCase() == 'ok';
|
||||
|
||||
String get videoTitle => _root['videoDetails']['title'];
|
||||
String get videoTitle => _videoTitle ??= _root['videoDetails']['title'];
|
||||
|
||||
String get videoAuthor => _root['videoDetails']['author'];
|
||||
String get videoAuthor => _videoAuthor ??= _root['videoDetails']['author'];
|
||||
|
||||
DateTime get videoUploadDate => DateTime.parse(
|
||||
DateTime get videoUploadDate => _videoUploadDate ??= DateTime.parse(
|
||||
_root['microformat']['playerMicroformatRenderer']['uploadDate']);
|
||||
|
||||
String get videoChannelId => _root['videoDetails']['channelId'];
|
||||
String get videoChannelId =>
|
||||
_videoChannelId ??= _root['videoDetails']['channelId'];
|
||||
|
||||
Duration get videoDuration =>
|
||||
Duration get videoDuration => _videoDuration ??=
|
||||
Duration(seconds: int.parse(_root['videoDetails']['lengthSeconds']));
|
||||
|
||||
Iterable<String> get videoKeywords =>
|
||||
Iterable<String> get videoKeywords => _videoKeywords ??=
|
||||
_root['videoDetails']['keywords']?.cast<String>() ?? const [];
|
||||
|
||||
String get videoDescription => _root['videoDetails']['shortDescription'];
|
||||
String get videoDescription =>
|
||||
_videoDescription ??= _root['videoDetails']['shortDescription'];
|
||||
|
||||
int get videoViewCount => int.parse(_root['videoDetails']['viewCount']);
|
||||
int get videoViewCount =>
|
||||
_videoViewCount ??= int.parse(_root['videoDetails']['viewCount']);
|
||||
|
||||
// Can be null
|
||||
String get previewVideoId =>
|
||||
_root
|
||||
String get previewVideoId => _previewVideoId ??= _root
|
||||
.get('playabilityStatus')
|
||||
?.get('errorScreen')
|
||||
?.get('playerLegacyDesktopYpcTrailerRenderer')
|
||||
|
@ -51,45 +75,46 @@ class PlayerResponse {
|
|||
?.getValue('playerVars') ??
|
||||
'')['video_id'];
|
||||
|
||||
bool get isLive => _root.get('videoDetails')?.getValue('isLive') ?? false;
|
||||
bool get isLive =>
|
||||
_isLive ??= _root.get('videoDetails')?.getValue('isLive') ?? false;
|
||||
|
||||
// Can be null
|
||||
String get hlsManifestUrl =>
|
||||
String get hlsManifestUrl => _hlsManifestUrl ??=
|
||||
_root.get('streamingData')?.getValue('hlsManifestUrl');
|
||||
|
||||
// Can be null
|
||||
String get dashManifestUrl =>
|
||||
String get dashManifestUrl => _dashManifestUrl ??=
|
||||
_root.get('streamingData')?.getValue('dashManifestUrl');
|
||||
|
||||
Iterable<StreamInfoProvider> get muxedStreams =>
|
||||
_root
|
||||
Iterable<StreamInfoProvider> get muxedStreams => _muxedStreams ??= _root
|
||||
?.get('streamingData')
|
||||
?.getValue('formats')
|
||||
?.map((e) => _StreamInfo(e))
|
||||
?.cast<StreamInfoProvider>() ??
|
||||
const <StreamInfoProvider>[];
|
||||
|
||||
Iterable<StreamInfoProvider> get adaptiveStreams =>
|
||||
_root
|
||||
Iterable<StreamInfoProvider> get adaptiveStreams => _adaptiveStreams ??= _root
|
||||
?.get('streamingData')
|
||||
?.getValue('adaptiveFormats')
|
||||
?.map((e) => _StreamInfo(e))
|
||||
?.cast<StreamInfoProvider>() ??
|
||||
const <StreamInfoProvider>[];
|
||||
|
||||
Iterable<StreamInfoProvider> get streams =>
|
||||
[...muxedStreams, ...adaptiveStreams];
|
||||
List<StreamInfoProvider> get streams =>
|
||||
_streams ??= [...muxedStreams, ...adaptiveStreams];
|
||||
|
||||
Iterable<ClosedCaptionTrack> get closedCaptionTrack =>
|
||||
_root
|
||||
.get('captions')
|
||||
?.get('playerCaptionsTracklistRenderer')
|
||||
?.getValue('captionTracks')
|
||||
?.map((e) => ClosedCaptionTrack(e))
|
||||
?.cast<ClosedCaptionTrack>() ??
|
||||
const [];
|
||||
_closedCaptionTrack ??= _root
|
||||
.get('captions')
|
||||
?.get('playerCaptionsTracklistRenderer')
|
||||
?.getValue('captionTracks')
|
||||
?.map((e) => ClosedCaptionTrack(e))
|
||||
?.cast<ClosedCaptionTrack>() ??
|
||||
const [];
|
||||
|
||||
String getVideoPlayabilityError() =>
|
||||
PlayerResponse(this._root);
|
||||
|
||||
String getVideoPlayabilityError() => _videoPlayabilityError ??=
|
||||
_root.get('playabilityStatus')?.getValue('reason');
|
||||
|
||||
PlayerResponse.parse(String raw) : _root = json.decode(raw);
|
||||
|
@ -99,53 +124,66 @@ class ClosedCaptionTrack {
|
|||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
|
||||
String _url;
|
||||
String _languageCode;
|
||||
String _languageName;
|
||||
bool _autoGenerated;
|
||||
|
||||
String get url => _url ??= _root['baseUrl'];
|
||||
|
||||
String get languageCode => _languageCode ??= _root['languageCode'];
|
||||
|
||||
String get languageName => _languageName ??= _root['name']['simpleText'];
|
||||
|
||||
bool get autoGenerated =>
|
||||
_autoGenerated ??= _root['vssId'].toLowerCase().startsWith("a.");
|
||||
|
||||
ClosedCaptionTrack(this._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.");
|
||||
}
|
||||
|
||||
class _StreamInfo extends StreamInfoProvider {
|
||||
static final _contentLenExp = RegExp(r'[\?&]clen=(\d+)');
|
||||
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
|
||||
_StreamInfo(this._root);
|
||||
int _bitrate;
|
||||
String _container;
|
||||
int _contentLength;
|
||||
int _framerate;
|
||||
String _signature;
|
||||
String _signatureParameter;
|
||||
int _tag;
|
||||
String _url;
|
||||
|
||||
@override
|
||||
int get bitrate => _root['bitrate'];
|
||||
int get bitrate => _bitrate ??= _root['bitrate'];
|
||||
|
||||
@override
|
||||
String get container => mimeType.subtype;
|
||||
|
||||
static final _contentLenExp = RegExp(r'[\?&]clen=(\d+)');
|
||||
String get container => _container ??= mimeType.subtype;
|
||||
|
||||
@override
|
||||
int get contentLength =>
|
||||
int.tryParse(_root['contentLength'] ?? '') ??
|
||||
_contentLenExp.firstMatch(url)?.group(1);
|
||||
_contentLength ??= int.tryParse(_root['contentLength'] ?? '') ??
|
||||
_contentLenExp.firstMatch(url)?.group(1);
|
||||
|
||||
@override
|
||||
int get framerate => _root['fps'];
|
||||
int get framerate => _framerate ??= _root['fps'];
|
||||
|
||||
@override
|
||||
String get signature =>
|
||||
Uri.splitQueryString(_root['signatureCipher'] ?? '')['s'];
|
||||
_signature ??= Uri.splitQueryString(_root['signatureCipher'] ?? '')['s'];
|
||||
|
||||
@override
|
||||
String get signatureParameter =>
|
||||
String get signatureParameter => _signatureParameter ??=
|
||||
Uri.splitQueryString(_root['cipher'] ?? '')['sp'] ??
|
||||
Uri.splitQueryString(_root['signatureCipher'] ?? '')['sp'];
|
||||
Uri.splitQueryString(_root['signatureCipher'] ?? '')['sp'];
|
||||
|
||||
@override
|
||||
int get tag => _root['itag'];
|
||||
int get tag => _tag ??= _root['itag'];
|
||||
|
||||
@override
|
||||
String get url => _getUrl();
|
||||
String get url => _url ??= _getUrl();
|
||||
|
||||
String _getUrl() {
|
||||
var url = _root['url'];
|
||||
|
@ -154,6 +192,10 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
return url;
|
||||
}
|
||||
|
||||
bool _isAudioOnly;
|
||||
MediaType _mimeType;
|
||||
String _codecs;
|
||||
|
||||
@override
|
||||
String get videoCodec =>
|
||||
isAudioOnly ? null : codecs.split(',').first.trim().nullIfWhitespace;
|
||||
|
@ -167,11 +209,12 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
@override
|
||||
int get videoWidth => _root['width'];
|
||||
|
||||
bool get isAudioOnly => mimeType.type == 'audio';
|
||||
bool get isAudioOnly => _isAudioOnly ??= mimeType.type == 'audio';
|
||||
|
||||
MediaType get mimeType => MediaType.parse(_root['mimeType']);
|
||||
MediaType get mimeType => _mimeType ??= MediaType.parse(_root['mimeType']);
|
||||
|
||||
String get codecs => mimeType?.parameters['codecs']?.toLowerCase();
|
||||
String get codecs =>
|
||||
_codecs ??= mimeType?.parameters['codecs']?.toLowerCase();
|
||||
|
||||
@override
|
||||
String get audioCodec =>
|
||||
|
@ -183,4 +226,6 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
}
|
||||
return codecs.last;
|
||||
}
|
||||
|
||||
_StreamInfo(this._root);
|
||||
}
|
||||
|
|
|
@ -15,16 +15,20 @@ class PlayerSource {
|
|||
|
||||
final String _root;
|
||||
|
||||
PlayerSource(this._root);
|
||||
String _sts;
|
||||
String _deciphererDefinitionBody;
|
||||
|
||||
String get sts {
|
||||
if (_sts != null) {
|
||||
return _sts;
|
||||
}
|
||||
var val = RegExp(r'(?<=invalid namespace.*?;\w+\s*=)\d+')
|
||||
.stringMatch(_root)
|
||||
?.nullIfWhitespace;
|
||||
if (val == null) {
|
||||
throw FatalFailureException('Could not find sts in player source.');
|
||||
}
|
||||
return val;
|
||||
return _sts ??= val;
|
||||
}
|
||||
|
||||
Iterable<CipherOperation> getCiperOperations() sync* {
|
||||
|
@ -74,11 +78,15 @@ class PlayerSource {
|
|||
}
|
||||
|
||||
String _getDeciphererFuncBody() {
|
||||
if (_deciphererDefinitionBody != null) {
|
||||
return _deciphererDefinitionBody;
|
||||
}
|
||||
var funcName = _funcBodyExp.firstMatch(_root).group(1);
|
||||
|
||||
var exp = RegExp(
|
||||
r'(?!h\.)' '${RegExp.escape(funcName)}' r'=function\(\w+\)\{(.*?)\}');
|
||||
return exp.firstMatch(_root).group(1).nullIfWhitespace;
|
||||
return _deciphererDefinitionBody ??=
|
||||
exp.firstMatch(_root).group(1).nullIfWhitespace;
|
||||
}
|
||||
|
||||
String _getDeciphererDefinitionBody(String deciphererFuncBody) {
|
||||
|
@ -92,6 +100,8 @@ class PlayerSource {
|
|||
return exp.firstMatch(_root).group(0).nullIfWhitespace;
|
||||
}
|
||||
|
||||
PlayerSource(this._root);
|
||||
|
||||
// Same as default constructor
|
||||
PlayerSource.parse(this._root);
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ class PlaylistResponse {
|
|||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
|
||||
PlaylistResponse(this._root);
|
||||
|
||||
String get title => _root['title'];
|
||||
|
||||
|
@ -26,6 +25,8 @@ class PlaylistResponse {
|
|||
Iterable<_Video> get 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.');
|
||||
|
|
|
@ -13,10 +13,13 @@ 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(
|
||||
|
@ -27,10 +30,6 @@ class SearchPage {
|
|||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
||||
'window["ytInitialData"] ='))));
|
||||
|
||||
String _xsrfToken;
|
||||
|
||||
static final _xsfrTokenExp = RegExp('"XSRF_TOKEN":"(.+?)"');
|
||||
|
||||
String get xsfrToken => _xsrfToken ??= _xsfrTokenExp
|
||||
.firstMatch(_root
|
||||
.querySelectorAll('script')
|
||||
|
@ -66,6 +65,7 @@ class SearchPage {
|
|||
: _initialData = initalData,
|
||||
_xsrfToken = xsfrToken;
|
||||
|
||||
// TODO: Replace this in favour of async* when quering;
|
||||
Future<SearchPage> nextPage(YoutubeHttpClient httpClient) {
|
||||
if (initialData.continuation == '') {
|
||||
return null;
|
||||
|
|
|
@ -9,30 +9,39 @@ import 'stream_info_provider.dart';
|
|||
class VideoInfoResponse {
|
||||
final Map<String, String> _root;
|
||||
|
||||
VideoInfoResponse(this._root);
|
||||
String _status;
|
||||
bool _isVideoAvailable;
|
||||
PlayerResponse _playerResponse;
|
||||
Iterable<_StreamInfo> _muxedStreams;
|
||||
Iterable<_StreamInfo> _adaptiveStreams;
|
||||
Iterable<_StreamInfo> _streams;
|
||||
|
||||
String get status => _root['status'];
|
||||
String get status => _status ??= _root['status'];
|
||||
|
||||
bool get isVideoAvailable => status.toLowerCase() != 'fail';
|
||||
bool get isVideoAvailable =>
|
||||
_isVideoAvailable ??= status.toLowerCase() != 'fail';
|
||||
|
||||
PlayerResponse get playerResponse =>
|
||||
PlayerResponse.parse(_root['player_response']);
|
||||
_playerResponse ??= PlayerResponse.parse(_root['player_response']);
|
||||
|
||||
Iterable<_StreamInfo> get muxedStreams =>
|
||||
_root['url_encoded_fmt_stream_map']
|
||||
?.split(',')
|
||||
?.map(Uri.splitQueryString)
|
||||
?.map((e) => _StreamInfo(e)) ??
|
||||
const [];
|
||||
_muxedStreams ??= _root['url_encoded_fmt_stream_map']
|
||||
?.split(',')
|
||||
?.map(Uri.splitQueryString)
|
||||
?.map((e) => _StreamInfo(e)) ??
|
||||
const [];
|
||||
|
||||
Iterable<_StreamInfo> get adaptiveStreams =>
|
||||
_root['adaptive_fmts']
|
||||
?.split(',')
|
||||
?.map(Uri.splitQueryString)
|
||||
?.map((e) => _StreamInfo(e)) ??
|
||||
const [];
|
||||
_adaptiveStreams ??= _root['adaptive_fmts']
|
||||
?.split(',')
|
||||
?.map(Uri.splitQueryString)
|
||||
?.map((e) => _StreamInfo(e)) ??
|
||||
const [];
|
||||
|
||||
Iterable<_StreamInfo> get streams => [...muxedStreams, ...adaptiveStreams];
|
||||
Iterable<_StreamInfo> get streams =>
|
||||
_streams ??= [...muxedStreams, ...adaptiveStreams];
|
||||
|
||||
VideoInfoResponse(this._root);
|
||||
|
||||
VideoInfoResponse.parse(String raw) : _root = Uri.splitQueryString(raw);
|
||||
|
||||
|
@ -57,55 +66,73 @@ class VideoInfoResponse {
|
|||
class _StreamInfo extends StreamInfoProvider {
|
||||
final Map<String, String> _root;
|
||||
|
||||
_StreamInfo(this._root);
|
||||
int _tag;
|
||||
String _url;
|
||||
String _signature;
|
||||
String _signatureParameter;
|
||||
int _contentLength;
|
||||
int _bitrate;
|
||||
MediaType _mimeType;
|
||||
String _container;
|
||||
List<String> _codecs;
|
||||
String _audioCodec;
|
||||
String _videoCodec;
|
||||
bool _isAudioOnly;
|
||||
String _videoQualityLabel;
|
||||
List<int> __size;
|
||||
int _videoWidth;
|
||||
int _videoHeight;
|
||||
int _framerate;
|
||||
|
||||
@override
|
||||
int get tag => int.parse(_root['itag']);
|
||||
int get tag => _tag ??= int.parse(_root['itag']);
|
||||
|
||||
@override
|
||||
String get url => _root['url'];
|
||||
String get url => _url ??= _root['url'];
|
||||
|
||||
@override
|
||||
String get signature => _root['s'];
|
||||
String get signature => _signature ??= _root['s'];
|
||||
|
||||
@override
|
||||
String get signatureParameter => _root['sp'];
|
||||
String get signatureParameter => _signatureParameter ??= _root['sp'];
|
||||
|
||||
@override
|
||||
int get contentLength => int.tryParse(_root['clen'] ??
|
||||
int get contentLength => _contentLength ??= int.tryParse(_root['clen'] ??
|
||||
StreamInfoProvider.contentLenExp.firstMatch(url).group(1));
|
||||
|
||||
@override
|
||||
int get bitrate => int.parse(_root['bitrate']);
|
||||
int get bitrate => _bitrate ??= int.parse(_root['bitrate']);
|
||||
|
||||
MediaType get mimeType => MediaType.parse(_root["type"]);
|
||||
MediaType get mimeType => _mimeType ??= MediaType.parse(_root["type"]);
|
||||
|
||||
@override
|
||||
String get container => mimeType.subtype;
|
||||
String get container => _container ??= mimeType.subtype;
|
||||
|
||||
List<String> get codecs =>
|
||||
mimeType.parameters['codecs'].split(',').map((e) => e.trim());
|
||||
_codecs ??= mimeType.parameters['codecs'].split(',').map((e) => e.trim());
|
||||
|
||||
@override
|
||||
String get audioCodec => codecs.last;
|
||||
String get audioCodec => _audioCodec ??= codecs.last;
|
||||
|
||||
@override
|
||||
String get videoCodec => isAudioOnly ? null : codecs.first;
|
||||
String get videoCodec => _videoCodec ??= isAudioOnly ? null : codecs.first;
|
||||
|
||||
bool get isAudioOnly => mimeType.type == 'audio';
|
||||
bool get isAudioOnly => _isAudioOnly ??= mimeType.type == 'audio';
|
||||
|
||||
@override
|
||||
String get videoQualityLabel => _root['quality_label'];
|
||||
String get videoQualityLabel => _videoQualityLabel ??= _root['quality_label'];
|
||||
|
||||
List<int> get _size =>
|
||||
_root['size'].split(',').map((e) => int.tryParse(e ?? ''));
|
||||
__size ??= _root['size'].split(',').map((e) => int.tryParse(e ?? ''));
|
||||
|
||||
@override
|
||||
int get videoWidth => _size.first;
|
||||
int get videoWidth => _videoWidth ??= _size.first;
|
||||
|
||||
@override
|
||||
int get videoHeight => _size.last;
|
||||
int get videoHeight => _videoHeight ??= _size.last;
|
||||
|
||||
@override
|
||||
int get framerate => int.tryParse(_root['fps'] ?? '');
|
||||
int get framerate => _framerate ??= int.tryParse(_root['fps'] ?? '');
|
||||
|
||||
_StreamInfo(this._root);
|
||||
}
|
||||
|
|
|
@ -26,10 +26,12 @@ class WatchPage {
|
|||
final String visitorInfoLive;
|
||||
final String ysc;
|
||||
|
||||
WatchPage(this._root, this.visitorInfoLive, this.ysc);
|
||||
_InitialData _initialData;
|
||||
String _xsfrToken;
|
||||
_PlayerConfig _playerConfig;
|
||||
|
||||
_InitialData get initialData =>
|
||||
_InitialData(json.decode(_matchJson(_extractJson(
|
||||
_initialData ??= _InitialData(json.decode(_matchJson(_extractJson(
|
||||
_root
|
||||
.querySelectorAll('script')
|
||||
.map((e) => e.text)
|
||||
|
@ -37,7 +39,7 @@ class WatchPage {
|
|||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
||||
'window["ytInitialData"] ='))));
|
||||
|
||||
String get xsfrToken => _xsfrTokenExp
|
||||
String get xsfrToken => _xsfrToken ??= _xsfrTokenExp
|
||||
.firstMatch(_root
|
||||
.querySelectorAll('script')
|
||||
.firstWhere((e) => _xsfrTokenExp.hasMatch(e.text))
|
||||
|
@ -101,6 +103,8 @@ 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);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: youtube_explode_dart
|
||||
description: A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
|
||||
version: 1.3.0
|
||||
version: 1.3.1
|
||||
homepage: https://github.com/Hexer10/youtube_explode_dart
|
||||
|
||||
environment:
|
||||
|
|
|
@ -58,26 +58,25 @@ void main() {
|
|||
'Qzu-fTdjeFY'
|
||||
]));
|
||||
});
|
||||
test('GetVideosInAnyPlaylist', () async {
|
||||
var data = const {
|
||||
'PL601B2E69B03FAB9D',
|
||||
'PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e',
|
||||
'PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk',
|
||||
'OLAK5uy_mtOdjCW76nDvf5yOzgcAVMYpJ5gcW5uKU',
|
||||
'RD1hu8-y6fKg0',
|
||||
'RDMMU-ty-2B02VY',
|
||||
'RDCLAK5uy_lf8okgl2ygD075nhnJVjlfhwp8NsUgEbs',
|
||||
'ULl6WWX-BgIiE',
|
||||
'UUTMt7iMWa7jy0fNXIktwyLA',
|
||||
'OLAK5uy_lLeonUugocG5J0EUAEDmbskX4emejKwcM',
|
||||
'FLEnBXANsKmyj2r9xVyKoDiQ'
|
||||
};
|
||||
|
||||
for (var playlistId in data) {
|
||||
var data = const {
|
||||
'PL601B2E69B03FAB9D',
|
||||
'PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e',
|
||||
'PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk',
|
||||
'OLAK5uy_mtOdjCW76nDvf5yOzgcAVMYpJ5gcW5uKU',
|
||||
'RD1hu8-y6fKg0',
|
||||
'RDMMU-ty-2B02VY',
|
||||
'RDCLAK5uy_lf8okgl2ygD075nhnJVjlfhwp8NsUgEbs',
|
||||
'ULl6WWX-BgIiE',
|
||||
'UUTMt7iMWa7jy0fNXIktwyLA',
|
||||
'OLAK5uy_lLeonUugocG5J0EUAEDmbskX4emejKwcM',
|
||||
'FLEnBXANsKmyj2r9xVyKoDiQ'
|
||||
};
|
||||
for (var playlistId in data) {
|
||||
test('GetVideosInAnyPlaylist - $playlistId', () async {
|
||||
var videos =
|
||||
await yt.playlists.getVideos(PlaylistId(playlistId)).toList();
|
||||
expect(videos, isNotEmpty);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,24 +12,24 @@ void main() {
|
|||
yt.close();
|
||||
});
|
||||
|
||||
test('GetStreamsOfAnyVideo', () async {
|
||||
var data = {
|
||||
'9bZkp7q19f0',
|
||||
var data = {
|
||||
'9bZkp7q19f0',
|
||||
// 'SkRSXFQerZs', age restricted videos are not supported anymore.
|
||||
'hySoCSoH-g8',
|
||||
'_kmeFXjjGfk',
|
||||
'MeJVWBSsPAY',
|
||||
'5VGm0dczmHc',
|
||||
'ZGdLIwrGHG8',
|
||||
'rsAAeyAr-9Y',
|
||||
'AI7ULzgf8RU'
|
||||
};
|
||||
for (var videoId in data) {
|
||||
'hySoCSoH-g8',
|
||||
'_kmeFXjjGfk',
|
||||
'MeJVWBSsPAY',
|
||||
'5VGm0dczmHc',
|
||||
'ZGdLIwrGHG8',
|
||||
'rsAAeyAr-9Y',
|
||||
'AI7ULzgf8RU'
|
||||
};
|
||||
for (var videoId in data) {
|
||||
test('GetStreamsOfAnyVideo - $videoId', () async {
|
||||
var manifest =
|
||||
await yt.videos.streamsClient.getManifest(VideoId(videoId));
|
||||
expect(manifest.streams, isNotEmpty);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('GetStreamOfUnplayableVideo', () async {
|
||||
expect(yt.videos.streamsClient.getManifest(VideoId('5qap5aO4i9A')),
|
||||
|
|
Loading…
Reference in New Issue