First implementation of channel page.

This commit is contained in:
Mattia 2020-10-01 16:55:32 +02:00
parent 41a4947db2
commit 7770769457
13 changed files with 5954 additions and 17 deletions

View File

@ -61,4 +61,4 @@ linter:
analyzer:
exclude:
- example\**
- src\**.g.gdart
- lib\src\reverse_engineering\responses\generated\**

View File

@ -0,0 +1,37 @@
import 'package:equatable/equatable.dart';
import 'channel_link.dart';
import 'channel_thumbnail.dart';
/// YouTube channel's about page metadata.
class ChannelAbout with EquatableMixin {
/// Full channel description.
final String description;
/// Channel view count.
final int viewCount;
/// Channel join date.
/// Formatted as: Gen 01, 2000
final String joinDate;
/// Channel title.
final String title;
/// Channel thumbnails.
final List<ChannelThumbnail> thumbnails;
/// Channel country.
final String country;
/// Channel links.
final List<ChannelLink> channelLinks;
/// Initialize an instance of [ChannelAbout]
ChannelAbout(this.description, this.viewCount, this.joinDate, this.title,
this.thumbnails, this.country, this.channelLinks);
@override
List<Object> get props =>
[description, viewCount, joinDate, title, thumbnails];
}

View File

@ -0,0 +1,20 @@
import 'package:equatable/equatable.dart';
/// Represents a channel link.
class ChannelLink with EquatableMixin {
/// Link title.
final String title;
/// Link URL.
/// Already decoded with the YouTube shortener already taken out.
final Uri url;
/// Link Icon URL.
final Uri icon;
/// Initialize an instance of [ChannelLink]
ChannelLink(this.title, this.url, this.icon);
@override
List<Object> get props => [title, url, icon];
}

View File

@ -0,0 +1,19 @@
import 'package:equatable/equatable.dart';
/// Represent a channel thumbnail
class ChannelThumbnail with EquatableMixin {
/// Image url.
final Uri url;
/// Image height.
final int height;
/// Image width.
final int width;
/// Initialize an instance of [ChannelThumbnail].
ChannelThumbnail(this.url, this.height, this.width);
@override
List<Object> get props => [url, height, width];
}

View File

@ -4,8 +4,11 @@
library youtube_explode.channels;
export 'channel.dart';
export 'channel_about.dart';
export 'channel_client.dart';
export 'channel_id.dart';
export 'channel_link.dart';
export 'channel_thumbnail.dart';
export 'channel_video.dart';
export 'username.dart';
export 'video_sorting.dart';

View File

@ -20,7 +20,4 @@ If this issue persists, please report it on the project's GitHub page.
Request: ${response.request}
Response: (${response.statusCode})
''';
@override
String toString() => 'FatalFailureException: $message';
}

View File

@ -21,7 +21,4 @@ Unfortunately, there's nothing the library can do to work around this error.
Request: ${response.request}
Response: $response
''';
@override
String toString() => 'RequestLimitExceeded: $message';
}

View File

@ -28,7 +28,4 @@ class VideoUnplayableException implements YoutubeExplodeException {
VideoUnplayableException.notLiveStream(VideoId videoId)
: message = 'Video \'$videoId\' is not an ongoing live stream.\n'
'Live stream manifest is not available for this video';
@override
String toString() => 'VideoUnplayableException: $message';
}

View File

@ -7,5 +7,5 @@ abstract class YoutubeExplodeException implements Exception {
YoutubeExplodeException(this.message);
@override
String toString();
String toString() => '$runtimeType: $message}';
}

View File

@ -0,0 +1,148 @@
import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser;
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import '../../exceptions/exceptions.dart';
import '../../retry.dart';
import '../youtube_http_client.dart';
import 'generated/channel_about_page_id.g.dart';
import '../../extensions/helpers_extension.dart';
///
class ChannelAboutPage {
final Document _root;
_InitialData _initialData;
///
_InitialData get initialData =>
_initialData ??= _InitialData(ChannelAboutPageId.fromRawJson(_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<ChannelAboutPage> 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<ChannelAboutPage> 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;
});
}
}
final _urlExp = RegExp(r'q=([^=]*)$');
class _InitialData {
// Json parsed class
final ChannelAboutPageId root;
_InitialData(this.root);
/* Cache results */
ChannelAboutFullMetadataRenderer _content;
ChannelAboutFullMetadataRenderer get content =>
_content ??= getContentContext();
ChannelAboutFullMetadataRenderer getContentContext() {
return root
.contents
.twoColumnBrowseResultsRenderer
.tabs[5]
.tabRenderer
.content
.sectionListRenderer
.contents
.first
.itemSectionRenderer
.contents
.first
.channelAboutFullMetadataRenderer;
}
String get description => content.description.simpleText;
List<ChannelLink> get channelLinks {
return content.primaryLinks.map((e) => ChannelLink(
e.title.simpleText,
extractUrl(e.navigationEndpoint.urlEndpoint.url),
Uri.parse(e.icon.thumbnails.first.url)));
}
int get viewCount =>
int.parse(content.viewCountText.simpleText.stripNonDigits());
String get joinDate => content.joinedDateText.runs[1].text;
String get title => content.title.simpleText;
dynamic get avatar => content.avatar.thumbnails;
/// todo: continue from here with more data!
String parseRuns(List<dynamic> runs) =>
runs?.map((e) => e.text)?.join() ?? '';
Uri extractUrl(String text) =>
Uri.parse(Uri.decodeFull(_urlExp.firstMatch(text).group(1)));
}

View File

@ -10,8 +10,7 @@ import '../youtube_http_client.dart';
///
class EmbedPage {
static final _playerConfigExp =
RegExp(r"yt\.setConfig\({'PLAYER_CONFIG':(.*)}\);");
RegExp(r"yt\.setConfig\({.*?'PLAYER_CONFIG':(.*?)}");
final Document _root;
_PlayerConfig _playerConfig;
String __playerConfigJson;

File diff suppressed because it is too large Load Diff

View File

@ -85,10 +85,17 @@ class WatchPage {
'0');
///
_PlayerConfig get playerConfig =>
_playerConfig ??= _PlayerConfig(PlayerConfigJson.fromRawJson(_extractJson(
_root.getElementsByTagName('html').first.text,
'ytplayer.config = ')));
_PlayerConfig get playerConfig {
if (_playerConfig != null) {
return _playerConfig;
}
var text = _root.getElementsByTagName('html').first.text;
if (!text.contains('ytplayer.config = ')) {
return null;
}
return _playerConfig = _PlayerConfig(
PlayerConfigJson.fromRawJson(_extractJson(text, 'ytplayer.config = ')));
}
String _extractJson(String html, String separator) {
return _matchJson(