From de6caf294918310f1ed6790c890705ca23f6dccc Mon Sep 17 00:00:00 2001 From: Hexah Date: Fri, 10 Jul 2020 22:28:19 +0200 Subject: [PATCH] First implement of #40 --- lib/src/channels/channel_video.dart | 20 +++ lib/src/channels/channels.dart | 1 + .../responses/channel_upload_page.dart | 140 ++++++++++++++++++ lib/src/search/search_playlist.dart | 7 +- lib/src/search/search_video.dart | 4 +- 5 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 lib/src/channels/channel_video.dart create mode 100644 lib/src/reverse_engineering/responses/channel_upload_page.dart diff --git a/lib/src/channels/channel_video.dart b/lib/src/channels/channel_video.dart new file mode 100644 index 0000000..fbb8014 --- /dev/null +++ b/lib/src/channels/channel_video.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; +import 'package:youtube_explode_dart/src/videos/video_id.dart'; + +/// Metadata related to a search query result (playlist) +class ChannelVideo with EquatableMixin { + /// Video ID. + final VideoId videoId; + + /// Video title. + final String videoTitle; + + /// Initialize an instance of [ChannelVideo] + ChannelVideo(this.videoId, this.videoTitle); + + @override + String toString() => '(ChannelVideo) $videoId ($videoTitle)'; + + @override + List get props => [videoId]; +} diff --git a/lib/src/channels/channels.dart b/lib/src/channels/channels.dart index 488019d..a6abca9 100644 --- a/lib/src/channels/channels.dart +++ b/lib/src/channels/channels.dart @@ -3,4 +3,5 @@ library youtube_explode.channels; export 'channel.dart'; export 'channel_client.dart'; export 'channel_id.dart'; +export 'channel_video.dart'; export 'username.dart'; diff --git a/lib/src/reverse_engineering/responses/channel_upload_page.dart b/lib/src/reverse_engineering/responses/channel_upload_page.dart new file mode 100644 index 0000000..a94798a --- /dev/null +++ b/lib/src/reverse_engineering/responses/channel_upload_page.dart @@ -0,0 +1,140 @@ +import 'dart:convert'; + +import 'package:html/dom.dart'; +import 'package:html/parser.dart' as parser; + +import '../../channels/channel_video.dart'; +import '../../extensions/helpers_extension.dart'; +import '../../retry.dart'; +import '../../videos/videos.dart'; +import '../youtube_http_client.dart'; + +class ChannelWatchPage { + final String channelId; + final Document _root; + + _InitialData _initialData; + + _InitialData get initialData => + _initialData ??= _InitialData(json.decode(_matchJson(_extractJson( + _root + .querySelectorAll('script') + .map((e) => e.text) + .toList() + .firstWhere((e) => e.contains('window["ytInitialData"] =')), + 'window["ytInitialData"] =')))); + + String _extractJson(String html, String separator) { + return _matchJson( + html.substring(html.indexOf(separator) + separator.length)); + } + + String _matchJson(String str) { + var bracketCount = 0; + 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); + } + + ChannelWatchPage(this._root, this.channelId); + + Future nextPage() {} + + static Future get( + YoutubeHttpClient httpClient, String channelId) { + var url = + 'https://www.youtube.com/channel/$channelId/videos?view=0&sort=dd&flow=grid'; + return retry(() async { + var raw = await httpClient.getString(url); + return ChannelWatchPage.parse(raw, channelId); + }); + } + + ChannelWatchPage.parse(String raw, this.channelId) + : _root = parser.parse(raw); +} + +class _InitialData { + // Json parsed map + final Map _root; + + _InitialData(this._root); + + /* Cache results */ + + List _uploads; + String _continuation; + String _clickTrackingParams; + + List> getContentContext(Map root) { + if (root['contents'] != null) { + return (_root['contents']['twoColumnBrowseResultsRenderer']['tabs'] + as List) + .map((e) => e['tabRenderer']) + .firstWhere((e) => e['selected'] == true)['content'] + ['sectionListRenderer']['contents'] + .first['itemSectionRenderer']['contents'] + .first['gridRenderer']['items'] + .cast>(); + ; + } + if (root['response'] != null) { + return _root['response']['continuationContents']['gridContinuation'] + ['items'] + .cast>(); + } + throw Exception('Couldn\'t find the content data'); + } + + Map getContinuationContext(Map root) { + if (_root['contents'] != null) { + return (_root['contents']['twoColumnBrowseResultsRenderer']['tabs'] + as List) + ?.map((e) => e['tabRenderer']) + ?.firstWhere((e) => e['selected'] == true)['content'] + ['sectionListRenderer']['contents'] + ?.first['itemSectionRenderer']['contents'] + ?.first['gridRenderer']['continuations'] + ?.first['nextContinuationData'] + ?.cast(); + } + if (_root['response'] != null) { + return _root['response']['continuationContents']['gridContinuation'] + ['continuations'] + ?.first + ?.cast(); + } + return null; + } + + List get uploads => _uploads ??= getContentContext(_root) + ?.map(_parseContent) + ?.where((e) => e != null) + ?.toList(); + + String get continuation => _continuation ??= + getContinuationContext(_root)?.getValue('continuation') ?? ''; + + String get clickTrackingParams => _clickTrackingParams ??= + getContinuationContext(_root)?.getValue('clickTrackingParams') ?? ''; + + dynamic _parseContent(content) { + if (content == null || content['gridVideoRenderer'] == null) { + return null; + } + var video = content['gridVideoRenderer'] as Map; + return ChannelVideo( + VideoId(video['videoId']), video['title']['simpleText']); + } +} diff --git a/lib/src/search/search_playlist.dart b/lib/src/search/search_playlist.dart index c86066f..cf8e625 100644 --- a/lib/src/search/search_playlist.dart +++ b/lib/src/search/search_playlist.dart @@ -1,7 +1,9 @@ +import 'package:equatable/equatable.dart'; + import '../playlists/playlist_id.dart'; /// Metadata related to a search query result (playlist) -class SearchPlaylist { +class SearchPlaylist with EquatableMixin { /// PlaylistId. final PlaylistId playlistId; @@ -16,4 +18,7 @@ class SearchPlaylist { @override String toString() => '(Playlist) $playlistTitle ($playlistId)'; + + @override + List get props => [playlistId]; } diff --git a/lib/src/search/search_video.dart b/lib/src/search/search_video.dart index a4c06f7..ba5e5b2 100644 --- a/lib/src/search/search_video.dart +++ b/lib/src/search/search_video.dart @@ -20,8 +20,8 @@ class SearchVideo { /// Video View Count final int videoViewCount; - /// Initialize a [RelatedQuery] instance. - SearchVideo(this.videoId, this.videoTitle, this.videoAuthor, + /// Initialize a [SearchVideo] instance. + const SearchVideo(this.videoId, this.videoTitle, this.videoAuthor, this.videoDescriptionSnippet, this.videoDuration, this.videoViewCount); @override