parent
ae097a3198
commit
36f3efac70
|
@ -1,5 +1,8 @@
|
||||||
library _youtube_explode.extensions;
|
library _youtube_explode.extensions;
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import '../reverse_engineering/cipher/cipher_operations.dart';
|
import '../reverse_engineering/cipher/cipher_operations.dart';
|
||||||
|
|
||||||
/// Utility for Strings.
|
/// Utility for Strings.
|
||||||
|
@ -11,36 +14,35 @@ extension StringUtility on String {
|
||||||
String substringUntil(String separator) => substring(0, indexOf(separator));
|
String substringUntil(String separator) => substring(0, indexOf(separator));
|
||||||
|
|
||||||
///
|
///
|
||||||
String substringAfter(String separator) =>
|
String substringAfter(String separator) => substring(indexOf(separator) + separator.length);
|
||||||
substring(indexOf(separator) + separator.length);
|
|
||||||
|
|
||||||
static final _exp = RegExp(r'\D');
|
static final _exp = RegExp(r'\D');
|
||||||
|
|
||||||
/// Strips out all non digit characters.
|
/// Strips out all non digit characters.
|
||||||
String stripNonDigits() => replaceAll(_exp, '');
|
String stripNonDigits() => replaceAll(_exp, '');
|
||||||
|
|
||||||
///
|
/// Extract and decode json from a string
|
||||||
String extractJson() {
|
Map<String, dynamic>? extractJson([String separator = '']) {
|
||||||
var buffer = StringBuffer();
|
final index = indexOf(separator) + separator.length;
|
||||||
var depth = 0;
|
if (index > length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < length; i++) {
|
final str = substring(index);
|
||||||
var ch = this[i];
|
|
||||||
var chPrv = i > 0 ? this[i - 1] : '';
|
|
||||||
|
|
||||||
buffer.write(ch);
|
final startIdx = str.indexOf('{');
|
||||||
|
var endIdx = str.lastIndexOf('}');
|
||||||
|
|
||||||
if (ch == '{' && chPrv != '\\') {
|
while (true) {
|
||||||
depth++;
|
try {
|
||||||
} else if (ch == '}' && chPrv != '\\') {
|
return json.decode(str.substring(startIdx, endIdx + 1)) as Map<String, dynamic>;
|
||||||
depth--;
|
} on FormatException {
|
||||||
}
|
endIdx = str.lastIndexOf(str.substring(0, endIdx));
|
||||||
|
if (endIdx == 0) {
|
||||||
if (depth == 0) {
|
return null;
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return buffer.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime parseDateTime() => DateTime.parse(this);
|
DateTime parseDateTime() => DateTime.parse(this);
|
||||||
|
@ -166,3 +168,16 @@ extension RunsParser on List<dynamic> {
|
||||||
///
|
///
|
||||||
String parseRuns() => map((e) => e['text']).join();
|
String parseRuns() => map((e) => e['text']).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension GenericExtract on List<String> {
|
||||||
|
/// Used to extract initial data that start with `var ytInitialData = ` or 'window["ytInitialData"] ='.
|
||||||
|
T extractGenericData<T>(T Function(Map<String, dynamic>) builder, Exception Function() orThrow) {
|
||||||
|
var initialData = firstWhereOrNull((e) => e.contains('var ytInitialData = '))?.extractJson('var ytInitialData = ');
|
||||||
|
initialData ??= firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='))?.extractJson('window["ytInitialData"] =');
|
||||||
|
|
||||||
|
if (initialData != null) {
|
||||||
|
return builder(initialData);
|
||||||
|
}
|
||||||
|
throw orThrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:html/parser.dart' as parser;
|
import 'package:html/parser.dart' as parser;
|
||||||
|
@ -18,57 +16,16 @@ class ChannelAboutPage {
|
||||||
late final _InitialData initialData = _getInitialData();
|
late final _InitialData initialData = _getInitialData();
|
||||||
|
|
||||||
_InitialData _getInitialData() {
|
_InitialData _getInitialData() {
|
||||||
final scriptText = _root
|
final scriptText = _root.querySelectorAll('script').map((e) => e.text).toList(growable: false);
|
||||||
.querySelectorAll('script')
|
return scriptText.extractGenericData(
|
||||||
.map((e) => e.text)
|
(obj) => _InitialData(obj),
|
||||||
.toList(growable: false);
|
() => TransientFailureException(
|
||||||
|
'Failed to retrieve initial data from the channel about page, please report this to the project GitHub page.'));
|
||||||
var initialDataText = scriptText.firstWhere(
|
|
||||||
(e) => e.contains('window["ytInitialData"] ='),
|
|
||||||
orElse: () => '');
|
|
||||||
if (initialDataText.isNotEmpty) {
|
|
||||||
return _InitialData(json
|
|
||||||
.decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
|
|
||||||
}
|
|
||||||
|
|
||||||
initialDataText = scriptText.firstWhere(
|
|
||||||
(e) => e.contains('var ytInitialData = '),
|
|
||||||
orElse: () => '');
|
|
||||||
if (initialDataText.isNotEmpty) {
|
|
||||||
return _InitialData(
|
|
||||||
json.decode(_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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
String get description => initialData.description;
|
String get description => initialData.description;
|
||||||
|
|
||||||
String _extractJson(String html, String separator) {
|
|
||||||
return _matchJson(
|
|
||||||
html.substring(html.indexOf(separator) + separator.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
String _matchJson(String str) {
|
|
||||||
var bracketCount = 0;
|
|
||||||
late int lastI;
|
|
||||||
for (var i = 0; i < str.length; i++) {
|
|
||||||
lastI = i;
|
|
||||||
if (str[i] == '{') {
|
|
||||||
bracketCount++;
|
|
||||||
} else if (str[i] == '}') {
|
|
||||||
bracketCount--;
|
|
||||||
} else if (str[i] == ';') {
|
|
||||||
if (bracketCount == 0) {
|
|
||||||
return str.substring(0, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str.substring(0, lastI + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
ChannelAboutPage(this._root);
|
ChannelAboutPage(this._root);
|
||||||
|
|
||||||
|
@ -88,8 +45,7 @@ class ChannelAboutPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
static Future<ChannelAboutPage> getByUsername(
|
static Future<ChannelAboutPage> getByUsername(YoutubeHttpClient httpClient, String username) {
|
||||||
YoutubeHttpClient httpClient, String username) {
|
|
||||||
var url = 'https://www.youtube.com/user/$username/about?hl=en';
|
var url = 'https://www.youtube.com/user/$username/about?hl=en';
|
||||||
|
|
||||||
return retry(() async {
|
return retry(() async {
|
||||||
|
@ -128,49 +84,29 @@ class _InitialData {
|
||||||
.get('channelAboutFullMetadataRenderer')!;
|
.get('channelAboutFullMetadataRenderer')!;
|
||||||
}
|
}
|
||||||
|
|
||||||
late final String description =
|
late final String description = content.get('description')!.getT<String>('simpleText')!;
|
||||||
content.get('description')!.getT<String>('simpleText')!;
|
|
||||||
|
|
||||||
late final List<ChannelLink> channelLinks = content
|
late final List<ChannelLink> channelLinks = content
|
||||||
.getList('primaryLinks')!
|
.getList('primaryLinks')!
|
||||||
.map((e) => ChannelLink(
|
.map((e) => ChannelLink(
|
||||||
e.get('title')?.getT<String>('simpleText') ?? '',
|
e.get('title')?.getT<String>('simpleText') ?? '',
|
||||||
extractUrl(e
|
extractUrl(e.get('navigationEndpoint')?.get('commandMetadata')?.get('webCommandMetadata')?.getT<String>('url') ??
|
||||||
.get('navigationEndpoint')
|
e.get('navigationEndpoint')?.get('urlEndpoint')?.getT<String>('url') ??
|
||||||
?.get('commandMetadata')
|
|
||||||
?.get('webCommandMetadata')
|
|
||||||
?.getT<String>('url') ??
|
|
||||||
e
|
|
||||||
.get('navigationEndpoint')
|
|
||||||
?.get('urlEndpoint')
|
|
||||||
?.getT<String>('url') ??
|
|
||||||
''),
|
''),
|
||||||
Uri.parse(e
|
Uri.parse(e.get('icon')?.getList('thumbnails')?.firstOrNull?.getT<String>('url') ?? '')))
|
||||||
.get('icon')
|
|
||||||
?.getList('thumbnails')
|
|
||||||
?.firstOrNull
|
|
||||||
?.getT<String>('url') ??
|
|
||||||
'')))
|
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
late final int viewCount = int.parse(content
|
late final int viewCount = int.parse(content.get('viewCountText')!.getT<String>('simpleText')!.stripNonDigits());
|
||||||
.get('viewCountText')!
|
|
||||||
.getT<String>('simpleText')!
|
|
||||||
.stripNonDigits());
|
|
||||||
|
|
||||||
late final String joinDate =
|
late final String joinDate = content.get('joinedDateText')!.getList('runs')![1].getT<String>('text')!;
|
||||||
content.get('joinedDateText')!.getList('runs')![1].getT<String>('text')!;
|
|
||||||
|
|
||||||
late final String title = content.get('title')!.getT<String>('simpleText')!;
|
late final String title = content.get('title')!.getT<String>('simpleText')!;
|
||||||
|
|
||||||
late final List<Map<String, dynamic>> avatar =
|
late final List<Map<String, dynamic>> avatar = content.get('avatar')!.getList('thumbnails')!;
|
||||||
content.get('avatar')!.getList('thumbnails')!;
|
|
||||||
|
|
||||||
String get country => content.get('country')!.getT<String>('simpleText')!;
|
String get country => content.get('country')!.getT<String>('simpleText')!;
|
||||||
|
|
||||||
String parseRuns(List<dynamic>? runs) =>
|
String parseRuns(List<dynamic>? runs) => runs?.map((e) => e.text).join() ?? '';
|
||||||
runs?.map((e) => e.text).join() ?? '';
|
|
||||||
|
|
||||||
Uri extractUrl(String text) =>
|
Uri extractUrl(String text) => Uri.parse(Uri.decodeFull(_urlExp.firstMatch(text)?.group(1) ?? ''));
|
||||||
Uri.parse(Uri.decodeFull(_urlExp.firstMatch(text)?.group(1) ?? ''));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,179 +30,140 @@ class ChannelUploadPage {
|
||||||
.map((e) => e.text)
|
.map((e) => e.text)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
|
||||||
var initialDataText = scriptText.firstWhere(
|
return scriptText.extractGenericData((obj) => _InitialData(obj), () =>
|
||||||
(e) => e.contains('window["ytInitialData"] ='),
|
TransientFailureException(
|
||||||
orElse: () => '');
|
'Failed to retrieve initial data from the channel upload page, please report this to the project GitHub page.'));
|
||||||
if (initialDataText.isNotEmpty) {
|
|
||||||
return _InitialData(json
|
|
||||||
.decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
|
|
||||||
}
|
|
||||||
|
|
||||||
initialDataText = scriptText.firstWhere(
|
|
||||||
(e) => e.contains('var ytInitialData = '),
|
|
||||||
orElse: () => '');
|
|
||||||
if (initialDataText.isNotEmpty) {
|
|
||||||
return _InitialData(
|
|
||||||
json.decode(_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) {
|
///
|
||||||
return _matchJson(
|
ChannelUploadPage(this._root, this.channelId, [_InitialData ? initialData]): _initialData = initialData;
|
||||||
html.substring(html.indexOf(separator) + separator.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
String _matchJson(String str) {
|
///
|
||||||
var bracketCount = 0;
|
Future<ChannelUploadPage?> nextPage(YoutubeHttpClient httpClient) {
|
||||||
late int lastI;
|
|
||||||
for (var i = 0; i < str.length; i++) {
|
|
||||||
lastI = i;
|
|
||||||
if (str[i] == '{') {
|
|
||||||
bracketCount++;
|
|
||||||
} else if (str[i] == '}') {
|
|
||||||
bracketCount--;
|
|
||||||
} else if (str[i] == ';') {
|
|
||||||
if (bracketCount == 0) {
|
|
||||||
return str.substring(0, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str.substring(0, lastI + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
ChannelUploadPage(this._root, this.channelId, [_InitialData? initialData])
|
|
||||||
: _initialData = initialData;
|
|
||||||
|
|
||||||
///
|
|
||||||
Future<ChannelUploadPage?> nextPage(YoutubeHttpClient httpClient) {
|
|
||||||
if (initialData.continuation.isEmpty) {
|
if (initialData.continuation.isEmpty) {
|
||||||
return Future.value(null);
|
return Future.value(null);
|
||||||
}
|
}
|
||||||
var url =
|
var url =
|
||||||
'https://www.youtube.com/browse_ajax?ctoken=${initialData.continuation}&continuation=${initialData.continuation}&itct=${initialData.clickTrackingParams}';
|
'https://www.youtube.com/browse_ajax?ctoken=${initialData.continuation}&continuation=${initialData.continuation}&itct=${initialData.clickTrackingParams}';
|
||||||
return retry(() async {
|
return retry(() async {
|
||||||
var raw = await httpClient.getString(url);
|
var raw = await httpClient.getString(url);
|
||||||
return ChannelUploadPage(
|
return ChannelUploadPage(
|
||||||
null, channelId, _InitialData(json.decode(raw)[1]));
|
null, channelId, _InitialData(json.decode(raw)[1]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
static Future<ChannelUploadPage> get(
|
static Future<ChannelUploadPage> get(
|
||||||
YoutubeHttpClient httpClient, String channelId, String sorting) {
|
YoutubeHttpClient httpClient, String channelId, String sorting) {
|
||||||
var url =
|
var url =
|
||||||
'https://www.youtube.com/channel/$channelId/videos?view=0&sort=$sorting&flow=grid';
|
'https://www.youtube.com/channel/$channelId/videos?view=0&sort=$sorting&flow=grid';
|
||||||
return retry(() async {
|
return retry(() async {
|
||||||
var raw = await httpClient.getString(url);
|
var raw = await httpClient.getString(url);
|
||||||
return ChannelUploadPage.parse(raw, channelId);
|
return ChannelUploadPage.parse(raw, channelId);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
ChannelUploadPage.parse(String raw, this.channelId)
|
||||||
|
: _root = parser.parse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
class _InitialData {
|
||||||
ChannelUploadPage.parse(String raw, this.channelId)
|
|
||||||
: _root = parser.parse(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InitialData {
|
|
||||||
// Json parsed map
|
// Json parsed map
|
||||||
final Map<String, dynamic> root;
|
final Map<String, dynamic> root;
|
||||||
|
|
||||||
_InitialData(this.root);
|
_InitialData(this.root);
|
||||||
|
|
||||||
late final Map<String, dynamic>? continuationContext =
|
late final Map<String, dynamic>? continuationContext =
|
||||||
getContinuationContext();
|
getContinuationContext();
|
||||||
|
|
||||||
late final String clickTrackingParams =
|
late final String clickTrackingParams =
|
||||||
continuationContext?.getT<String>('continuationContext') ?? '';
|
continuationContext?.getT<String>('continuationContext') ?? '';
|
||||||
|
|
||||||
late final List<ChannelVideo> uploads =
|
late final List<ChannelVideo> uploads =
|
||||||
getContentContext().map(_parseContent).whereNotNull().toList();
|
getContentContext().map(_parseContent).whereNotNull().toList();
|
||||||
|
|
||||||
late final String continuation =
|
late final String continuation =
|
||||||
continuationContext?.getT<String>('continuation') ?? '';
|
continuationContext?.getT<String>('continuation') ?? '';
|
||||||
|
|
||||||
List<Map<String, dynamic>> getContentContext() {
|
List<Map<String, dynamic>> getContentContext() {
|
||||||
List<Map<String, dynamic>>? context;
|
List<Map<String, dynamic>>? context;
|
||||||
if (root.containsKey('contents')) {
|
if (root.containsKey('contents')) {
|
||||||
context = root
|
context = root
|
||||||
.get('contents')
|
.get('contents')
|
||||||
?.get('twoColumnBrowseResultsRenderer')
|
?.get('twoColumnBrowseResultsRenderer')
|
||||||
?.getList('tabs')
|
?.getList('tabs')
|
||||||
?.map((e) => e['tabRenderer'])
|
?.map((e) => e['tabRenderer'])
|
||||||
.cast<Map<String, dynamic>>()
|
.cast<Map<String, dynamic>>()
|
||||||
.firstWhereOrNull((e) => e['selected'] as bool)
|
.firstWhereOrNull((e) => e['selected'] as bool)
|
||||||
?.get('content')
|
?.get('content')
|
||||||
?.get('sectionListRenderer')
|
?.get('sectionListRenderer')
|
||||||
?.getList('contents')
|
?.getList('contents')
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.get('itemSectionRenderer')
|
?.get('itemSectionRenderer')
|
||||||
?.getList('contents')
|
?.getList('contents')
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.get('gridRenderer')
|
?.get('gridRenderer')
|
||||||
?.getList('items')
|
?.getList('items')
|
||||||
?.cast<Map<String, dynamic>>();
|
?.cast<Map<String, dynamic>>();
|
||||||
}
|
}
|
||||||
if (context == null && root.containsKey('response')) {
|
if (context == null && root.containsKey('response')) {
|
||||||
context = root
|
context = root
|
||||||
.get('response')
|
.get('response')
|
||||||
?.get('continuationContents')
|
?.get('continuationContents')
|
||||||
?.get('gridContinuation')
|
?.get('gridContinuation')
|
||||||
?.getList('items')
|
?.getList('items')
|
||||||
?.cast<Map<String, dynamic>>();
|
?.cast<Map<String, dynamic>>();
|
||||||
}
|
}
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
throw FatalFailureException('Failed to get initial data context.');
|
throw FatalFailureException('Failed to get initial data context.');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic>? getContinuationContext() {
|
Map<String, dynamic>? getContinuationContext() {
|
||||||
if (root.containsKey('contents')) {
|
if (root.containsKey('contents')) {
|
||||||
return root
|
return root
|
||||||
.get('contents')
|
.get('contents')
|
||||||
?.get('twoColumnBrowseResultsRenderer')
|
?.get('twoColumnBrowseResultsRenderer')
|
||||||
?.getList('tabs')
|
?.getList('tabs')
|
||||||
?.map((e) => e['tabRenderer'])
|
?.map((e) => e['tabRenderer'])
|
||||||
.cast<Map<String, dynamic>>()
|
.cast<Map<String, dynamic>>()
|
||||||
.firstWhereOrNull((e) => e['selected'] as bool)
|
.firstWhereOrNull((e) => e['selected'] as bool)
|
||||||
?.get('content')
|
?.get('content')
|
||||||
?.get('sectionListRenderer')
|
?.get('sectionListRenderer')
|
||||||
?.getList('contents')
|
?.getList('contents')
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.get('itemSectionRenderer')
|
?.get('itemSectionRenderer')
|
||||||
?.getList('contents')
|
?.getList('contents')
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.get('gridRenderer')
|
?.get('gridRenderer')
|
||||||
?.getList('continuations')
|
?.getList('continuations')
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.get('nextContinuationData');
|
?.get('nextContinuationData');
|
||||||
}
|
}
|
||||||
if (root.containsKey('response')) {
|
if (root.containsKey('response')) {
|
||||||
return root
|
return root
|
||||||
.get('response')
|
.get('response')
|
||||||
?.get('continuationContents')
|
?.get('continuationContents')
|
||||||
?.get('gridContinuation')
|
?.get('gridContinuation')
|
||||||
?.getList('continuations')
|
?.getList('continuations')
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.get('nextContinuationData');
|
?.get('nextContinuationData');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelVideo? _parseContent(Map<String, dynamic>? content) {
|
ChannelVideo? _parseContent(Map<String, dynamic>? content) {
|
||||||
if (content == null || !content.containsKey('gridVideoRenderer')) {
|
if (content == null || !content.containsKey('gridVideoRenderer')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var video = content.get('gridVideoRenderer')!;
|
var video = content.get('gridVideoRenderer')!;
|
||||||
return ChannelVideo(
|
return ChannelVideo(
|
||||||
VideoId(video.getT<String>('videoId')!),
|
VideoId(video.getT<String>('videoId')!),
|
||||||
video.get('title')?.getT<String>('simpleText') ??
|
video.get('title')?.getT<String>('simpleText') ??
|
||||||
video.get('title')?.getList('runs')?.map((e) => e['text']).join() ??
|
video.get('title')?.getList('runs')?.map((e) => e['text']).join() ??
|
||||||
'');
|
'');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:html/parser.dart' as parser;
|
import 'package:html/parser.dart' as parser;
|
||||||
|
@ -11,8 +9,7 @@ import 'player_config_base.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
class EmbedPage {
|
class EmbedPage {
|
||||||
static final _playerConfigExp =
|
static final _playerConfigExp = RegExp('[\'""]PLAYER_CONFIG[\'""]\\s*:\\s*(\\{.*\\})');
|
||||||
RegExp('[\'""]PLAYER_CONFIG[\'""]\\s*:\\s*(\\{.*\\})');
|
|
||||||
static final _playerConfigExp2 = RegExp(r'yt.setConfig\((\{.*\})');
|
static final _playerConfigExp2 = RegExp(r'yt.setConfig\((\{.*\})');
|
||||||
|
|
||||||
final Document root;
|
final Document root;
|
||||||
|
@ -24,8 +21,7 @@ class EmbedPage {
|
||||||
.querySelectorAll('*[name="player_ias/base"]')
|
.querySelectorAll('*[name="player_ias/base"]')
|
||||||
.map((e) => e.attributes['src'])
|
.map((e) => e.attributes['src'])
|
||||||
.where((e) => !e.isNullOrWhiteSpace)
|
.where((e) => !e.isNullOrWhiteSpace)
|
||||||
.firstWhere((e) => e!.contains('player_ias') && e.endsWith('.js'),
|
.firstWhere((e) => e!.contains('player_ias') && e.endsWith('.js'), orElse: () => null);
|
||||||
orElse: () => null);
|
|
||||||
// _root.querySelector('*[name="player_ias/base"]').attributes['src'];
|
// _root.querySelector('*[name="player_ias/base"]').attributes['src'];
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -35,11 +31,11 @@ class EmbedPage {
|
||||||
|
|
||||||
///
|
///
|
||||||
EmbedPlayerConfig? getPlayerConfig() {
|
EmbedPlayerConfig? getPlayerConfig() {
|
||||||
var playerConfigJson = _playerConfigJson ?? _playerConfigJson2;
|
var playerConfigJson = (_playerConfigJson ?? _playerConfigJson2)?.extractJson();
|
||||||
if (playerConfigJson == null) {
|
if (playerConfigJson == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return EmbedPlayerConfig(json.decode(playerConfigJson.extractJson()));
|
return EmbedPlayerConfig(playerConfigJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? get _playerConfigJson => root
|
String? get _playerConfigJson => root
|
||||||
|
|
|
@ -123,6 +123,8 @@ class PlayerResponse {
|
||||||
late final String? videoPlayabilityError =
|
late final String? videoPlayabilityError =
|
||||||
root.get('playabilityStatus')?.getT<String>('reason');
|
root.get('playabilityStatus')?.getT<String>('reason');
|
||||||
|
|
||||||
|
PlayerResponse(this.root);
|
||||||
|
|
||||||
///
|
///
|
||||||
PlayerResponse.parse(String raw) : root = json.decode(raw);
|
PlayerResponse.parse(String raw) : root = json.decode(raw);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,59 +24,16 @@ class PlaylistPage {
|
||||||
return _initialData!;
|
return _initialData!;
|
||||||
}
|
}
|
||||||
|
|
||||||
final scriptText = root!
|
final scriptText = root!.querySelectorAll('script').map((e) => e.text).toList(growable: false);
|
||||||
.querySelectorAll('script')
|
|
||||||
.map((e) => e.text)
|
|
||||||
.toList(growable: false);
|
|
||||||
|
|
||||||
var initialDataText = scriptText
|
return scriptText.extractGenericData(
|
||||||
.firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='));
|
(obj) => _InitialData(obj),
|
||||||
if (initialDataText != null) {
|
() => TransientFailureException(
|
||||||
return _InitialData(json
|
'Failed to retrieve initial data from the search page, please report this to the project GitHub page.'));
|
||||||
.decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
|
|
||||||
}
|
|
||||||
|
|
||||||
initialDataText =
|
|
||||||
scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = '));
|
|
||||||
if (initialDataText != null) {
|
|
||||||
return _InitialData(
|
|
||||||
json.decode(_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) {
|
|
||||||
var index = html.indexOf(separator) + separator.length;
|
|
||||||
if (index > html.length) {
|
|
||||||
throw TransientFailureException(
|
|
||||||
'Failed to retrieve initial data from the search page, please report this to the project GitHub page. Couldn\'t extract json: $html');
|
|
||||||
}
|
|
||||||
return _matchJson(html.substring(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
String _matchJson(String str) {
|
|
||||||
var bracketCount = 0;
|
|
||||||
var lastI = 0;
|
|
||||||
for (var i = 0; i < str.length; i++) {
|
|
||||||
lastI = i;
|
|
||||||
if (str[i] == '{') {
|
|
||||||
bracketCount++;
|
|
||||||
} else if (str[i] == '}') {
|
|
||||||
bracketCount--;
|
|
||||||
} else if (str[i] == ';') {
|
|
||||||
if (bracketCount == 0) {
|
|
||||||
return str.substring(0, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str.substring(0, lastI + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
PlaylistPage(this.root, this.playlistId, [_InitialData? initialData])
|
PlaylistPage(this.root, this.playlistId, [_InitialData? initialData]) : _initialData = initialData;
|
||||||
: _initialData = initialData;
|
|
||||||
|
|
||||||
///
|
///
|
||||||
Future<PlaylistPage?> nextPage(YoutubeHttpClient httpClient) async {
|
Future<PlaylistPage?> nextPage(YoutubeHttpClient httpClient) async {
|
||||||
|
@ -87,26 +44,19 @@ class PlaylistPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
static Future<PlaylistPage> get(YoutubeHttpClient httpClient, String id,
|
static Future<PlaylistPage> get(YoutubeHttpClient httpClient, String id, {String? token}) {
|
||||||
{String? token}) {
|
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
var url =
|
var url = 'https://www.youtube.com/youtubei/v1/guide?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
||||||
'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
|
||||||
|
|
||||||
return retry(() async {
|
return retry(() async {
|
||||||
var body = {
|
var body = {
|
||||||
'context': const {
|
'context': const {
|
||||||
'client': {
|
'client': {'hl': 'en', 'clientName': 'WEB', 'clientVersion': '2.20200911.04.00'}
|
||||||
'hl': 'en',
|
|
||||||
'clientName': 'WEB',
|
|
||||||
'clientVersion': '2.20200911.04.00'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'continuation': token
|
'continuation': token
|
||||||
};
|
};
|
||||||
|
|
||||||
var raw =
|
var raw = await httpClient.post(Uri.parse(url), body: json.encode(body));
|
||||||
await httpClient.post(Uri.parse(url), body: json.encode(body));
|
|
||||||
return PlaylistPage(null, id, _InitialData(json.decode(raw.body)));
|
return PlaylistPage(null, id, _InitialData(json.decode(raw.body)));
|
||||||
});
|
});
|
||||||
// Ask for next page,
|
// Ask for next page,
|
||||||
|
@ -130,10 +80,7 @@ class _InitialData {
|
||||||
|
|
||||||
_InitialData(this.root);
|
_InitialData(this.root);
|
||||||
|
|
||||||
late final String? title = root
|
late final String? title = root.get('metadata')?.get('playlistMetadataRenderer')?.getT<String>('title');
|
||||||
.get('metadata')
|
|
||||||
?.get('playlistMetadataRenderer')
|
|
||||||
?.getT<String>('title');
|
|
||||||
|
|
||||||
late final String? author = root
|
late final String? author = root
|
||||||
.get('sidebar')
|
.get('sidebar')
|
||||||
|
@ -147,10 +94,7 @@ class _InitialData {
|
||||||
?.getT<List<dynamic>>('runs')
|
?.getT<List<dynamic>>('runs')
|
||||||
?.parseRuns();
|
?.parseRuns();
|
||||||
|
|
||||||
late final String? description = root
|
late final String? description = root.get('metadata')?.get('playlistMetadataRenderer')?.getT<String>('description');
|
||||||
.get('metadata')
|
|
||||||
?.get('playlistMetadataRenderer')
|
|
||||||
?.getT<String>('description');
|
|
||||||
|
|
||||||
late final int? viewCount = root
|
late final int? viewCount = root
|
||||||
.get('sidebar')
|
.get('sidebar')
|
||||||
|
@ -163,13 +107,12 @@ class _InitialData {
|
||||||
?.getT<String>('simpleText')
|
?.getT<String>('simpleText')
|
||||||
?.parseInt();
|
?.parseInt();
|
||||||
|
|
||||||
late final String? continuationToken =
|
late final String? continuationToken = (videosContent ?? playlistVideosContent)
|
||||||
(videosContent ?? playlistVideosContent)
|
?.firstWhereOrNull((e) => e['continuationItemRenderer'] != null)
|
||||||
?.firstWhereOrNull((e) => e['continuationItemRenderer'] != null)
|
?.get('continuationItemRenderer')
|
||||||
?.get('continuationItemRenderer')
|
?.get('continuationEndpoint')
|
||||||
?.get('continuationEndpoint')
|
?.get('continuationCommand')
|
||||||
?.get('continuationCommand')
|
?.getT<String>('token');
|
||||||
?.getT<String>('token');
|
|
||||||
|
|
||||||
List<Map<String, dynamic>>? get playlistVideosContent =>
|
List<Map<String, dynamic>>? get playlistVideosContent =>
|
||||||
root
|
root
|
||||||
|
@ -187,11 +130,7 @@ class _InitialData {
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.get('playlistVideoListRenderer')
|
?.get('playlistVideoListRenderer')
|
||||||
?.getList('contents') ??
|
?.getList('contents') ??
|
||||||
root
|
root.getList('onResponseReceivedActions')?.firstOrNull?.get('appendContinuationItemsAction')?.getList('continuationItems');
|
||||||
.getList('onResponseReceivedActions')
|
|
||||||
?.firstOrNull
|
|
||||||
?.get('appendContinuationItemsAction')
|
|
||||||
?.getList('continuationItems');
|
|
||||||
|
|
||||||
late final List<Map<String, dynamic>>? videosContent = root
|
late final List<Map<String, dynamic>>? videosContent = root
|
||||||
.get('contents')
|
.get('contents')
|
||||||
|
@ -199,17 +138,10 @@ class _InitialData {
|
||||||
?.get('primaryContents')
|
?.get('primaryContents')
|
||||||
?.get('sectionListRenderer')
|
?.get('sectionListRenderer')
|
||||||
?.getList('contents') ??
|
?.getList('contents') ??
|
||||||
root
|
root.getList('onResponseReceivedCommands')?.firstOrNull?.get('appendContinuationItemsAction')?.getList('continuationItems');
|
||||||
.getList('onResponseReceivedCommands')
|
|
||||||
?.firstOrNull
|
|
||||||
?.get('appendContinuationItemsAction')
|
|
||||||
?.getList('continuationItems');
|
|
||||||
|
|
||||||
List<_Video> get playlistVideos =>
|
List<_Video> get playlistVideos =>
|
||||||
playlistVideosContent
|
playlistVideosContent?.where((e) => e['playlistVideoRenderer'] != null).map((e) => _Video(e['playlistVideoRenderer'])).toList() ??
|
||||||
?.where((e) => e['playlistVideoRenderer'] != null)
|
|
||||||
.map((e) => _Video(e['playlistVideoRenderer']))
|
|
||||||
.toList() ??
|
|
||||||
const [];
|
const [];
|
||||||
|
|
||||||
List<_Video> get videos =>
|
List<_Video> get videos =>
|
||||||
|
@ -236,13 +168,7 @@ class _Video {
|
||||||
'';
|
'';
|
||||||
|
|
||||||
String get channelId =>
|
String get channelId =>
|
||||||
root
|
root.get('ownerText')?.getList('runs')?.firstOrNull?.get('navigationEndpoint')?.get('browseEndpoint')?.getT<String>('browseId') ??
|
||||||
.get('ownerText')
|
|
||||||
?.getList('runs')
|
|
||||||
?.firstOrNull
|
|
||||||
?.get('navigationEndpoint')
|
|
||||||
?.get('browseEndpoint')
|
|
||||||
?.getT<String>('browseId') ??
|
|
||||||
root
|
root
|
||||||
.get('shortBylineText')
|
.get('shortBylineText')
|
||||||
?.getList('runs')
|
?.getList('runs')
|
||||||
|
@ -254,14 +180,11 @@ class _Video {
|
||||||
|
|
||||||
String get title => root.get('title')?.getList('runs')?.parseRuns() ?? '';
|
String get title => root.get('title')?.getList('runs')?.parseRuns() ?? '';
|
||||||
|
|
||||||
String get description =>
|
String get description => root.getList('descriptionSnippet')?.parseRuns() ?? '';
|
||||||
root.getList('descriptionSnippet')?.parseRuns() ?? '';
|
|
||||||
|
|
||||||
Duration? get duration =>
|
Duration? get duration => _stringToDuration(root.get('lengthText')?.getT<String>('simpleText'));
|
||||||
_stringToDuration(root.get('lengthText')?.getT<String>('simpleText'));
|
|
||||||
|
|
||||||
int get viewCount =>
|
int get viewCount => root.get('viewCountText')?.getT<String>('simpleText')?.parseInt() ?? 0;
|
||||||
root.get('viewCountText')?.getT<String>('simpleText')?.parseInt() ?? 0;
|
|
||||||
|
|
||||||
/// Format: HH:MM:SS
|
/// Format: HH:MM:SS
|
||||||
static Duration? _stringToDuration(String? string) {
|
static Duration? _stringToDuration(String? string) {
|
||||||
|
@ -276,14 +199,10 @@ class _Video {
|
||||||
return Duration(seconds: int.parse(parts.first));
|
return Duration(seconds: int.parse(parts.first));
|
||||||
}
|
}
|
||||||
if (parts.length == 2) {
|
if (parts.length == 2) {
|
||||||
return Duration(
|
return Duration(minutes: int.parse(parts.first), seconds: int.parse(parts[1]));
|
||||||
minutes: int.parse(parts.first), seconds: int.parse(parts[1]));
|
|
||||||
}
|
}
|
||||||
if (parts.length == 3) {
|
if (parts.length == 3) {
|
||||||
return Duration(
|
return Duration(hours: int.parse(parts[0]), minutes: int.parse(parts[1]), seconds: int.parse(parts[2]));
|
||||||
hours: int.parse(parts[0]),
|
|
||||||
minutes: int.parse(parts[1]),
|
|
||||||
seconds: int.parse(parts[2]));
|
|
||||||
}
|
}
|
||||||
throw Error();
|
throw Error();
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,98 +28,43 @@ class SearchPage {
|
||||||
return _initialData!;
|
return _initialData!;
|
||||||
}
|
}
|
||||||
|
|
||||||
final scriptText = root!
|
final scriptText = root!.querySelectorAll('script').map((e) => e.text).toList(growable: false);
|
||||||
.querySelectorAll('script')
|
return scriptText.extractGenericData(
|
||||||
.map((e) => e.text)
|
(obj) => _InitialData(obj),
|
||||||
.toList(growable: false);
|
() => TransientFailureException(
|
||||||
|
'Failed to retrieve initial data from the search page, please report this to the project GitHub page.'));
|
||||||
var initialDataText = scriptText
|
|
||||||
.firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='));
|
|
||||||
if (initialDataText != null) {
|
|
||||||
return _initialData = _InitialData(json
|
|
||||||
.decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
|
|
||||||
}
|
|
||||||
|
|
||||||
initialDataText =
|
|
||||||
scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = '));
|
|
||||||
if (initialDataText != null) {
|
|
||||||
return _initialData = _InitialData(
|
|
||||||
json.decode(_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) {
|
|
||||||
var index = html.indexOf(separator) + separator.length;
|
|
||||||
if (index > html.length) {
|
|
||||||
throw TransientFailureException(
|
|
||||||
'Failed to retrieve initial data from the search page, please report this to the project GitHub page. Couldn\'t extract json: $html');
|
|
||||||
}
|
|
||||||
return _matchJson(html.substring(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
String _matchJson(String str) {
|
|
||||||
var bracketCount = 0;
|
|
||||||
var lastI = 0;
|
|
||||||
for (var i = 0; i < str.length; i++) {
|
|
||||||
lastI = i;
|
|
||||||
if (str[i] == '{') {
|
|
||||||
bracketCount++;
|
|
||||||
} else if (str[i] == '}') {
|
|
||||||
bracketCount--;
|
|
||||||
} else if (str[i] == ';') {
|
|
||||||
if (bracketCount == 0) {
|
|
||||||
return str.substring(0, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str.substring(0, lastI + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
SearchPage(this.root, this.queryString, [_InitialData? initialData])
|
SearchPage(this.root, this.queryString, [_InitialData? initialData]) : _initialData = initialData;
|
||||||
: _initialData = initialData;
|
|
||||||
|
|
||||||
Future<SearchPage?> nextPage(YoutubeHttpClient httpClient) async {
|
Future<SearchPage?> nextPage(YoutubeHttpClient httpClient) async {
|
||||||
if (initialData.continuationToken == '' ||
|
if (initialData.continuationToken == '' || initialData.estimatedResults == 0) {
|
||||||
initialData.estimatedResults == 0) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return get(httpClient, queryString, token: initialData.continuationToken);
|
return get(httpClient, queryString, token: initialData.continuationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
static Future<SearchPage> get(
|
static Future<SearchPage> get(YoutubeHttpClient httpClient, String queryString, {String? token}) {
|
||||||
YoutubeHttpClient httpClient, String queryString,
|
|
||||||
{String? token}) {
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
var url =
|
var url = 'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
||||||
'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
|
||||||
|
|
||||||
return retry(() async {
|
return retry(() async {
|
||||||
var body = {
|
var body = {
|
||||||
'context': const {
|
'context': const {
|
||||||
'client': {
|
'client': {'hl': 'en', 'clientName': 'WEB', 'clientVersion': '2.20200911.04.00'}
|
||||||
'hl': 'en',
|
|
||||||
'clientName': 'WEB',
|
|
||||||
'clientVersion': '2.20200911.04.00'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'continuation': token
|
'continuation': token
|
||||||
};
|
};
|
||||||
|
|
||||||
var raw =
|
var raw = await httpClient.post(Uri.parse(url), body: json.encode(body));
|
||||||
await httpClient.post(Uri.parse(url), body: json.encode(body));
|
return SearchPage(null, queryString, _InitialData(json.decode(raw.body)));
|
||||||
return SearchPage(
|
|
||||||
null, queryString, _InitialData(json.decode(raw.body)));
|
|
||||||
});
|
});
|
||||||
// Ask for next page,
|
// Ask for next page,
|
||||||
|
|
||||||
}
|
}
|
||||||
var url =
|
var url = 'https://www.youtube.com/results?search_query=${Uri.encodeQueryComponent(queryString)}';
|
||||||
'https://www.youtube.com/results?search_query=${Uri.encodeQueryComponent(queryString)}';
|
|
||||||
return retry(() async {
|
return retry(() async {
|
||||||
var raw = await httpClient.getString(url);
|
var raw = await httpClient.getString(url);
|
||||||
return SearchPage.parse(raw, queryString);
|
return SearchPage.parse(raw, queryString);
|
||||||
|
@ -197,9 +142,7 @@ class _InitialData {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains only [SearchVideo] or [SearchPlaylist]
|
// Contains only [SearchVideo] or [SearchPlaylist]
|
||||||
late final List<BaseSearchContent> searchContent =
|
late final List<BaseSearchContent> searchContent = getContentContext()?.map(_parseContent).whereNotNull().toList() ?? const [];
|
||||||
getContentContext()?.map(_parseContent).whereNotNull().toList() ??
|
|
||||||
const [];
|
|
||||||
|
|
||||||
List<RelatedQuery> get relatedQueries =>
|
List<RelatedQuery> get relatedQueries =>
|
||||||
getContentContext()
|
getContentContext()
|
||||||
|
@ -207,10 +150,8 @@ class _InitialData {
|
||||||
.map((e) => e.get('horizontalCardListRenderer')?.getList('cards'))
|
.map((e) => e.get('horizontalCardListRenderer')?.getList('cards'))
|
||||||
.firstOrNull
|
.firstOrNull
|
||||||
?.map((e) => e['searchRefinementCardRenderer'])
|
?.map((e) => e['searchRefinementCardRenderer'])
|
||||||
.map((e) => RelatedQuery(
|
.map((e) =>
|
||||||
e.searchEndpoint.searchEndpoint.query,
|
RelatedQuery(e.searchEndpoint.searchEndpoint.query, VideoId(Uri.parse(e.thumbnail.thumbnails.first.url).pathSegments[1])))
|
||||||
VideoId(
|
|
||||||
Uri.parse(e.thumbnail.thumbnails.first.url).pathSegments[1])))
|
|
||||||
.toList()
|
.toList()
|
||||||
.cast<RelatedQuery>() ??
|
.cast<RelatedQuery>() ??
|
||||||
const [];
|
const [];
|
||||||
|
@ -218,11 +159,7 @@ class _InitialData {
|
||||||
List<dynamic> get relatedVideos =>
|
List<dynamic> get relatedVideos =>
|
||||||
getContentContext()
|
getContentContext()
|
||||||
?.where((e) => e['shelfRenderer'] != null)
|
?.where((e) => e['shelfRenderer'] != null)
|
||||||
.map((e) => e
|
.map((e) => e.get('shelfRenderer')?.get('content')?.get('verticalListRenderer')?.getList('items'))
|
||||||
.get('shelfRenderer')
|
|
||||||
?.get('content')
|
|
||||||
?.get('verticalListRenderer')
|
|
||||||
?.getList('items'))
|
|
||||||
.firstOrNull
|
.firstOrNull
|
||||||
?.map(_parseContent)
|
?.map(_parseContent)
|
||||||
.whereNotNull()
|
.whereNotNull()
|
||||||
|
@ -231,8 +168,7 @@ class _InitialData {
|
||||||
|
|
||||||
late final String? continuationToken = _getContinuationToken();
|
late final String? continuationToken = _getContinuationToken();
|
||||||
|
|
||||||
late final int estimatedResults =
|
late final int estimatedResults = int.parse(root.getT<String>('estimatedResults') ?? '0');
|
||||||
int.parse(root.getT<String>('estimatedResults') ?? '0');
|
|
||||||
|
|
||||||
BaseSearchContent? _parseContent(Map<String, dynamic>? content) {
|
BaseSearchContent? _parseContent(Map<String, dynamic>? content) {
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
|
@ -247,47 +183,24 @@ class _InitialData {
|
||||||
_parseRuns(renderer.get('ownerText')?.getList('runs')),
|
_parseRuns(renderer.get('ownerText')?.getList('runs')),
|
||||||
_parseRuns(renderer.get('descriptionSnippet')?.getList('runs')),
|
_parseRuns(renderer.get('descriptionSnippet')?.getList('runs')),
|
||||||
renderer.get('lengthText')?.getT<String>('simpleText') ?? '',
|
renderer.get('lengthText')?.getT<String>('simpleText') ?? '',
|
||||||
int.parse(renderer
|
int.parse(renderer.get('viewCountText')?.getT<String>('simpleText')?.stripNonDigits().nullIfWhitespace ??
|
||||||
.get('viewCountText')
|
renderer.get('viewCountText')?.getList('runs')?.firstOrNull?.getT<String>('text')?.stripNonDigits().nullIfWhitespace ??
|
||||||
?.getT<String>('simpleText')
|
|
||||||
?.stripNonDigits()
|
|
||||||
.nullIfWhitespace ??
|
|
||||||
renderer
|
|
||||||
.get('viewCountText')
|
|
||||||
?.getList('runs')
|
|
||||||
?.firstOrNull
|
|
||||||
?.getT<String>('text')
|
|
||||||
?.stripNonDigits()
|
|
||||||
.nullIfWhitespace ??
|
|
||||||
'0'),
|
'0'),
|
||||||
(renderer.get('thumbnail')?.getList('thumbnails') ?? const [])
|
(renderer.get('thumbnail')?.getList('thumbnails') ?? const [])
|
||||||
.map((e) =>
|
.map((e) => Thumbnail(Uri.parse(e['url']), e['height'], e['width']))
|
||||||
Thumbnail(Uri.parse(e['url']), e['height'], e['width']))
|
|
||||||
.toList(),
|
.toList(),
|
||||||
renderer.get('publishedTimeText')?.getT<String>('simpleText'),
|
renderer.get('publishedTimeText')?.getT<String>('simpleText'),
|
||||||
renderer
|
renderer.get('viewCountText')?.getList('runs')?.elementAtSafe(1)?.getT<String>('text')?.trim() == 'watching');
|
||||||
.get('viewCountText')
|
|
||||||
?.getList('runs')
|
|
||||||
?.elementAtSafe(1)
|
|
||||||
?.getT<String>('text')
|
|
||||||
?.trim() ==
|
|
||||||
'watching');
|
|
||||||
}
|
}
|
||||||
if (content['radioRenderer'] != null) {
|
if (content['radioRenderer'] != null) {
|
||||||
var renderer = content.get('radioRenderer')!;
|
var renderer = content.get('radioRenderer')!;
|
||||||
|
|
||||||
return SearchPlaylist(
|
return SearchPlaylist(PlaylistId(renderer.getT<String>('playlistId')!), renderer.get('title')!.getT<String>('simpleText')!,
|
||||||
PlaylistId(renderer.getT<String>('playlistId')!),
|
int.parse(_parseRuns(renderer.get('videoCountText')?.getList('runs')).stripNonDigits().nullIfWhitespace ?? '0'));
|
||||||
renderer.get('title')!.getT<String>('simpleText')!,
|
|
||||||
int.parse(_parseRuns(renderer.get('videoCountText')?.getList('runs'))
|
|
||||||
.stripNonDigits()
|
|
||||||
.nullIfWhitespace ??
|
|
||||||
'0'));
|
|
||||||
}
|
}
|
||||||
// Here ignore 'horizontalCardListRenderer' & 'shelfRenderer'
|
// Here ignore 'horizontalCardListRenderer' & 'shelfRenderer'
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _parseRuns(List<dynamic>? runs) =>
|
String _parseRuns(List<dynamic>? runs) => runs?.map((e) => e['text']).join() ?? '';
|
||||||
runs?.map((e) => e['text']).join() ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:html/parser.dart' as parser;
|
import 'package:html/parser.dart' as parser;
|
||||||
|
@ -14,15 +12,11 @@ import 'player_response.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"');
|
static final RegExp _videoDislikeExp = RegExp(r'"label"\s*:\s*"([\d,\.]+) dislikes"');
|
||||||
static final RegExp _videoDislikeExp =
|
static final RegExp _visitorInfoLiveExp = RegExp('VISITOR_INFO1_LIVE=([^;]+)');
|
||||||
RegExp(r'"label"\s*:\s*"([\d,\.]+) dislikes"');
|
|
||||||
static final RegExp _visitorInfoLiveExp =
|
|
||||||
RegExp('VISITOR_INFO1_LIVE=([^;]+)');
|
|
||||||
static final RegExp _yscExp = RegExp('YSC=([^;]+)');
|
static final RegExp _yscExp = RegExp('YSC=([^;]+)');
|
||||||
static final RegExp _playerResponseExp =
|
static final RegExp _playerResponseExp = RegExp(r'var\s+ytInitialPlayerResponse\s*=\s*(\{.*\})');
|
||||||
RegExp(r'var\s+ytInitialPlayerResponse\s*=\s*(\{.*\})');
|
|
||||||
|
|
||||||
static final _xsfrTokenExp = RegExp(r'"XSRF_TOKEN"\s*:\s*"(.+?)"');
|
static final _xsfrTokenExp = RegExp(r'"XSRF_TOKEN"\s*:\s*"(.+?)"');
|
||||||
|
|
||||||
|
@ -57,85 +51,51 @@ class WatchPage {
|
||||||
return _initialData!;
|
return _initialData!;
|
||||||
}
|
}
|
||||||
|
|
||||||
final scriptText = root
|
final scriptText = root.querySelectorAll('script').map((e) => e.text).toList(growable: false);
|
||||||
.querySelectorAll('script')
|
return scriptText.extractGenericData(
|
||||||
.map((e) => e.text)
|
(obj) => _InitialData(obj),
|
||||||
.toList(growable: false);
|
() => TransientFailureException(
|
||||||
|
'Failed to retrieve initial data from the watch page, please report this to the project GitHub page.'));
|
||||||
var initialDataText = scriptText
|
|
||||||
.firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='));
|
|
||||||
if (initialDataText != null) {
|
|
||||||
return _initialData = _InitialData(json
|
|
||||||
.decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
|
|
||||||
}
|
|
||||||
|
|
||||||
initialDataText =
|
|
||||||
scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = '));
|
|
||||||
if (initialDataText != null) {
|
|
||||||
return _initialData = _InitialData(
|
|
||||||
json.decode(_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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
late final String xsfrToken = getXsfrToken()!;
|
late final String xsfrToken = getXsfrToken()!;
|
||||||
|
|
||||||
///
|
///
|
||||||
String? getXsfrToken() {
|
String? getXsfrToken() {
|
||||||
return _xsfrTokenExp
|
return _xsfrTokenExp.firstMatch(root.querySelectorAll('script').firstWhere((e) => _xsfrTokenExp.hasMatch(e.text)).text)?.group(1);
|
||||||
.firstMatch(root
|
|
||||||
.querySelectorAll('script')
|
|
||||||
.firstWhere((e) => _xsfrTokenExp.hasMatch(e.text))
|
|
||||||
.text)
|
|
||||||
?.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)?.group(1)?.stripNonDigits().nullIfWhitespace ??
|
||||||
.firstMatch(root.outerHtml)
|
root.querySelector('.like-button-renderer-like-button')?.text.stripNonDigits().nullIfWhitespace ??
|
||||||
?.group(1)
|
|
||||||
?.stripNonDigits()
|
|
||||||
.nullIfWhitespace ??
|
|
||||||
root
|
|
||||||
.querySelector('.like-button-renderer-like-button')
|
|
||||||
?.text
|
|
||||||
.stripNonDigits()
|
|
||||||
.nullIfWhitespace ??
|
|
||||||
'0');
|
'0');
|
||||||
|
|
||||||
///
|
///
|
||||||
int get videoDislikeCount => int.parse(_videoDislikeExp
|
int get videoDislikeCount => int.parse(_videoDislikeExp.firstMatch(root.outerHtml)?.group(1)?.stripNonDigits().nullIfWhitespace ??
|
||||||
.firstMatch(root.outerHtml)
|
root.querySelector('.like-button-renderer-dislike-button')?.text.stripNonDigits().nullIfWhitespace ??
|
||||||
?.group(1)
|
|
||||||
?.stripNonDigits()
|
|
||||||
.nullIfWhitespace ??
|
|
||||||
root
|
|
||||||
.querySelector('.like-button-renderer-dislike-button')
|
|
||||||
?.text
|
|
||||||
.stripNonDigits()
|
|
||||||
.nullIfWhitespace ??
|
|
||||||
'0');
|
'0');
|
||||||
|
|
||||||
static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\})');
|
static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\})');
|
||||||
|
|
||||||
late final WatchPlayerConfig playerConfig = WatchPlayerConfig(json.decode(
|
late final WatchPlayerConfig? playerConfig = getPlayerConfig();
|
||||||
_playerConfigExp
|
|
||||||
.firstMatch(root.getElementsByTagName('html').first.text)
|
|
||||||
?.group(1)
|
|
||||||
?.extractJson() ??
|
|
||||||
'a'));
|
|
||||||
|
|
||||||
late final PlayerResponse? playerResponse = getPlayerResponse();
|
late final PlayerResponse? playerResponse = getPlayerResponse();
|
||||||
|
|
||||||
|
///
|
||||||
|
WatchPlayerConfig? getPlayerConfig() {
|
||||||
|
final jsonMap = _playerConfigExp.firstMatch(root.getElementsByTagName('html').first.text)?.group(1)?.extractJson();
|
||||||
|
if (jsonMap == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return WatchPlayerConfig(jsonMap);
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
PlayerResponse? getPlayerResponse() {
|
PlayerResponse? getPlayerResponse() {
|
||||||
final val = root
|
final val = root
|
||||||
|
@ -147,18 +107,14 @@ class WatchPage {
|
||||||
if (val == null) {
|
if (val == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return PlayerResponse.parse(val);
|
return PlayerResponse(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _extractJson(String html, String separator) =>
|
|
||||||
html.substring(html.indexOf(separator) + separator.length).extractJson();
|
|
||||||
|
|
||||||
///
|
///
|
||||||
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) {
|
||||||
|
@ -167,9 +123,9 @@ class WatchPage {
|
||||||
var req = await httpClient.get(url, validate: true);
|
var req = await httpClient.get(url, validate: true);
|
||||||
|
|
||||||
var cookies = req.headers['set-cookie']!;
|
var cookies = req.headers['set-cookie']!;
|
||||||
var visitorInfoLive = _visitorInfoLiveExp.firstMatch(cookies)!.group(1)!;
|
var visitorInfoLive = _visitorInfoLiveExp.firstMatch(cookies)?.group(1)!;
|
||||||
var ysc = _yscExp.firstMatch(cookies)!.group(1)!;
|
var ysc = _yscExp.firstMatch(cookies)!.group(1)!;
|
||||||
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.');
|
||||||
|
@ -192,12 +148,10 @@ class WatchPlayerConfig implements PlayerConfigBase<Map<String, dynamic>> {
|
||||||
WatchPlayerConfig(this.root);
|
WatchPlayerConfig(this.root);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late final String sourceUrl =
|
late final String sourceUrl = 'https://youtube.com${root.get('assets')!.getT<String>('js')}';
|
||||||
'https://youtube.com${root.get('assets')!.getT<String>('js')}';
|
|
||||||
|
|
||||||
///
|
///
|
||||||
late final PlayerResponse playerResponse =
|
late final PlayerResponse playerResponse = PlayerResponse.parse(root.get('args')!.getT<String>('playerResponse')!);
|
||||||
PlayerResponse.parse(root.get('args')!.getT<String>('playerResponse')!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InitialData {
|
class _InitialData {
|
||||||
|
@ -223,9 +177,7 @@ class _InitialData {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
late final String continuation =
|
late final String continuation = getContinuationContext()?.getT<String>('continuation') ?? '';
|
||||||
getContinuationContext()?.getT<String>('continuation') ?? '';
|
|
||||||
|
|
||||||
late final String clickTrackingParams =
|
late final String clickTrackingParams = getContinuationContext()?.getT<String>('clickTrackingParams') ?? '';
|
||||||
getContinuationContext()?.getT<String>('clickTrackingParams') ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,8 @@ class StreamsClient {
|
||||||
/// Initializes an instance of [StreamsClient]
|
/// Initializes an instance of [StreamsClient]
|
||||||
StreamsClient(this._httpClient);
|
StreamsClient(this._httpClient);
|
||||||
|
|
||||||
Future<DashManifest> _getDashManifest(
|
Future<DashManifest> _getDashManifest(Uri dashManifestUrl, Iterable<CipherOperation> cipherOperations) {
|
||||||
Uri dashManifestUrl, Iterable<CipherOperation> cipherOperations) {
|
var signature = DashManifest.getSignatureFromUrl(dashManifestUrl.toString());
|
||||||
var signature =
|
|
||||||
DashManifest.getSignatureFromUrl(dashManifestUrl.toString());
|
|
||||||
if (!signature.isNullOrWhiteSpace) {
|
if (!signature.isNullOrWhiteSpace) {
|
||||||
signature = cipherOperations.decipher(signature!);
|
signature = cipherOperations.decipher(signature!);
|
||||||
dashManifestUrl = dashManifestUrl.setQueryParam('signature', signature);
|
dashManifestUrl = dashManifestUrl.setQueryParam('signature', signature);
|
||||||
|
@ -39,74 +37,56 @@ class StreamsClient {
|
||||||
throw VideoUnplayableException.unplayable(videoId);
|
throw VideoUnplayableException.unplayable(videoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerSource = await PlayerSource.get(
|
var playerSource = await PlayerSource.get(_httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl);
|
||||||
_httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl);
|
|
||||||
var cipherOperations = playerSource.getCipherOperations();
|
var cipherOperations = playerSource.getCipherOperations();
|
||||||
|
|
||||||
var videoInfoResponse = await VideoInfoResponse.get(
|
var videoInfoResponse = await VideoInfoResponse.get(_httpClient, videoId.toString(), playerSource.sts);
|
||||||
_httpClient, videoId.toString(), playerSource.sts);
|
|
||||||
var playerResponse = videoInfoResponse.playerResponse;
|
var playerResponse = videoInfoResponse.playerResponse;
|
||||||
|
|
||||||
var previewVideoId = playerResponse.previewVideoId;
|
var previewVideoId = playerResponse.previewVideoId;
|
||||||
if (!previewVideoId.isNullOrWhiteSpace) {
|
if (!previewVideoId.isNullOrWhiteSpace) {
|
||||||
throw VideoRequiresPurchaseException.preview(
|
throw VideoRequiresPurchaseException.preview(videoId, VideoId(previewVideoId!));
|
||||||
videoId, VideoId(previewVideoId!));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!playerResponse.isVideoPlayable) {
|
if (!playerResponse.isVideoPlayable) {
|
||||||
throw VideoUnplayableException.unplayable(videoId,
|
throw VideoUnplayableException.unplayable(videoId, reason: playerResponse.videoPlayabilityError ?? '');
|
||||||
reason: playerResponse.videoPlayabilityError ?? '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerResponse.isLive) {
|
if (playerResponse.isLive) {
|
||||||
throw VideoUnplayableException.liveStream(videoId);
|
throw VideoUnplayableException.liveStream(videoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var streamInfoProviders = <StreamInfoProvider>[
|
var streamInfoProviders = <StreamInfoProvider>[...videoInfoResponse.streams, ...playerResponse.streams];
|
||||||
...videoInfoResponse.streams,
|
|
||||||
...playerResponse.streams
|
|
||||||
];
|
|
||||||
|
|
||||||
var dashManifestUrl = playerResponse.dashManifestUrl;
|
var dashManifestUrl = playerResponse.dashManifestUrl;
|
||||||
if (!dashManifestUrl.isNullOrWhiteSpace) {
|
if (!dashManifestUrl.isNullOrWhiteSpace) {
|
||||||
var dashManifest =
|
var dashManifest = await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations);
|
||||||
await _getDashManifest(Uri.parse(dashManifestUrl!), cipherOperations);
|
|
||||||
streamInfoProviders.addAll(dashManifest.streams);
|
streamInfoProviders.addAll(dashManifest.streams);
|
||||||
}
|
}
|
||||||
return StreamContext(streamInfoProviders, cipherOperations);
|
return StreamContext(streamInfoProviders, cipherOperations);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<StreamContext> _getStreamContextFromWatchPage(VideoId videoId) async {
|
Future<StreamContext> _getStreamContextFromWatchPage(VideoId videoId) async {
|
||||||
var watchPage = await WatchPage.get(_httpClient, videoId.toString());
|
final watchPage = await WatchPage.get(_httpClient, videoId.toString());
|
||||||
|
|
||||||
WatchPlayerConfig? playerConfig;
|
final playerConfig = watchPage.playerConfig;
|
||||||
try {
|
|
||||||
playerConfig = watchPage.playerConfig;
|
var playerResponse = playerConfig?.playerResponse ?? watchPage.playerResponse;
|
||||||
} on FormatException {
|
|
||||||
playerConfig = null;
|
|
||||||
}
|
|
||||||
var playerResponse =
|
|
||||||
playerConfig?.playerResponse ?? watchPage.playerResponse;
|
|
||||||
if (playerResponse == null) {
|
if (playerResponse == null) {
|
||||||
throw VideoUnplayableException.unplayable(videoId);
|
throw VideoUnplayableException.unplayable(videoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var previewVideoId = playerResponse.previewVideoId;
|
var previewVideoId = playerResponse.previewVideoId;
|
||||||
if (!previewVideoId.isNullOrWhiteSpace) {
|
if (!previewVideoId.isNullOrWhiteSpace) {
|
||||||
throw VideoRequiresPurchaseException.preview(
|
throw VideoRequiresPurchaseException.preview(videoId, VideoId(previewVideoId!));
|
||||||
videoId, VideoId(previewVideoId!));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerSourceUrl = watchPage.sourceUrl ?? playerConfig?.sourceUrl;
|
var playerSourceUrl = watchPage.sourceUrl ?? playerConfig?.sourceUrl;
|
||||||
var playerSource = !playerSourceUrl.isNullOrWhiteSpace
|
var playerSource = !playerSourceUrl.isNullOrWhiteSpace ? await PlayerSource.get(_httpClient, playerSourceUrl!) : null;
|
||||||
? await PlayerSource.get(_httpClient, playerSourceUrl!)
|
var cipherOperations = playerSource?.getCipherOperations() ?? const <CipherOperation>[];
|
||||||
: null;
|
|
||||||
var cipherOperations =
|
|
||||||
playerSource?.getCipherOperations() ?? const <CipherOperation>[];
|
|
||||||
|
|
||||||
if (!playerResponse.isVideoPlayable) {
|
if (!playerResponse.isVideoPlayable) {
|
||||||
throw VideoUnplayableException.unplayable(videoId,
|
throw VideoUnplayableException.unplayable(videoId, reason: playerResponse.videoPlayabilityError ?? '');
|
||||||
reason: playerResponse.videoPlayabilityError ?? '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerResponse.isLive) {
|
if (playerResponse.isLive) {
|
||||||
|
@ -119,8 +99,7 @@ class StreamsClient {
|
||||||
|
|
||||||
var dashManifestUrl = playerResponse.dashManifestUrl;
|
var dashManifestUrl = playerResponse.dashManifestUrl;
|
||||||
if (!(dashManifestUrl?.isNullOrWhiteSpace ?? true)) {
|
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);
|
||||||
}
|
}
|
||||||
return StreamContext(streamInfoProviders, cipherOperations);
|
return StreamContext(streamInfoProviders, cipherOperations);
|
||||||
|
@ -144,9 +123,7 @@ class StreamsClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content length
|
// Content length
|
||||||
var contentLength = streamInfo.contentLength ??
|
var contentLength = streamInfo.contentLength ?? await _httpClient.getContentLength(url, validate: false) ?? 0;
|
||||||
await _httpClient.getContentLength(url, validate: false) ??
|
|
||||||
0;
|
|
||||||
|
|
||||||
if (contentLength <= 0) {
|
if (contentLength <= 0) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -163,53 +140,31 @@ class StreamsClient {
|
||||||
// Muxed or Video-only
|
// Muxed or Video-only
|
||||||
if (!videoCodec.isNullOrWhiteSpace) {
|
if (!videoCodec.isNullOrWhiteSpace) {
|
||||||
var framerate = Framerate(streamInfo.framerate ?? 24);
|
var framerate = Framerate(streamInfo.framerate ?? 24);
|
||||||
var videoQualityLabel = streamInfo.videoQualityLabel ??
|
var videoQualityLabel =
|
||||||
VideoQualityUtil.getLabelFromTagWithFramerate(
|
streamInfo.videoQualityLabel ?? VideoQualityUtil.getLabelFromTagWithFramerate(tag, framerate.framesPerSecond.toDouble());
|
||||||
tag, framerate.framesPerSecond.toDouble());
|
|
||||||
|
|
||||||
var videoQuality = VideoQualityUtil.fromLabel(videoQualityLabel);
|
var videoQuality = VideoQualityUtil.fromLabel(videoQualityLabel);
|
||||||
|
|
||||||
var videoWidth = streamInfo.videoWidth;
|
var videoWidth = streamInfo.videoWidth;
|
||||||
var videoHeight = streamInfo.videoHeight;
|
var videoHeight = streamInfo.videoHeight;
|
||||||
var videoResolution = videoWidth != -1 && videoHeight != -1
|
var videoResolution =
|
||||||
? VideoResolution(videoWidth ?? 0, videoHeight ?? 0)
|
videoWidth != -1 && videoHeight != -1 ? VideoResolution(videoWidth ?? 0, videoHeight ?? 0) : videoQuality.toVideoResolution();
|
||||||
: videoQuality.toVideoResolution();
|
|
||||||
|
|
||||||
// Muxed
|
// Muxed
|
||||||
if (!audioCodec.isNullOrWhiteSpace) {
|
if (!audioCodec.isNullOrWhiteSpace) {
|
||||||
streams[tag] = MuxedStreamInfo(
|
streams[tag] = MuxedStreamInfo(tag, url, container, fileSize, bitrate, audioCodec!, videoCodec!, videoQualityLabel, videoQuality,
|
||||||
tag,
|
videoResolution, framerate);
|
||||||
url,
|
|
||||||
container,
|
|
||||||
fileSize,
|
|
||||||
bitrate,
|
|
||||||
audioCodec!,
|
|
||||||
videoCodec!,
|
|
||||||
videoQualityLabel,
|
|
||||||
videoQuality,
|
|
||||||
videoResolution,
|
|
||||||
framerate);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video only
|
// Video only
|
||||||
streams[tag] = VideoOnlyStreamInfo(
|
streams[tag] = VideoOnlyStreamInfo(
|
||||||
tag,
|
tag, url, container, fileSize, bitrate, videoCodec!, videoQualityLabel, videoQuality, videoResolution, framerate);
|
||||||
url,
|
|
||||||
container,
|
|
||||||
fileSize,
|
|
||||||
bitrate,
|
|
||||||
videoCodec!,
|
|
||||||
videoQualityLabel,
|
|
||||||
videoQuality,
|
|
||||||
videoResolution,
|
|
||||||
framerate);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Audio-only
|
// Audio-only
|
||||||
if (!audioCodec.isNullOrWhiteSpace) {
|
if (!audioCodec.isNullOrWhiteSpace) {
|
||||||
streams[tag] = AudioOnlyStreamInfo(
|
streams[tag] = AudioOnlyStreamInfo(tag, url, container, fileSize, bitrate, audioCodec!);
|
||||||
tag, url, container, fileSize, bitrate, audioCodec!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #if DEBUG
|
// #if DEBUG
|
||||||
|
@ -239,12 +194,10 @@ class StreamsClient {
|
||||||
/// Gets the HTTP Live Stream (HLS) manifest URL
|
/// Gets the HTTP Live Stream (HLS) manifest URL
|
||||||
/// for the specified video (if it's a live video stream).
|
/// for the specified video (if it's a live video stream).
|
||||||
Future<String> getHttpLiveStreamUrl(VideoId videoId) async {
|
Future<String> getHttpLiveStreamUrl(VideoId videoId) async {
|
||||||
var videoInfoResponse =
|
var videoInfoResponse = await VideoInfoResponse.get(_httpClient, videoId.toString());
|
||||||
await VideoInfoResponse.get(_httpClient, videoId.toString());
|
|
||||||
var playerResponse = videoInfoResponse.playerResponse;
|
var playerResponse = videoInfoResponse.playerResponse;
|
||||||
if (!playerResponse.isVideoPlayable) {
|
if (!playerResponse.isVideoPlayable) {
|
||||||
throw VideoUnplayableException.unplayable(videoId,
|
throw VideoUnplayableException.unplayable(videoId, reason: playerResponse.videoPlayabilityError ?? '');
|
||||||
reason: playerResponse.videoPlayabilityError ?? '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var hlsManifest = playerResponse.hlsManifestUrl;
|
var hlsManifest = playerResponse.hlsManifestUrl;
|
||||||
|
@ -255,6 +208,5 @@ class StreamsClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the actual stream which is identified by the specified metadata.
|
/// Gets the actual stream which is identified by the specified metadata.
|
||||||
Stream<List<int>> get(StreamInfo streamInfo) =>
|
Stream<List<int>> get(StreamInfo streamInfo) => _httpClient.getStream(streamInfo);
|
||||||
_httpClient.getStream(streamInfo);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.9.0-nullsafety.3
|
version: 1.9.0-nullsafety.4
|
||||||
|
|
||||||
homepage: https://github.com/Hexer10/youtube_explode_dart
|
homepage: https://github.com/Hexer10/youtube_explode_dart
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ void main() {
|
||||||
inInclusiveRange(rangeMs - 86400000, rangeMs + 86400000));
|
inInclusiveRange(rangeMs - 86400000, rangeMs + 86400000));
|
||||||
expect(video.description, contains('246pp'));
|
expect(video.description, contains('246pp'));
|
||||||
// Should be 1:38 but sometimes it differs
|
// Should be 1:38 but sometimes it differs
|
||||||
// so where using a 10 seconds range from it.
|
// so we're using a 10 seconds range from it.
|
||||||
expect(video.duration!.inSeconds, inInclusiveRange(108, 118));
|
expect(video.duration!.inSeconds, inInclusiveRange(108, 118));
|
||||||
expect(video.thumbnails.lowResUrl, isNotEmpty);
|
expect(video.thumbnails.lowResUrl, isNotEmpty);
|
||||||
expect(video.thumbnails.mediumResUrl, isNotEmpty);
|
expect(video.thumbnails.mediumResUrl, isNotEmpty);
|
||||||
|
|
Loading…
Reference in New Issue