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-18 22:22:34 +01:00
|
|
|
final scriptText = root!.querySelectorAll('script').map((e) => e.text).toList(growable: false);
|
2021-03-04 10:46:37 +01:00
|
|
|
|
2021-03-18 22:22:34 +01:00
|
|
|
return scriptText.extractGenericData(
|
|
|
|
(obj) => _InitialData(obj),
|
|
|
|
() => TransientFailureException(
|
|
|
|
'Failed to retrieve initial data from the search page, please report this to the project GitHub page.'));
|
2021-03-04 10:46:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
///
|
2021-03-18 22:22:34 +01:00
|
|
|
PlaylistPage(this.root, this.playlistId, [_InitialData? initialData]) : _initialData = initialData;
|
2021-03-04 10:46:37 +01:00
|
|
|
|
|
|
|
///
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
///
|
2021-03-18 22:22:34 +01:00
|
|
|
static Future<PlaylistPage> get(YoutubeHttpClient httpClient, String id, {String? token}) {
|
2021-03-04 10:46:37 +01:00
|
|
|
if (token != null && token.isNotEmpty) {
|
2021-03-18 22:22:34 +01:00
|
|
|
var url = 'https://www.youtube.com/youtubei/v1/guide?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
2021-03-04 10:46:37 +01:00
|
|
|
|
|
|
|
return retry(() async {
|
|
|
|
var body = {
|
|
|
|
'context': const {
|
2021-03-18 22:22:34 +01:00
|
|
|
'client': {'hl': 'en', 'clientName': 'WEB', 'clientVersion': '2.20200911.04.00'}
|
2021-03-04 10:46:37 +01:00
|
|
|
},
|
|
|
|
'continuation': token
|
|
|
|
};
|
|
|
|
|
2021-03-18 22:22:34 +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-18 22:22:34 +01:00
|
|
|
late final String? title = root.get('metadata')?.get('playlistMetadataRenderer')?.getT<String>('title');
|
2021-03-04 10:46:37 +01:00
|
|
|
|
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-18 22:22:34 +01:00
|
|
|
late final String? description = root.get('metadata')?.get('playlistMetadataRenderer')?.getT<String>('description');
|
2021-03-04 10:46:37 +01:00
|
|
|
|
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-18 22:22:34 +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') ??
|
2021-03-18 22:22:34 +01:00
|
|
|
root.getList('onResponseReceivedActions')?.firstOrNull?.get('appendContinuationItemsAction')?.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') ??
|
2021-03-18 22:22:34 +01:00
|
|
|
root.getList('onResponseReceivedCommands')?.firstOrNull?.get('appendContinuationItemsAction')?.getList('continuationItems');
|
2021-03-04 10:46:37 +01:00
|
|
|
|
|
|
|
List<_Video> get playlistVideos =>
|
2021-03-18 22:22:34 +01:00
|
|
|
playlistVideosContent?.where((e) => e['playlistVideoRenderer'] != null).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 =>
|
2021-03-18 22:22:34 +01:00
|
|
|
root.get('ownerText')?.getList('runs')?.firstOrNull?.get('navigationEndpoint')?.get('browseEndpoint')?.getT<String>('browseId') ??
|
2021-03-04 10:46:37 +01:00
|
|
|
root
|
|
|
|
.get('shortBylineText')
|
|
|
|
?.getList('runs')
|
|
|
|
?.firstOrNull
|
|
|
|
?.get('navigationEndpoint')
|
|
|
|
?.get('browseEndpoint')
|
|
|
|
?.getT<String>('browseId') ??
|
|
|
|
'';
|
|
|
|
|
|
|
|
String get title => root.get('title')?.getList('runs')?.parseRuns() ?? '';
|
|
|
|
|
2021-03-18 22:22:34 +01:00
|
|
|
String get description => root.getList('descriptionSnippet')?.parseRuns() ?? '';
|
2021-03-04 10:46:37 +01:00
|
|
|
|
2021-03-18 22:22:34 +01:00
|
|
|
Duration? get duration => _stringToDuration(root.get('lengthText')?.getT<String>('simpleText'));
|
2021-03-04 10:46:37 +01:00
|
|
|
|
2021-03-18 22:22:34 +01:00
|
|
|
int get viewCount => root.get('viewCountText')?.getT<String>('simpleText')?.parseInt() ?? 0;
|
2021-03-04 10:46:37 +01:00
|
|
|
|
|
|
|
/// 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) {
|
2021-03-18 22:22:34 +01:00
|
|
|
return Duration(minutes: int.parse(parts.first), seconds: int.parse(parts[1]));
|
2021-03-04 10:46:37 +01:00
|
|
|
}
|
|
|
|
if (parts.length == 3) {
|
2021-03-18 22:22:34 +01:00
|
|
|
return Duration(hours: int.parse(parts[0]), minutes: int.parse(parts[1]), seconds: int.parse(parts[2]));
|
2021-03-04 10:46:37 +01:00
|
|
|
}
|
|
|
|
throw Error();
|
|
|
|
}
|
|
|
|
}
|