parent
51e40f27cc
commit
9c8e9630ab
|
@ -3,6 +3,9 @@
|
||||||
- Only throw custom exceptions from the library.
|
- Only throw custom exceptions from the library.
|
||||||
- `getUploadsFromPage` no longer throws.
|
- `getUploadsFromPage` no longer throws.
|
||||||
|
|
||||||
|
## 1.6.2
|
||||||
|
- Bug fixes: #80
|
||||||
|
|
||||||
## 1.6.1
|
## 1.6.1
|
||||||
- Add thumbnail to `SearchVideo` thanks to @shinyford !
|
- Add thumbnail to `SearchVideo` thanks to @shinyford !
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,32 @@ extension StringUtility on String {
|
||||||
|
|
||||||
/// Strips out all non digit characters.
|
/// Strips out all non digit characters.
|
||||||
String stripNonDigits() => replaceAll(_exp, '');
|
String stripNonDigits() => replaceAll(_exp, '');
|
||||||
|
|
||||||
|
///
|
||||||
|
String extractJson() {
|
||||||
|
var buffer = StringBuffer();
|
||||||
|
var depth = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
var ch = this[i];
|
||||||
|
var chPrv = i > 0 ? this[i - 1] : '';
|
||||||
|
|
||||||
|
buffer.write(ch);
|
||||||
|
|
||||||
|
if (ch == '{' && chPrv != '\\') {
|
||||||
|
depth++;
|
||||||
|
} else if (ch == '}' && chPrv != '\\') {
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List decipher utility.
|
/// List decipher utility.
|
||||||
|
|
|
@ -7,14 +7,25 @@ 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 = RegExp(r"'PLAYER_CONFIG':\s*(\{.*\})\}");
|
static final _playerConfigExp =
|
||||||
|
RegExp('[\'"]PLAYER_CONFIG[\'"]\\s*:\\s*(\\{.*\\})');
|
||||||
|
|
||||||
final Document _root;
|
final Document _root;
|
||||||
_PlayerConfig _playerConfig;
|
_PlayerConfig _playerConfig;
|
||||||
String __playerConfigJson;
|
String __playerConfigJson;
|
||||||
|
|
||||||
|
///
|
||||||
|
String get sourceUrl {
|
||||||
|
var url = _root.querySelector('*[name="player_ias/base"]').attributes['src'];
|
||||||
|
if (url == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 'https://youtube.com$url';
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
_PlayerConfig get playerconfig {
|
_PlayerConfig get playerconfig {
|
||||||
if (_playerConfig != null) {
|
if (_playerConfig != null) {
|
||||||
|
@ -24,7 +35,8 @@ class EmbedPage {
|
||||||
if (playerConfigJson == null) {
|
if (playerConfigJson == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return _playerConfig = _PlayerConfig(json.decode(playerConfigJson));
|
return _playerConfig =
|
||||||
|
_PlayerConfig(json.decode(playerConfigJson.extractJson()));
|
||||||
}
|
}
|
||||||
|
|
||||||
String get _playerConfigJson => __playerConfigJson ??= _root
|
String get _playerConfigJson => __playerConfigJson ??= _root
|
||||||
|
|
|
@ -30,7 +30,7 @@ class PlayerSource {
|
||||||
var val = RegExp(r'(?<=invalid namespace.*?;[\w\s]+=)\d+')
|
var val = RegExp(r'(?<=invalid namespace.*?;[\w\s]+=)\d+')
|
||||||
.stringMatch(_root)
|
.stringMatch(_root)
|
||||||
?.nullIfWhitespace ??
|
?.nullIfWhitespace ??
|
||||||
RegExp(r'(?<=this\.signatureTimestamp=)\d+')
|
RegExp(r'(?<=signatureTimestamp[=\:])\d+')
|
||||||
.stringMatch(_root)
|
.stringMatch(_root)
|
||||||
?.nullIfWhitespace;
|
?.nullIfWhitespace;
|
||||||
if (val == null) {
|
if (val == null) {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
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;
|
||||||
|
|
||||||
|
@ -35,6 +33,16 @@ class WatchPage {
|
||||||
String _xsfrToken;
|
String _xsfrToken;
|
||||||
_PlayerConfig _playerConfig;
|
_PlayerConfig _playerConfig;
|
||||||
|
|
||||||
|
///
|
||||||
|
String get sourceUrl {
|
||||||
|
var url =
|
||||||
|
_root.querySelector('*[name="player_ias/base"]').attributes['src'];
|
||||||
|
if (url == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 'https://youtube.com$url';
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
_InitialData get initialData =>
|
_InitialData get initialData =>
|
||||||
_initialData ??= _InitialData(WatchPageId.fromRawJson(_extractJson(
|
_initialData ??= _InitialData(WatchPageId.fromRawJson(_extractJson(
|
||||||
|
@ -86,13 +94,14 @@ class WatchPage {
|
||||||
?.nullIfWhitespace ??
|
?.nullIfWhitespace ??
|
||||||
'0');
|
'0');
|
||||||
|
|
||||||
static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\}\});');
|
static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\})');
|
||||||
|
|
||||||
///
|
///
|
||||||
_PlayerConfig get playerConfig => _playerConfig ??= _PlayerConfig(
|
_PlayerConfig get playerConfig => _playerConfig ??= _PlayerConfig(
|
||||||
PlayerConfigJson.fromRawJson(_playerConfigExp
|
PlayerConfigJson.fromRawJson(_playerConfigExp
|
||||||
.firstMatch(_root.getElementsByTagName('html').first.text)
|
.firstMatch(_root.getElementsByTagName('html').first.text)
|
||||||
?.group(1)));
|
?.group(1)
|
||||||
|
?.extractJson()));
|
||||||
|
|
||||||
String _extractJson(String html, String separator) {
|
String _extractJson(String html, String separator) {
|
||||||
return _matchJson(
|
return _matchJson(
|
||||||
|
|
|
@ -11,7 +11,7 @@ class YoutubeHttpClient extends http.BaseClient {
|
||||||
|
|
||||||
final Map<String, String> _defaultHeaders = const {
|
final Map<String, String> _defaultHeaders = const {
|
||||||
'user-agent':
|
'user-agent':
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
|
||||||
'accept-language': 'en-US,en;q=1.0',
|
'accept-language': 'en-US,en;q=1.0',
|
||||||
'x-youtube-client-name': '1',
|
'x-youtube-client-name': '1',
|
||||||
'x-youtube-client-version': '2.20200609.04.02',
|
'x-youtube-client-version': '2.20200609.04.02',
|
||||||
|
|
|
@ -39,8 +39,8 @@ class StreamsClient {
|
||||||
throw VideoUnplayableException.unplayable(videoId);
|
throw VideoUnplayableException.unplayable(videoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerSource =
|
var playerSource = await PlayerSource.get(
|
||||||
await PlayerSource.get(_httpClient, playerConfig.sourceUrl);
|
_httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl);
|
||||||
var cipherOperations = playerSource.getCiperOperations();
|
var cipherOperations = playerSource.getCiperOperations();
|
||||||
|
|
||||||
var videoInfoResponse = await VideoInfoResponse.get(
|
var videoInfoResponse = await VideoInfoResponse.get(
|
||||||
|
@ -91,8 +91,8 @@ class StreamsClient {
|
||||||
videoId, VideoId(previewVideoId));
|
videoId, VideoId(previewVideoId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerSource =
|
var playerSource = await PlayerSource.get(
|
||||||
await PlayerSource.get(_httpClient, playerConfig.sourceUrl);
|
_httpClient, watchPage.sourceUrl ?? playerConfig.sourceUrl);
|
||||||
var cipherOperations = playerSource.getCiperOperations();
|
var cipherOperations = playerSource.getCiperOperations();
|
||||||
|
|
||||||
if (!playerResponse.isVideoPlayable) {
|
if (!playerResponse.isVideoPlayable) {
|
||||||
|
|
|
@ -3,11 +3,11 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
YoutubeExplode yt;
|
YoutubeExplode yt;
|
||||||
setUp(() {
|
setUpAll(() {
|
||||||
yt = YoutubeExplode();
|
yt = YoutubeExplode();
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDownAll(() {
|
||||||
yt.close();
|
yt.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
YoutubeExplode yt;
|
YoutubeExplode yt;
|
||||||
setUp(() {
|
setUpAll(() {
|
||||||
yt = YoutubeExplode();
|
yt = YoutubeExplode();
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDownAll(() {
|
||||||
yt.close();
|
yt.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
YoutubeExplode yt;
|
YoutubeExplode yt;
|
||||||
setUp(() {
|
setUpAll(() {
|
||||||
yt = YoutubeExplode();
|
yt = YoutubeExplode();
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDownAll(() {
|
||||||
yt.close();
|
yt.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
YoutubeExplode yt;
|
YoutubeExplode yt;
|
||||||
setUp(() {
|
setUpAll(() {
|
||||||
yt = YoutubeExplode();
|
yt = YoutubeExplode();
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDownAll(() {
|
||||||
yt.close();
|
yt.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
YoutubeExplode yt;
|
YoutubeExplode yt;
|
||||||
setUp(() {
|
setUpAll(() {
|
||||||
yt = YoutubeExplode();
|
yt = YoutubeExplode();
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDownAll(() {
|
||||||
yt.close();
|
yt.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -35,12 +35,12 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Search youtube videos have thumbnails', () async {
|
test('Search youtube videos have thumbnails', () async {
|
||||||
var searchQuery = await yt.search.queryFromPage('hello');
|
var searchQuery = await yt.search.queryFromPage('hello');
|
||||||
expect(searchQuery.content.first, isA<SearchVideo>());
|
expect(searchQuery.content.first, isA<SearchVideo>());
|
||||||
|
|
||||||
var video = searchQuery.content.first as SearchVideo;
|
var video = searchQuery.content.first as SearchVideo;
|
||||||
expect(video.videoThumbnails, isNotEmpty);
|
expect(video.videoThumbnails, isNotEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Search youtube videos from search page (stream)', () async {
|
test('Search youtube videos from search page (stream)', () async {
|
||||||
var query = await yt.search.getVideosFromPage('hello').take(30).toList();
|
var query = await yt.search.getVideosFromPage('hello').take(30).toList();
|
||||||
|
|
|
@ -3,11 +3,11 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
YoutubeExplode yt;
|
YoutubeExplode yt;
|
||||||
setUp(() {
|
setUpAll(() {
|
||||||
yt = YoutubeExplode();
|
yt = YoutubeExplode();
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDownAll(() {
|
||||||
yt.close();
|
yt.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,5 +53,5 @@ void main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, skip: 'Occasionally may fail with certain videos');
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,11 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
YoutubeExplode yt;
|
YoutubeExplode yt;
|
||||||
setUp(() {
|
setUpAll(() {
|
||||||
yt = YoutubeExplode();
|
yt = YoutubeExplode();
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDownAll(() {
|
||||||
yt.close();
|
yt.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue