First implementation of channel page.
This commit is contained in:
parent
41a4947db2
commit
7770769457
|
@ -61,4 +61,4 @@ linter:
|
|||
analyzer:
|
||||
exclude:
|
||||
- example\**
|
||||
- src\**.g.gdart
|
||||
- lib\src\reverse_engineering\responses\generated\**
|
|
@ -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];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ abstract class YoutubeExplodeException implements Exception {
|
|||
YoutubeExplodeException(this.message);
|
||||
|
||||
@override
|
||||
String toString();
|
||||
String toString() => '$runtimeType: $message}';
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
|
@ -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
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue