youtube_explode/lib/src/reverse_engineering/responses/playlist_page.dart

291 lines
8.2 KiB
Dart
Raw Normal View History

2021-03-04 10:46:37 +01:00
import 'dart:convert';
2021-03-11 14:20:10 +01:00
import 'package:collection/collection.dart';
2021-03-04 10:46:37 +01:00
import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser;
import '../../../youtube_explode_dart.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../youtube_http_client.dart';
///
class PlaylistPage {
///
final String playlistId;
2021-03-11 14:20:10 +01:00
final Document? root;
2021-03-04 10:46:37 +01:00
2021-03-11 14:20:10 +01:00
late final _InitialData initialData = getInitialData();
_InitialData? _initialData;
2021-03-04 10:46:37 +01:00
///
2021-03-11 14:20:10 +01:00
_InitialData getInitialData() {
2021-03-04 10:46:37 +01:00
if (_initialData != null) {
2021-03-11 14:20:10 +01:00
return _initialData!;
2021-03-04 10:46:37 +01:00
}
2021-03-11 14:20:10 +01:00
final scriptText = root!
2021-03-04 10:46:37 +01:00
.querySelectorAll('script')
.map((e) => e.text)
.toList(growable: false);
2021-03-11 14:20:10 +01:00
var initialDataText = scriptText
.firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='));
2021-03-04 10:46:37 +01:00
if (initialDataText != null) {
2021-03-11 14:20:10 +01:00
return _InitialData(json
2021-03-04 10:46:37 +01:00
.decode(_extractJson(initialDataText, 'window["ytInitialData"] =')));
}
2021-03-11 14:20:10 +01:00
initialDataText =
scriptText.firstWhereOrNull((e) => e.contains('var ytInitialData = '));
2021-03-04 10:46:37 +01:00
if (initialDataText != null) {
2021-03-11 14:20:10 +01:00
return _InitialData(
2021-03-04 10:46:37 +01:00
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) {
2021-03-11 14:20:10 +01:00
throw TransientFailureException(
'Failed to retrieve initial data from the search page, please report this to the project GitHub page. Couldn\'t extract json: $html');
2021-03-04 10:46:37 +01:00
}
return _matchJson(html.substring(index));
}
String _matchJson(String str) {
var bracketCount = 0;
2021-03-11 14:20:10 +01:00
var lastI = 0;
2021-03-04 10:46:37 +01:00
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);
}
///
2021-03-11 14:20:10 +01:00
PlaylistPage(this.root, this.playlistId, [_InitialData? initialData])
2021-03-04 10:46:37 +01:00
: _initialData = initialData;
///
2021-03-11 14:20:10 +01:00
Future<PlaylistPage?> nextPage(YoutubeHttpClient httpClient) async {
2021-03-04 10:46:37 +01:00
if (initialData.continuationToken == null) {
return null;
}
return get(httpClient, playlistId, token: initialData.continuationToken);
}
///
static Future<PlaylistPage> get(YoutubeHttpClient httpClient, String id,
2021-03-11 14:20:10 +01:00
{String? token}) {
2021-03-04 10:46:37 +01:00
if (token != null && token.isNotEmpty) {
var url =
'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
return retry(() async {
var body = {
'context': const {
'client': {
'hl': 'en',
'clientName': 'WEB',
'clientVersion': '2.20200911.04.00'
}
},
'continuation': token
};
2021-03-11 14:20:10 +01:00
var raw =
await httpClient.post(Uri.parse(url), body: json.encode(body));
2021-03-04 10:46:37 +01:00
return PlaylistPage(null, id, _InitialData(json.decode(raw.body)));
});
// Ask for next page,
}
var url = 'https://www.youtube.com/playlist?list=$id&hl=en&persist_hl=1';
return retry(() async {
var raw = await httpClient.getString(url);
return PlaylistPage.parse(raw, id);
});
// ask for next page
}
///
2021-03-11 14:20:10 +01:00
PlaylistPage.parse(String raw, this.playlistId) : root = parser.parse(raw);
2021-03-04 10:46:37 +01:00
}
class _InitialData {
// Json parsed map
final Map<String, dynamic> root;
_InitialData(this.root);
2021-03-11 14:20:10 +01:00
late final String? title = root
.get('metadata')
2021-03-04 10:46:37 +01:00
?.get('playlistMetadataRenderer')
?.getT<String>('title');
2021-03-11 14:20:10 +01:00
late final String? author = root
2021-03-04 10:46:37 +01:00
.get('sidebar')
?.get('playlistSidebarRenderer')
?.getList('items')
?.elementAtSafe(1)
?.get('playlistSidebarSecondaryInfoRenderer')
?.get('videoOwner')
?.get('videoOwnerRenderer')
?.get('title')
?.getT<List<dynamic>>('runs')
?.parseRuns();
2021-03-11 14:20:10 +01:00
late final String? description = root
.get('metadata')
2021-03-04 10:46:37 +01:00
?.get('playlistMetadataRenderer')
?.getT<String>('description');
2021-03-11 14:20:10 +01:00
late final int? viewCount = root
.get('sidebar')
2021-03-04 10:46:37 +01:00
?.get('playlistSidebarRenderer')
?.getList('items')
?.firstOrNull
?.get('playlistSidebarPrimaryInfoRenderer')
?.getList('stats')
?.elementAtSafe(1)
?.getT<String>('simpleText')
?.parseInt();
2021-03-11 14:20:10 +01:00
late final String? continuationToken =
(videosContent ?? playlistVideosContent)
?.firstWhereOrNull((e) => e['continuationItemRenderer'] != null)
?.get('continuationItemRenderer')
?.get('continuationEndpoint')
?.get('continuationCommand')
?.getT<String>('token');
2021-03-04 10:46:37 +01:00
2021-03-11 14:20:10 +01:00
List<Map<String, dynamic>>? get playlistVideosContent =>
2021-03-04 10:46:37 +01:00
root
.get('contents')
?.get('twoColumnBrowseResultsRenderer')
?.getList('tabs')
?.firstOrNull
?.get('tabRenderer')
?.get('content')
?.get('sectionListRenderer')
?.getList('contents')
?.firstOrNull
?.get('itemSectionRenderer')
?.getList('contents')
?.firstOrNull
?.get('playlistVideoListRenderer')
?.getList('contents') ??
root
.getList('onResponseReceivedActions')
?.firstOrNull
?.get('appendContinuationItemsAction')
2021-03-11 14:20:10 +01:00
?.getList('continuationItems');
2021-03-04 10:46:37 +01:00
2021-03-11 14:20:10 +01:00
late final List<Map<String, dynamic>>? videosContent = root
2021-03-04 10:46:37 +01:00
.get('contents')
?.get('twoColumnSearchResultsRenderer')
?.get('primaryContents')
?.get('sectionListRenderer')
?.getList('contents') ??
root
2021-03-11 14:20:10 +01:00
.getList('onResponseReceivedCommands')
2021-03-04 10:46:37 +01:00
?.firstOrNull
?.get('appendContinuationItemsAction')
2021-03-11 14:20:10 +01:00
?.getList('continuationItems');
2021-03-04 10:46:37 +01:00
List<_Video> get playlistVideos =>
playlistVideosContent
?.where((e) => e['playlistVideoRenderer'] != null)
2021-03-11 14:20:10 +01:00
.map((e) => _Video(e['playlistVideoRenderer']))
.toList() ??
2021-03-04 10:46:37 +01:00
const [];
List<_Video> get videos =>
videosContent?.firstOrNull
?.get('itemSectionRenderer')
?.getList('contents')
?.where((e) => e['videoRenderer'] != null)
2021-03-11 14:20:10 +01:00
.map((e) => _Video(e))
.toList() ??
2021-03-04 10:46:37 +01:00
const [];
}
class _Video {
// Json parsed map
final Map<String, dynamic> root;
_Video(this.root);
2021-03-11 14:20:10 +01:00
String get id => root.getT<String>('videoId')!;
2021-03-04 10:46:37 +01:00
String get author =>
2021-03-11 14:20:10 +01:00
root.get('ownerText')?.getT<List<dynamic>>('runs')?.parseRuns() ??
root.get('shortBylineText')?.getT<List<dynamic>>('runs')?.parseRuns() ??
2021-03-04 10:46:37 +01:00
'';
String get channelId =>
root
.get('ownerText')
?.getList('runs')
?.firstOrNull
?.get('navigationEndpoint')
?.get('browseEndpoint')
?.getT<String>('browseId') ??
root
.get('shortBylineText')
?.getList('runs')
?.firstOrNull
?.get('navigationEndpoint')
?.get('browseEndpoint')
?.getT<String>('browseId') ??
'';
String get title => root.get('title')?.getList('runs')?.parseRuns() ?? '';
String get description =>
root.getList('descriptionSnippet')?.parseRuns() ?? '';
2021-03-11 14:20:10 +01:00
Duration? get duration =>
2021-03-04 10:46:37 +01:00
_stringToDuration(root.get('lengthText')?.getT<String>('simpleText'));
int get viewCount =>
root.get('viewCountText')?.getT<String>('simpleText')?.parseInt() ?? 0;
/// Format: HH:MM:SS
2021-03-11 14:20:10 +01:00
static Duration? _stringToDuration(String? string) {
2021-03-04 10:46:37 +01:00
if (string == null || string.trim().isEmpty) {
return null;
}
var parts = string.split(':');
assert(parts.length <= 3);
if (parts.length == 1) {
return Duration(seconds: int.parse(parts.first));
}
if (parts.length == 2) {
return Duration(
minutes: int.parse(parts.first), seconds: int.parse(parts[1]));
}
if (parts.length == 3) {
return Duration(
hours: int.parse(parts[0]),
minutes: int.parse(parts[1]),
seconds: int.parse(parts[2]));
}
throw Error();
}
}