Implement caching of some results

This commit is contained in:
Hexah 2020-06-22 17:40:57 +02:00
parent 7f093fa79f
commit 6f0f3601cc
14 changed files with 254 additions and 149 deletions

View File

@ -1,3 +1,6 @@
## 1.3.1
- Implement caching of some results.
## 1.3.0
- Added api get youtube comments of a video.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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')),