parent
10312329d3
commit
8372726a65
|
@ -1,3 +1,8 @@
|
||||||
|
## 1.7.4
|
||||||
|
- Fix slow download ( #92 )
|
||||||
|
- Fix stream retrieving on some videos ( #90 )
|
||||||
|
- Updates tests
|
||||||
|
|
||||||
## 1.7.3
|
## 1.7.3
|
||||||
- Fix exceptions on some videos.
|
- Fix exceptions on some videos.
|
||||||
- Closes #89, #88
|
- Closes #89, #88
|
||||||
|
|
|
@ -15,14 +15,35 @@ class ChannelAboutPage {
|
||||||
_InitialData _initialData;
|
_InitialData _initialData;
|
||||||
|
|
||||||
///
|
///
|
||||||
_InitialData get initialData =>
|
_InitialData get initialData {
|
||||||
_initialData ??= _InitialData(ChannelAboutPageId.fromRawJson(_extractJson(
|
if (_initialData != null) {
|
||||||
_root
|
return _initialData;
|
||||||
.querySelectorAll('script')
|
}
|
||||||
.map((e) => e.text)
|
|
||||||
.toList()
|
final scriptText = _root
|
||||||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
.querySelectorAll('script')
|
||||||
'window["ytInitialData"] =')));
|
.map((e) => e.text)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
var initialDataText = scriptText.firstWhere(
|
||||||
|
(e) => e.contains('window["ytInitialData"] ='),
|
||||||
|
orElse: () => null);
|
||||||
|
if (initialDataText != null) {
|
||||||
|
return _initialData = _InitialData(ChannelAboutPageId.fromRawJson(
|
||||||
|
_extractJson(initialDataText, 'window["ytInitialData"] =')));
|
||||||
|
}
|
||||||
|
|
||||||
|
initialDataText = scriptText.firstWhere(
|
||||||
|
(e) => e.contains('var ytInitialData = '),
|
||||||
|
orElse: () => null);
|
||||||
|
if (initialDataText != null) {
|
||||||
|
return _initialData = _InitialData(ChannelAboutPageId.fromRawJson(
|
||||||
|
_extractJson(initialDataText, 'var ytInitialData = ')));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw TransientFailureException(
|
||||||
|
'Failed to retrieve initial data from the channel about page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
bool get isOk => initialData != null;
|
bool get isOk => initialData != null;
|
||||||
|
|
|
@ -19,14 +19,35 @@ class ChannelUploadPage {
|
||||||
_InitialData _initialData;
|
_InitialData _initialData;
|
||||||
|
|
||||||
///
|
///
|
||||||
_InitialData get initialData => _initialData ??= _InitialData(
|
_InitialData get initialData {
|
||||||
ChannelUploadPageId.fromJson(json.decode(_extractJson(
|
if (_initialData != null) {
|
||||||
_root
|
return _initialData;
|
||||||
.querySelectorAll('script')
|
}
|
||||||
.map((e) => e.text)
|
|
||||||
.toList()
|
final scriptText = _root
|
||||||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
.querySelectorAll('script')
|
||||||
'window["ytInitialData"] ='))));
|
.map((e) => e.text)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
var initialDataText = scriptText.firstWhere(
|
||||||
|
(e) => e.contains('window["ytInitialData"] ='),
|
||||||
|
orElse: () => null);
|
||||||
|
if (initialDataText != null) {
|
||||||
|
return _initialData = _InitialData(ChannelUploadPageId.fromRawJson(
|
||||||
|
_extractJson(initialDataText, 'window["ytInitialData"] =')));
|
||||||
|
}
|
||||||
|
|
||||||
|
initialDataText = scriptText.firstWhere(
|
||||||
|
(e) => e.contains('var ytInitialData = '),
|
||||||
|
orElse: () => null);
|
||||||
|
if (initialDataText != null) {
|
||||||
|
return _initialData = _InitialData(ChannelUploadPageId.fromRawJson(
|
||||||
|
_extractJson(initialDataText, 'var ytInitialData = ')));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw TransientFailureException(
|
||||||
|
'Failed to retrieve initial data from the channel upload page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars
|
||||||
|
}
|
||||||
|
|
||||||
String _extractJson(String html, String separator) {
|
String _extractJson(String html, String separator) {
|
||||||
return _matchJson(
|
return _matchJson(
|
||||||
|
|
|
@ -38,17 +38,30 @@ class SearchPage {
|
||||||
if (_initialData != null) {
|
if (_initialData != null) {
|
||||||
return _initialData;
|
return _initialData;
|
||||||
}
|
}
|
||||||
var scriptTag = _extractJson(
|
|
||||||
_root.querySelectorAll('script').map((e) => e.text).toList().firstWhere(
|
final scriptText = _root
|
||||||
(e) => e.contains('window["ytInitialData"] ='),
|
.querySelectorAll('script')
|
||||||
orElse: () => null),
|
.map((e) => e.text)
|
||||||
'window["ytInitialData"] =');
|
.toList(growable: false);
|
||||||
scriptTag ??= _extractJson(
|
|
||||||
_root.querySelectorAll('script').map((e) => e.text).toList().firstWhere(
|
var initialDataText = scriptText.firstWhere(
|
||||||
(e) => e.contains('var ytInitialData ='),
|
(e) => e.contains('window["ytInitialData"] ='),
|
||||||
orElse: () => '{}'),
|
orElse: () => null);
|
||||||
'var ytInitialData =');
|
if (initialDataText != null) {
|
||||||
return _initialData ??= _InitialData(SearchPageId.fromRawJson(scriptTag));
|
return _initialData = _InitialData(SearchPageId.fromRawJson(
|
||||||
|
_extractJson(initialDataText, 'window["ytInitialData"] =')));
|
||||||
|
}
|
||||||
|
|
||||||
|
initialDataText = scriptText.firstWhere(
|
||||||
|
(e) => e.contains('var ytInitialData = '),
|
||||||
|
orElse: () => null);
|
||||||
|
if (initialDataText != null) {
|
||||||
|
return _initialData = _InitialData(SearchPageId.fromRawJson(
|
||||||
|
_extractJson(initialDataText, 'var ytInitialData = ')));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw TransientFailureException(
|
||||||
|
'Failed to retrieve initial data from the search page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars
|
||||||
}
|
}
|
||||||
|
|
||||||
String _extractJson(String html, String separator) {
|
String _extractJson(String html, String separator) {
|
||||||
|
|
|
@ -51,14 +51,35 @@ class WatchPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
_InitialData get initialData =>
|
_InitialData get initialData {
|
||||||
_initialData ??= _InitialData(WatchPageId.fromRawJson(_extractJson(
|
if (_initialData != null) {
|
||||||
_root
|
return _initialData;
|
||||||
.querySelectorAll('script')
|
}
|
||||||
.map((e) => e.text)
|
|
||||||
.toList()
|
final scriptText = _root
|
||||||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
.querySelectorAll('script')
|
||||||
'window["ytInitialData"] =')));
|
.map((e) => e.text)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
var initialDataText = scriptText.firstWhere(
|
||||||
|
(e) => e.contains('window["ytInitialData"] ='),
|
||||||
|
orElse: () => null);
|
||||||
|
if (initialDataText != null) {
|
||||||
|
return _initialData = _InitialData(WatchPageId.fromRawJson(
|
||||||
|
_extractJson(initialDataText, 'window["ytInitialData"] =')));
|
||||||
|
}
|
||||||
|
|
||||||
|
initialDataText = scriptText.firstWhere(
|
||||||
|
(e) => e.contains('var ytInitialData = '),
|
||||||
|
orElse: () => null);
|
||||||
|
if (initialDataText != null) {
|
||||||
|
return _initialData = _InitialData(WatchPageId.fromRawJson(
|
||||||
|
_extractJson(initialDataText, 'var ytInitialData = ')));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw TransientFailureException(
|
||||||
|
'Failed to retrieve initial data from the watch page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
String get xsfrToken => _xsfrToken ??= _xsfrTokenExp
|
String get xsfrToken => _xsfrToken ??= _xsfrTokenExp
|
||||||
|
@ -110,10 +131,10 @@ class WatchPage {
|
||||||
?.group(1)
|
?.group(1)
|
||||||
?.extractJson()));
|
?.extractJson()));
|
||||||
|
|
||||||
|
///
|
||||||
PlayerResponse get playerResponse => PlayerResponse.parse(_root
|
PlayerResponse get playerResponse => PlayerResponse.parse(_root
|
||||||
.querySelectorAll('script')
|
.querySelectorAll('script')
|
||||||
.map((e) => e.text)
|
.map((e) => e.text)
|
||||||
.map((e) => null)
|
|
||||||
.map((e) => _playerResponseExp.firstMatch(e)?.group(1))
|
.map((e) => _playerResponseExp.firstMatch(e)?.group(1))
|
||||||
.firstWhere((e) => !e.isNullOrWhiteSpace)
|
.firstWhere((e) => !e.isNullOrWhiteSpace)
|
||||||
.extractJson());
|
.extractJson());
|
||||||
|
|
|
@ -84,25 +84,41 @@ class YoutubeHttpClient extends http.BaseClient {
|
||||||
return response.body;
|
return response.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
Stream<List<int>> getStream(StreamInfo streamInfo,
|
Stream<List<int>> getStream(StreamInfo streamInfo,
|
||||||
{Map<String, String> headers,
|
{Map<String, String> headers,
|
||||||
bool validate = true,
|
bool validate = true,
|
||||||
int start = 0,
|
int start = 0,
|
||||||
int errorCount = 0}) async* {
|
int errorCount = 0}) async* {
|
||||||
var url = streamInfo.url;
|
var url = streamInfo.url;
|
||||||
|
var bytesCount = start;
|
||||||
var query = Map<String, String>.from(url.queryParameters);
|
for (var i = start; i < streamInfo.size.totalBytes; i += 9898989) {
|
||||||
query['ratebypass'] = 'yes';
|
try {
|
||||||
url = url.replace(queryParameters: query);
|
final request = http.Request('get', url);
|
||||||
|
request.headers['range'] = 'bytes=$i-${i + 9898989 - 1}';
|
||||||
var request = http.Request('get', url);
|
final response = await send(request);
|
||||||
request.headers.addAll(_defaultHeaders);
|
if (validate) {
|
||||||
var response = await request.send();
|
_validateResponse(response, response.statusCode);
|
||||||
if (validate) {
|
}
|
||||||
_validateResponse(response, response.statusCode);
|
final stream = StreamController<List<int>>();
|
||||||
|
response.stream.listen((data) {
|
||||||
|
bytesCount += data.length;
|
||||||
|
stream.add(data);
|
||||||
|
}, onError: (_) => null, onDone: stream.close, cancelOnError: false);
|
||||||
|
errorCount = 0;
|
||||||
|
yield* stream.stream;
|
||||||
|
} on Exception {
|
||||||
|
if (errorCount == 5) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
yield* getStream(streamInfo,
|
||||||
|
headers: headers,
|
||||||
|
validate: validate,
|
||||||
|
start: bytesCount,
|
||||||
|
errorCount: errorCount + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
yield* response.stream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
|
@ -78,7 +78,13 @@ class StreamsClient {
|
||||||
|
|
||||||
Future<StreamContext> _getStreamContextFromWatchPage(VideoId videoId) async {
|
Future<StreamContext> _getStreamContextFromWatchPage(VideoId videoId) async {
|
||||||
var watchPage = await WatchPage.get(_httpClient, videoId.toString());
|
var watchPage = await WatchPage.get(_httpClient, videoId.toString());
|
||||||
var playerConfig = watchPage.playerConfig;
|
|
||||||
|
dynamic /* _PlayerConfig */ playerConfig;
|
||||||
|
try {
|
||||||
|
playerConfig = watchPage.playerConfig;
|
||||||
|
} on FormatException {
|
||||||
|
playerConfig = null;
|
||||||
|
}
|
||||||
var playerResponse =
|
var playerResponse =
|
||||||
playerConfig?.playerResponse ?? watchPage.playerResponse;
|
playerConfig?.playerResponse ?? watchPage.playerResponse;
|
||||||
if (playerResponse == null) {
|
if (playerResponse == null) {
|
||||||
|
@ -86,12 +92,13 @@ class StreamsClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
var previewVideoId = playerResponse.previewVideoId;
|
var previewVideoId = playerResponse.previewVideoId;
|
||||||
if (!previewVideoId.isNullOrWhiteSpace) {
|
if (!((previewVideoId as String)?.isNullOrWhiteSpace ?? true)) {
|
||||||
throw VideoRequiresPurchaseException.preview(
|
throw VideoRequiresPurchaseException.preview(
|
||||||
videoId, VideoId(previewVideoId));
|
videoId, VideoId(previewVideoId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerSourceUrl = watchPage.sourceUrl ?? playerConfig?.sourceUrl;
|
var playerSourceUrl =
|
||||||
|
watchPage.sourceUrl ?? playerConfig?.sourceUrl as String;
|
||||||
var playerSource = !playerSourceUrl.isNullOrWhiteSpace
|
var playerSource = !playerSourceUrl.isNullOrWhiteSpace
|
||||||
? await PlayerSource.get(_httpClient, playerSourceUrl)
|
? await PlayerSource.get(_httpClient, playerSourceUrl)
|
||||||
: null;
|
: null;
|
||||||
|
@ -112,7 +119,7 @@ class StreamsClient {
|
||||||
];
|
];
|
||||||
|
|
||||||
var dashManifestUrl = playerResponse.dashManifestUrl;
|
var dashManifestUrl = playerResponse.dashManifestUrl;
|
||||||
if (!dashManifestUrl.isNullOrWhiteSpace) {
|
if (!(dashManifestUrl?.isNullOrWhiteSpace ?? true)) {
|
||||||
var dashManifest =
|
var dashManifest =
|
||||||
await _getDashManifest(Uri.parse(dashManifestUrl), cipherOperations);
|
await _getDashManifest(Uri.parse(dashManifestUrl), cipherOperations);
|
||||||
streamInfoProviders.addAll(dashManifest.streams);
|
streamInfoProviders.addAll(dashManifest.streams);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: youtube_explode_dart
|
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.
|
description: A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
|
||||||
version: 1.7.3
|
version: 1.7.4
|
||||||
homepage: https://github.com/Hexer10/youtube_explode_dart
|
homepage: https://github.com/Hexer10/youtube_explode_dart
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -11,12 +11,18 @@ void main() {
|
||||||
yt.close();
|
yt.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Get streams of any video', () {
|
group('Get streams manifest of any video', () {
|
||||||
for (var val in {
|
for (var val in {
|
||||||
VideoId('5VGm0dczmHc'), // rating is not allowed
|
VideoId('9bZkp7q19f0'), // very popular
|
||||||
|
VideoId('SkRSXFQerZs'), // age restricted (embed allowed)
|
||||||
|
VideoId('hySoCSoH-g8'), // age restricted (embed not allowed)
|
||||||
|
VideoId('_kmeFXjjGfk'), // embed not allowed (type 1)
|
||||||
|
VideoId('MeJVWBSsPAY'), // embed not allowed (type 2)
|
||||||
|
VideoId('5VGm0dczmHc'), // rating not allowed
|
||||||
VideoId('ZGdLIwrGHG8'), // unlisted
|
VideoId('ZGdLIwrGHG8'), // unlisted
|
||||||
VideoId('rsAAeyAr-9Y'),
|
VideoId('rsAAeyAr-9Y'), // recording of a live stream
|
||||||
VideoId('AI7ULzgf8RU')
|
VideoId('AI7ULzgf8RU'), // has DASH manifest
|
||||||
|
VideoId('-xNN-bJQ4vI'), // 360° video
|
||||||
}) {
|
}) {
|
||||||
test('VideoId - ${val.value}', () async {
|
test('VideoId - ${val.value}', () async {
|
||||||
var manifest = await yt.videos.streamsClient.getManifest(val);
|
var manifest = await yt.videos.streamsClient.getManifest(val);
|
||||||
|
@ -39,12 +45,18 @@ void main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Get stream of any playable video', () {
|
group('Get specific stream of any playable video', () {
|
||||||
for (var val in {
|
for (var val in {
|
||||||
VideoId('5VGm0dczmHc'), // rating is not allowed
|
VideoId('9bZkp7q19f0'), // very popular
|
||||||
|
VideoId('SkRSXFQerZs'), // age restricted (embed allowed)
|
||||||
|
VideoId('hySoCSoH-g8'), // age restricted (embed not allowed)
|
||||||
|
VideoId('_kmeFXjjGfk'), // embed not allowed (type 1)
|
||||||
|
VideoId('MeJVWBSsPAY'), // embed not allowed (type 2)
|
||||||
|
VideoId('5VGm0dczmHc'), // rating not allowed
|
||||||
VideoId('ZGdLIwrGHG8'), // unlisted
|
VideoId('ZGdLIwrGHG8'), // unlisted
|
||||||
VideoId('rsAAeyAr-9Y'),
|
VideoId('rsAAeyAr-9Y'), // recording of a live stream
|
||||||
VideoId('AI7ULzgf8RU')
|
VideoId('AI7ULzgf8RU'), // has DASH manifest
|
||||||
|
VideoId('-xNN-bJQ4vI'), // 360° video
|
||||||
}) {
|
}) {
|
||||||
test('VideoId - ${val.value}', () async {
|
test('VideoId - ${val.value}', () async {
|
||||||
var manifest = await yt.videos.streamsClient.getManifest(val);
|
var manifest = await yt.videos.streamsClient.getManifest(val);
|
||||||
|
|
|
@ -32,7 +32,12 @@ void main() {
|
||||||
expect(video.thumbnails.highResUrl, isNotEmpty);
|
expect(video.thumbnails.highResUrl, isNotEmpty);
|
||||||
expect(video.thumbnails.standardResUrl, isNotEmpty);
|
expect(video.thumbnails.standardResUrl, isNotEmpty);
|
||||||
expect(video.thumbnails.maxResUrl, isNotEmpty);
|
expect(video.thumbnails.maxResUrl, isNotEmpty);
|
||||||
expect(video.keywords, containsAll(['osu', 'mouse' /*, 'rhythm game'*/]));
|
expect(
|
||||||
|
video.keywords,
|
||||||
|
containsAll([
|
||||||
|
'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));
|
||||||
|
|
Loading…
Reference in New Issue