From 5f5da821a9f139eba572d00cc762db65423d7a85 Mon Sep 17 00:00:00 2001 From: "Michael J. Miller" Date: Fri, 18 Sep 2020 02:12:41 -0600 Subject: [PATCH 1/2] channel description --- lib/src/channels/channel.dart | 5 +- lib/src/channels/channel_about.dart | 13 ++ lib/src/channels/channel_client.dart | 10 +- .../responses/channel_about_page.dart | 113 ++++++++++++++++++ 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 lib/src/channels/channel_about.dart create mode 100644 lib/src/reverse_engineering/responses/channel_about_page.dart diff --git a/lib/src/channels/channel.dart b/lib/src/channels/channel.dart index 8c47ba1..dbaaf4c 100644 --- a/lib/src/channels/channel.dart +++ b/lib/src/channels/channel.dart @@ -13,11 +13,14 @@ class Channel with EquatableMixin { /// Channel title. final String title; + /// Channel description + final String description; + /// URL of the channel's logo image. final String logoUrl; /// Initializes an instance of [Channel] - Channel(this.id, this.title, this.logoUrl); + Channel(this.id, this.title, this.description, this.logoUrl); @override String toString() => 'Channel ($title)'; diff --git a/lib/src/channels/channel_about.dart b/lib/src/channels/channel_about.dart new file mode 100644 index 0000000..f27c6f6 --- /dev/null +++ b/lib/src/channels/channel_about.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; + +/// YouTube channel about metadata. +class ChannelAbout with EquatableMixin { + /// Channel description. + final String description; + + /// Initializes an instance of [ChannelAbout] + ChannelAbout(this.description); + + @override + List get props => [description]; +} diff --git a/lib/src/channels/channel_client.dart b/lib/src/channels/channel_client.dart index 8ad79eb..b47d0f0 100644 --- a/lib/src/channels/channel_client.dart +++ b/lib/src/channels/channel_client.dart @@ -1,5 +1,6 @@ import '../extensions/helpers_extension.dart'; import '../playlists/playlists.dart'; +import '../reverse_engineering/responses/channel_about_page.dart'; import '../reverse_engineering/responses/channel_upload_page.dart'; import '../reverse_engineering/responses/responses.dart'; import '../reverse_engineering/youtube_http_client.dart'; @@ -24,8 +25,10 @@ class ChannelClient { Future get(dynamic id) async { id = ChannelId.fromString(id); var channelPage = await ChannelPage.get(_httpClient, id.value); + var channelAboutPage = await ChannelAboutPage.get(_httpClient, id.value); - return Channel(id, channelPage.channelTitle, channelPage.channelLogoUrl); + return Channel(id, channelPage.channelTitle, channelAboutPage.description, + channelPage.channelLogoUrl); } /// Gets the metadata associated with the channel of the specified user. @@ -36,8 +39,11 @@ class ChannelClient { var channelPage = await ChannelPage.getByUsername(_httpClient, username.value); + var channelAboutPage = + await ChannelAboutPage.getByUsername(_httpClient, username.value); + return Channel(ChannelId(channelPage.channelId), channelPage.channelTitle, - channelPage.channelLogoUrl); + channelAboutPage.description, channelPage.channelLogoUrl); } /// Gets the metadata associated with the channel diff --git a/lib/src/reverse_engineering/responses/channel_about_page.dart b/lib/src/reverse_engineering/responses/channel_about_page.dart new file mode 100644 index 0000000..9278d9a --- /dev/null +++ b/lib/src/reverse_engineering/responses/channel_about_page.dart @@ -0,0 +1,113 @@ +import 'dart:convert'; + +import 'package:html/dom.dart'; +import 'package:html/parser.dart' as parser; + +import '../../exceptions/exceptions.dart'; +import '../../extensions/helpers_extension.dart'; +import '../../retry.dart'; +import '../youtube_http_client.dart'; + +/// +class ChannelAboutPage { + 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"] =')))); + + /// + bool get isOk => initialData != null; + + /// + 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; + 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.parse(String raw) : _root = parser.parse(raw); + + /// + static Future get(YoutubeHttpClient httpClient, String id) { + var url = 'https://www.youtube.com/channel/$id/about?hl=en'; + + return retry(() async { + var raw = await httpClient.getString(url); + var result = ChannelAboutPage.parse(raw); + + if (!result.isOk) { + throw TransientFailureException('Channel about page is broken'); + } + return result; + }); + } + + /// + static Future getByUsername( + YoutubeHttpClient httpClient, String username) { + var url = 'https://www.youtube.com/user/$username/about?hl=en'; + + return retry(() async { + var raw = await httpClient.getString(url); + var result = ChannelAboutPage.parse(raw); + + if (!result.isOk) { + throw TransientFailureException('Channel about page is broken'); + } + return result; + }); + } +} + +class _InitialData { + // Json parsed map + final Map root; + + _InitialData(this.root); + + /* Cache results */ + + String _description; + + Map getDescriptionContext(Map root) { + if (root['metadata'] != null) { + return root['metadata']['channelMetadataRenderer']; + } + return null; + } + + String get description => _description ??= + getDescriptionContext(root)?.getValue('description') ?? ''; +} From 08c51163baf472e9c2a98780ba9f453c86d35915 Mon Sep 17 00:00:00 2001 From: "Michael J. Miller" Date: Fri, 18 Sep 2020 02:13:56 -0600 Subject: [PATCH 2/2] cleanup --- lib/src/channels/channel_about.dart | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 lib/src/channels/channel_about.dart diff --git a/lib/src/channels/channel_about.dart b/lib/src/channels/channel_about.dart deleted file mode 100644 index f27c6f6..0000000 --- a/lib/src/channels/channel_about.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// YouTube channel about metadata. -class ChannelAbout with EquatableMixin { - /// Channel description. - final String description; - - /// Initializes an instance of [ChannelAbout] - ChannelAbout(this.description); - - @override - List get props => [description]; -}