Skip to content

Commit

Permalink
Version 1.10.0
Browse files Browse the repository at this point in the history
Code refactoring
Fix #144
Bumped min sdk version to 2.13.0
Changed comments api interface (still experimental)
  • Loading branch information
Hexer10 committed Jul 22, 2021
1 parent ec80924 commit 73504b2
Show file tree
Hide file tree
Showing 18 changed files with 252 additions and 296 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.10.0
- Fix issue #144: get_video_info was removed from yt.
- Min sdk version now is 2.13.0
- BREAKING CHANGE: New comments API implementation.

## 1.9.10
- Close #139: Implement Channel.subscribersCount.

Expand Down
7 changes: 4 additions & 3 deletions lib/src/channels/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import 'channel_id.dart';
part 'channel.freezed.dart';

/// YouTube channel metadata.
@Freezed()
@freezed
class Channel with _$Channel {
const Channel._();

///
const factory Channel(
/// Channel ID.
ChannelId id,
Expand All @@ -25,4 +24,6 @@ class Channel with _$Channel {

/// Channel URL.
String get url => 'https://www.youtube.com/channel/$id';

const Channel._();
}
11 changes: 6 additions & 5 deletions lib/src/channels/channel_client.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:youtube_explode_dart/src/channels/channel_uploads_list.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/pages/channel_page.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/responses/video_info_response.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/pages/watch_page.dart';

import '../common/common.dart';
import '../extensions/helpers_extension.dart';
Expand Down Expand Up @@ -54,7 +54,7 @@ class ChannelClient {
channelId = ChannelId.fromString(channelId);

final aboutPage = await ChannelAboutPage.get(_httpClient, channelId.value);
final id = aboutPage.initialData;

return ChannelAbout(
aboutPage.description,
aboutPage.viewCount,
Expand All @@ -76,6 +76,8 @@ class ChannelClient {

var channelAboutPage =
await ChannelAboutPage.getByUsername(_httpClient, username.value);

// TODO: Expose metadata from the [ChannelAboutPage] class.
var id = channelAboutPage.initialData;
return ChannelAbout(
id.description,
Expand All @@ -94,9 +96,8 @@ class ChannelClient {
/// that uploaded the specified video.
Future<Channel> getByVideo(dynamic videoId) async {
videoId = VideoId.fromString(videoId);
var videoInfoResponse =
await VideoInfoResponse.get(_httpClient, videoId.value);
var playerResponse = videoInfoResponse.playerResponse;
var videoInfoResponse = await WatchPage.get(_httpClient, videoId.value);
var playerResponse = videoInfoResponse.playerResponse!;

var channelId = playerResponse.videoChannelId;
return get(ChannelId(channelId));
Expand Down
53 changes: 43 additions & 10 deletions lib/src/extensions/helpers_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,44 @@ extension StringUtility on String {

/// Utility for Strings.
extension StringUtility2 on String? {
static final RegExp _unitSplit = RegExp(r'^(\d+(?:\.\d)?)(\w)');

/// Parses this value as int stripping the non digit characters,
/// returns null if this fails.
int? parseInt() => int.tryParse(this?.stripNonDigits() ?? '');

int? parseIntWithUnits() {
if (this == null) {
return null;
}
final match = _unitSplit.firstMatch(this!.trim());
if (match == null) {
return null;
}
if (match.groupCount != 2) {
return null;
}

final count = double.tryParse(match.group(1) ?? '');
if (count == null) {
return null;
}

final multiplierText = match.group(2);
if (multiplierText == null) {
return null;
}

var multiplier = 1;
if (multiplierText == 'K') {
multiplier = 1000;
} else if (multiplierText == 'M') {
multiplier = 1000000;
}

return (count * multiplier).toInt();
}

/// Returns true if the string is null or empty.
bool get isNullOrWhiteSpace {
if (this == null) {
Expand Down Expand Up @@ -235,19 +269,18 @@ extension RunsParser on List<dynamic> {
}

extension GenericExtract on List<String> {
/// Used to extract initial data that start with `var ytInitialData = ` or 'window["ytInitialData"] ='.
T extractGenericData<T>(
/// Used to extract initial data.
T extractGenericData<T>(List<String> match,
T Function(Map<String, dynamic>) builder, Exception Function() orThrow) {
var initialData =
firstWhereOrNull((e) => e.contains('var ytInitialData = '))
?.extractJson('var ytInitialData = ');
initialData ??=
firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='))
?.extractJson('window["ytInitialData"] =');
JsonMap? initialData;

if (initialData != null) {
return builder(initialData);
for (final m in match) {
initialData = firstWhereOrNull((e) => e.contains(m))?.extractJson(m);
if (initialData != null) {
return builder(initialData);
}
}

throw orThrow();
}
}
1 change: 1 addition & 0 deletions lib/src/reverse_engineering/models/youtube_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ abstract class YoutubePage<T extends InitialData> {
.map((e) => e.text)
.toList(growable: false);
return scriptText.extractGenericData(
['var ytInitialData = ', 'window["ytInitialData"] ='],
initialDataBuilder!,
() => TransientFailureException(
'Failed to retrieve initial data from $runtimeType, please report this to the project GitHub page.'));
Expand Down
31 changes: 15 additions & 16 deletions lib/src/reverse_engineering/pages/watch_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class WatchPage extends YoutubePage<_InitialData> {
.nullIfWhitespace ??
'0');

String? get commentsContinuation => initialData.commentsContinuation;

static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\})');

late final WatchPlayerConfig? playerConfig = getPlayerConfig();
Expand All @@ -111,18 +113,16 @@ class WatchPage extends YoutubePage<_InitialData> {
return WatchPlayerConfig(jsonMap);
}

///
PlayerResponse? getPlayerResponse() {
final val = root
final scriptText = root
.querySelectorAll('script')
.map((e) => e.text)
.map((e) => _playerResponseExp.firstMatch(e)?.group(1))
.firstWhereOrNull((e) => !e.isNullOrWhiteSpace)
?.extractJson();
if (val == null) {
return null;
}
return PlayerResponse(val);
.toList(growable: false);
return scriptText.extractGenericData(
['var ytInitialPlayerResponse = '],
(root) => PlayerResponse(root),
() => TransientFailureException(
'Failed to retrieve initial player response, please report this to the project GitHub page.'));
}

///
Expand Down Expand Up @@ -183,16 +183,15 @@ class _InitialData extends InitialData {
?.getList('contents')
?.firstWhere((e) => e['itemSectionRenderer'] != null)
.get('itemSectionRenderer')
?.getList('continuations')
?.getList('contents')
?.firstOrNull
?.get('nextContinuationData');
?.get('continuationItemRenderer')
?.get('continuationEndpoint')
?.get('continuationCommand');
}
return null;
}

late final String continuation =
getContinuationContext()?.getT<String>('continuation') ?? '';

late final String clickTrackingParams =
getContinuationContext()?.getT<String>('clickTrackingParams') ?? '';
late final String commentsContinuation =
getContinuationContext()?.getT<String>('token') ?? '';
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@ import '../../retry.dart';
import '../youtube_http_client.dart';

///
class ClosedCaptionTrackResponse {
class ClosedCaptionClient {
final xml.XmlDocument root;

///
late final Iterable<ClosedCaption> closedCaptions =
root.findAllElements('p').map((e) => ClosedCaption._(e));

///
ClosedCaptionTrackResponse(this.root);
ClosedCaptionClient(this.root);

///
// ignore: deprecated_member_use
ClosedCaptionTrackResponse.parse(String raw) : root = xml.parse(raw);
ClosedCaptionClient.parse(String raw) : root = xml.parse(raw);

///
static Future<ClosedCaptionTrackResponse> get(
static Future<ClosedCaptionClient> get(
YoutubeHttpClient httpClient, Uri url) {
var formatUrl = url.replaceQueryParameters({'fmt': 'srv3'});
final formatUrl = url.replaceQueryParameters({'fmt': 'srv3'});
return retry(() async {
var raw = await httpClient.getString(formatUrl);
return ClosedCaptionTrackResponse.parse(raw);
return ClosedCaptionClient.parse(raw);
});
}
}
Expand Down
Empty file.
117 changes: 117 additions & 0 deletions lib/src/reverse_engineering/responses/comments_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import 'package:collection/collection.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/pages/watch_page.dart';

import '../../../youtube_explode_dart.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../youtube_http_client.dart';

class CommentsClient {
final JsonMap root;

late final List<JsonMap> _commentRenderers = _getCommentRenderers();

late final List<_Comment> comments =
_commentRenderers.map((e) => _Comment(e)).toList(growable: false);

CommentsClient(this.root);

///
static Future<CommentsClient?> get(
YoutubeHttpClient httpClient, Video video) async {
final watchPage = video.watchPage ??
await retry<WatchPage>(
() async => WatchPage.get(httpClient, video.id.value));

final continuation = watchPage.commentsContinuation;
if (continuation == null) {
return null;
}

final data = await httpClient.sendPost('next', continuation);
return CommentsClient(data);
}

List<JsonMap> _getCommentRenderers() {
return root
.getList('onResponseReceivedEndpoints')![1]
.get('reloadContinuationItemsCommand')!
.getList('continuationItems')!
.where((e) => e['commentThreadRenderer'] != null)
.map((e) => e.get('commentThreadRenderer')!)
.toList(growable: false);
}
}

class _Comment {
final JsonMap root;

late final JsonMap _commentRenderer =
root.get('comment')!.get('commentRenderer')!;

late final JsonMap? _commentRepliesRenderer =
root.get('replies')?.get('commentRepliesRenderer');

/// Used to get replies
late final String? continuation = _commentRepliesRenderer
?.getList('contents')
?.firstOrNull
?.get('continuationItemRenderer')
?.get('continuationEndpoint')
?.get('continuationCommand')
?.getT<String>('token');

late final int? repliesCount = _commentRepliesRenderer
?.get('viewReplies')
?.get('buttonRenderer')
?.get('text')
?.getList('runs')
?.elementAtSafe(2)
?.getT<String>('text')
?.parseIntWithUnits();

late final String author =
_commentRenderer.get('authorText')!.getT<String>('simpleText')!;

late final String channelThumbnail = _commentRenderer
.get('authorThumbnail')!
.getList('thumbnails')!
.last
.getT<String>('url')!;

late final String channelId = _commentRenderer
.get('authorEndpoint')!
.get('browseEndpoint')!
.getT<String>('browseId')!;

late final String text = _commentRenderer
.get('contentText')!
.getT<List<dynamic>>('runs')!
.parseRuns();

late final String publishTime = _commentRenderer
.get('publishedTimeText')!
.getList('runs')!
.first
.getT<String>('text')!;

/// Needs to be parsed as an int current is like: 1.2K
late final int? likeCount = _commentRenderer
.get('actionButtons')
?.get('commentActionButtonsRenderer')
?.get('likeButton')
?.get('toggleButtonRenderer')
?.get('defaultServiceEndpoint')
?.get('performCommentActionEndpoint')
?.getList('clientActions')
?.first
.get('updateCommentVoteAction')
?.get('voteCount')
?.getT<String>('simpleText')
?.parseIntWithUnits();

_Comment(this.root);

@override
String toString() => '$author: $text';
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import '../player/player_response.dart';
import '../models/stream_info_provider.dart';

///
class VideoInfoResponse {
///
@deprecated
class VideoInfoClient {
final Map<String, String> root;

///
Expand Down Expand Up @@ -43,13 +45,13 @@ class VideoInfoResponse {
];

///
VideoInfoResponse(this.root);
VideoInfoClient(this.root);

///
VideoInfoResponse.parse(String raw) : root = Uri.splitQueryString(raw);
VideoInfoClient.parse(String raw) : root = Uri.splitQueryString(raw);

///
static Future<VideoInfoResponse> get(
static Future<VideoInfoClient> get(
YoutubeHttpClient httpClient, String videoId,
[String? sts]) {
var eurl = Uri.encodeFull('https://youtube.googleapis.com/v/$videoId');
Expand All @@ -71,7 +73,7 @@ class VideoInfoResponse {

return retry(() async {
var raw = await httpClient.getString(url);
var result = VideoInfoResponse.parse(raw);
var result = VideoInfoClient.parse(raw);

if (!result.isVideoAvailable || !result.playerResponse.isVideoAvailable) {
throw VideoUnplayableException(videoId);
Expand Down
Loading

0 comments on commit 73504b2

Please sign in to comment.