Version 1.10.0

Code refactoring
Fix #144
Bumped min sdk version to 2.13.0
Changed comments api interface (still experimental)
This commit is contained in:
Mattia 2021-07-22 15:03:07 +02:00
parent ec80924a28
commit 73504b2a44
18 changed files with 250 additions and 294 deletions

View File

@ -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.

View File

@ -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,
@ -25,4 +24,6 @@ class Channel with _$Channel {
/// Channel URL.
String get url => 'https://www.youtube.com/channel/$id';
const Channel._();
}

View File

@ -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';
@ -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,
@ -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,
@ -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));

View File

@ -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) {
@ -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();
}
}

View File

@ -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.'));

View File

@ -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();
@ -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.'));
}
///
@ -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') ?? '';
}

View File

@ -5,7 +5,7 @@ import '../../retry.dart';
import '../youtube_http_client.dart';
///
class ClosedCaptionTrackResponse {
class ClosedCaptionClient {
final xml.XmlDocument root;
///
@ -13,19 +13,19 @@ class ClosedCaptionTrackResponse {
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);
});
}
}

View File

@ -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';
}

View File

@ -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;
///
@ -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');
@ -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);

View File

@ -1,7 +1,7 @@
import '../../extensions/helpers_extension.dart';
import '../../reverse_engineering/responses/closed_caption_track_response.dart'
show ClosedCaptionTrackResponse;
import '../../reverse_engineering/responses/video_info_response.dart';
import '../../reverse_engineering/responses/closed_caption_client.dart' as re
show ClosedCaptionClient;
import '../../reverse_engineering/responses/video_info_client.dart';
import '../../reverse_engineering/youtube_http_client.dart';
import '../videos.dart';
import 'closed_caption.dart';
@ -35,7 +35,7 @@ class ClosedCaptionClient {
videoId = VideoId.fromString(videoId);
var tracks = <ClosedCaptionTrackInfo>{};
var videoInfoResponse =
await VideoInfoResponse.get(_httpClient, videoId.value);
await VideoInfoClient.get(_httpClient, videoId.value);
var playerResponse = videoInfoResponse.playerResponse;
for (final track in playerResponse.closedCaptionTrack) {
@ -54,8 +54,7 @@ class ClosedCaptionClient {
/// Gets the actual closed caption track which is
/// identified by the specified metadata.
Future<ClosedCaptionTrack> get(ClosedCaptionTrackInfo trackInfo) async {
var response =
await ClosedCaptionTrackResponse.get(_httpClient, trackInfo.url);
var response = await re.ClosedCaptionClient.get(_httpClient, trackInfo.url);
var captions = response.closedCaptions
.where((e) => !e.text.isNullOrWhiteSpace)

View File

@ -9,9 +9,6 @@ part 'comment.freezed.dart';
class Comment with _$Comment {
/// Initializes an instance of [Comment]
const factory Comment(
/// Comment id.
String commentId,
/// Comment author name.
String author,
@ -33,9 +30,5 @@ class Comment with _$Comment {
/// Used internally.
/// Shouldn't be used in the code.
@internal String? continuation,
/// Used internally.
/// Shouldn't be used in the code.
@internal String? clicktrackingParams,
) = _Comment;
}

View File

@ -16,18 +16,9 @@ final _privateConstructorUsedError = UnsupportedError(
class _$CommentTearOff {
const _$CommentTearOff();
_Comment call(
String commentId,
String author,
ChannelId channelId,
String text,
int likeCount,
String publishedTime,
int replyCount,
@internal String? continuation,
@internal String? clicktrackingParams) {
_Comment call(String author, ChannelId channelId, String text, int likeCount,
String publishedTime, int replyCount, @internal String? continuation) {
return _Comment(
commentId,
author,
channelId,
text,
@ -35,7 +26,6 @@ class _$CommentTearOff {
publishedTime,
replyCount,
continuation,
clicktrackingParams,
);
}
}
@ -45,9 +35,6 @@ const $Comment = _$CommentTearOff();
/// @nodoc
mixin _$Comment {
/// Comment id.
String get commentId => throw _privateConstructorUsedError;
/// Comment author name.
String get author => throw _privateConstructorUsedError;
@ -71,11 +58,6 @@ mixin _$Comment {
@internal
String? get continuation => throw _privateConstructorUsedError;
/// Used internally.
/// Shouldn't be used in the code.
@internal
String? get clicktrackingParams => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$CommentCopyWith<Comment> get copyWith => throw _privateConstructorUsedError;
}
@ -85,15 +67,13 @@ abstract class $CommentCopyWith<$Res> {
factory $CommentCopyWith(Comment value, $Res Function(Comment) then) =
_$CommentCopyWithImpl<$Res>;
$Res call(
{String commentId,
String author,
{String author,
ChannelId channelId,
String text,
int likeCount,
String publishedTime,
int replyCount,
@internal String? continuation,
@internal String? clicktrackingParams});
@internal String? continuation});
$ChannelIdCopyWith<$Res> get channelId;
}
@ -108,7 +88,6 @@ class _$CommentCopyWithImpl<$Res> implements $CommentCopyWith<$Res> {
@override
$Res call({
Object? commentId = freezed,
Object? author = freezed,
Object? channelId = freezed,
Object? text = freezed,
@ -116,13 +95,8 @@ class _$CommentCopyWithImpl<$Res> implements $CommentCopyWith<$Res> {
Object? publishedTime = freezed,
Object? replyCount = freezed,
Object? continuation = freezed,
Object? clicktrackingParams = freezed,
}) {
return _then(_value.copyWith(
commentId: commentId == freezed
? _value.commentId
: commentId // ignore: cast_nullable_to_non_nullable
as String,
author: author == freezed
? _value.author
: author // ignore: cast_nullable_to_non_nullable
@ -151,10 +125,6 @@ class _$CommentCopyWithImpl<$Res> implements $CommentCopyWith<$Res> {
? _value.continuation
: continuation // ignore: cast_nullable_to_non_nullable
as String?,
clicktrackingParams: clicktrackingParams == freezed
? _value.clicktrackingParams
: clicktrackingParams // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@ -172,15 +142,13 @@ abstract class _$CommentCopyWith<$Res> implements $CommentCopyWith<$Res> {
__$CommentCopyWithImpl<$Res>;
@override
$Res call(
{String commentId,
String author,
{String author,
ChannelId channelId,
String text,
int likeCount,
String publishedTime,
int replyCount,
@internal String? continuation,
@internal String? clicktrackingParams});
@internal String? continuation});
@override
$ChannelIdCopyWith<$Res> get channelId;
@ -197,7 +165,6 @@ class __$CommentCopyWithImpl<$Res> extends _$CommentCopyWithImpl<$Res>
@override
$Res call({
Object? commentId = freezed,
Object? author = freezed,
Object? channelId = freezed,
Object? text = freezed,
@ -205,13 +172,8 @@ class __$CommentCopyWithImpl<$Res> extends _$CommentCopyWithImpl<$Res>
Object? publishedTime = freezed,
Object? replyCount = freezed,
Object? continuation = freezed,
Object? clicktrackingParams = freezed,
}) {
return _then(_Comment(
commentId == freezed
? _value.commentId
: commentId // ignore: cast_nullable_to_non_nullable
as String,
author == freezed
? _value.author
: author // ignore: cast_nullable_to_non_nullable
@ -240,10 +202,6 @@ class __$CommentCopyWithImpl<$Res> extends _$CommentCopyWithImpl<$Res>
? _value.continuation
: continuation // ignore: cast_nullable_to_non_nullable
as String?,
clicktrackingParams == freezed
? _value.clicktrackingParams
: clicktrackingParams // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
@ -251,23 +209,11 @@ class __$CommentCopyWithImpl<$Res> extends _$CommentCopyWithImpl<$Res>
/// @nodoc
class _$_Comment implements _Comment {
const _$_Comment(
this.commentId,
this.author,
this.channelId,
this.text,
this.likeCount,
this.publishedTime,
this.replyCount,
@internal this.continuation,
@internal this.clicktrackingParams);
const _$_Comment(this.author, this.channelId, this.text, this.likeCount,
this.publishedTime, this.replyCount, @internal this.continuation);
@override
/// Comment id.
final String commentId;
@override
/// Comment author name.
final String author;
@override
@ -296,25 +242,16 @@ class _$_Comment implements _Comment {
/// Shouldn't be used in the code.
@internal
final String? continuation;
@override
/// Used internally.
/// Shouldn't be used in the code.
@internal
final String? clicktrackingParams;
@override
String toString() {
return 'Comment(commentId: $commentId, author: $author, channelId: $channelId, text: $text, likeCount: $likeCount, publishedTime: $publishedTime, replyCount: $replyCount, continuation: $continuation, clicktrackingParams: $clicktrackingParams)';
return 'Comment(author: $author, channelId: $channelId, text: $text, likeCount: $likeCount, publishedTime: $publishedTime, replyCount: $replyCount, continuation: $continuation)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other is _Comment &&
(identical(other.commentId, commentId) ||
const DeepCollectionEquality()
.equals(other.commentId, commentId)) &&
(identical(other.author, author) ||
const DeepCollectionEquality().equals(other.author, author)) &&
(identical(other.channelId, channelId) ||
@ -333,24 +270,19 @@ class _$_Comment implements _Comment {
.equals(other.replyCount, replyCount)) &&
(identical(other.continuation, continuation) ||
const DeepCollectionEquality()
.equals(other.continuation, continuation)) &&
(identical(other.clicktrackingParams, clicktrackingParams) ||
const DeepCollectionEquality()
.equals(other.clicktrackingParams, clicktrackingParams)));
.equals(other.continuation, continuation)));
}
@override
int get hashCode =>
runtimeType.hashCode ^
const DeepCollectionEquality().hash(commentId) ^
const DeepCollectionEquality().hash(author) ^
const DeepCollectionEquality().hash(channelId) ^
const DeepCollectionEquality().hash(text) ^
const DeepCollectionEquality().hash(likeCount) ^
const DeepCollectionEquality().hash(publishedTime) ^
const DeepCollectionEquality().hash(replyCount) ^
const DeepCollectionEquality().hash(continuation) ^
const DeepCollectionEquality().hash(clicktrackingParams);
const DeepCollectionEquality().hash(continuation);
@JsonKey(ignore: true)
@override
@ -360,22 +292,16 @@ class _$_Comment implements _Comment {
abstract class _Comment implements Comment {
const factory _Comment(
String commentId,
String author,
ChannelId channelId,
String text,
int likeCount,
String publishedTime,
int replyCount,
@internal String? continuation,
@internal String? clicktrackingParams) = _$_Comment;
@internal String? continuation) = _$_Comment;
@override
/// Comment id.
String get commentId => throw _privateConstructorUsedError;
@override
/// Comment author name.
String get author => throw _privateConstructorUsedError;
@override
@ -405,12 +331,6 @@ abstract class _Comment implements Comment {
@internal
String? get continuation => throw _privateConstructorUsedError;
@override
/// Used internally.
/// Shouldn't be used in the code.
@internal
String? get clicktrackingParams => throw _privateConstructorUsedError;
@override
@JsonKey(ignore: true)
_$CommentCopyWith<_Comment> get copyWith =>
throw _privateConstructorUsedError;

View File

@ -1,47 +1,20 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../channels/channel_id.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../../reverse_engineering/responses/comments_client.dart' as re;
import '../../reverse_engineering/youtube_http_client.dart';
import '../videos.dart';
import 'comment.dart';
/// Queries related to comments of YouTube videos.
@experimental
class CommentsClient {
final YoutubeHttpClient _httpClient;
/// Initializes an instance of [CommentsClient]
CommentsClient(this._httpClient);
/// Returns the json parsed comments map.
Future<Map<String, dynamic>> _getCommentJson(
String continuation,
String clickTrackingParams,
String xsfrToken,
String visitorInfoLive,
String ysc) async {
final url = Uri(
scheme: 'https',
host: 'www.youtube.com',
path: '/next',
queryParameters: {
'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
});
return retry(() async {
var raw = await _httpClient.postString(url, headers: {
'x-youtube-client-name': '1',
'x-youtube-client-version': '2.20210622.10.00',
'cookie':
'YSC=$ysc; CONSENT=YES+cb; GPS=1; VISITOR_INFO1_LIVE=$visitorInfoLive',
}, body: {
'session_token': xsfrToken
});
return json.decode(raw);
});
}
/// Returns a stream emitting all the [video]'s comment.
/// A request is page for every comment page,
/// a page contains at most 20 comments, use .take if you want to limit
@ -50,98 +23,23 @@ class CommentsClient {
/// The streams doesn't emit any data if [Video.hasWatchPage] is false.
/// Use `videos.get(videoId, forceWatchPage: true)` to assure that the
/// WatchPage is fetched.
Stream<Comment> getComments(Video video) async* {
Future<List<Comment>> getComments(Video video) async {
if (video.watchPage == null) {
return;
return const [];
}
yield* _getComments(
video.watchPage!.initialData.continuation,
video.watchPage!.initialData.clickTrackingParams,
video.watchPage!.xsfrToken,
video.watchPage!.visitorInfoLive,
video.watchPage!.ysc);
}
Stream<Comment> _getComments(String continuation, String clickTrackingParams,
String xsfrToken, String visitorInfoLive, String ysc) async* {
// contents.twoColumnWatchNextResults.results.results.contents[2](firstWhere itemSectionRenderer != null).itemSectionRenderer.contents[0].continuationItemRenderer
var data = await _getCommentJson(
continuation, clickTrackingParams, xsfrToken, visitorInfoLive, ysc);
var contentRoot = data
.get('response')
?.get('continuationContents')
?.get('itemSectionContinuation')
?.getT<List<dynamic>>('contents')
?.map((e) => e['commentThreadRenderer'])
.toList()
.cast<Map<String, dynamic>>();
if (contentRoot == null) {
return;
}
for (final content in contentRoot) {
var commentRaw = content.get('comment')!.get('commentRenderer')!;
String? continuation;
String? clickTrackingParams;
final replies = content.get('replies');
if (replies != null) {
final continuationData = replies
.get('commentRepliesRenderer')!
.getList('continuations')!
.first
.get('nextContinuationData')!;
final page = await re.CommentsClient.get(_httpClient, video);
continuation = continuationData.getT<String>('continuation');
clickTrackingParams =
continuationData.getT<String>('clickTrackingParams');
}
yield Comment(
commentRaw.getT<String>('commentId')!,
commentRaw.get('authorText')!.getT<String>('simpleText')!,
ChannelId(commentRaw
.get('authorEndpoint')!
.get('browseEndpoint')!
.getT<String>('browseId')!),
commentRaw
.get('contentText')!
.getT<List<dynamic>>('runs')!
.parseRuns(),
commentRaw.get('voteCount')?.getT<String>('simpleText')?.parseInt() ??
commentRaw
.get('voteCount')
?.getT<List<dynamic>>('runs')
?.parseRuns()
.parseInt() ??
0,
commentRaw
.get('publishedTimeText')!
.getT<List<dynamic>>('runs')!
.parseRuns(),
commentRaw.getT<int>('replyCount') ?? 0,
continuation,
clickTrackingParams);
}
var continuationRoot = (data
.get('response')
?.get('continuationContents')
?.get('itemSectionContinuation')
?.getT<List<dynamic>>('continuations')
?.first)
?.get('nextContinuationData');
if (continuationRoot != null) {
yield* _getComments(
continuationRoot['continuation'],
continuationRoot['clickTrackingParams'],
xsfrToken,
visitorInfoLive,
ysc);
}
}
Stream<Comment> getReplies(Video video, Comment comment) async* {
if (video.watchPage == null ||
comment.continuation == null ||
comment.clicktrackingParams == null) {
return;
}
return page?.comments
.map((e) => Comment(
e.author,
ChannelId(e.channelId),
e.text,
e.likeCount ?? 0,
e.publishTime,
e.repliesCount ?? 0,
e.continuation))
.toList(growable: false) ??
const [];
}
}

View File

@ -1,13 +1,13 @@
import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart';
import '../../reverse_engineering/cipher/cipher_operations.dart';
import '../../reverse_engineering/dash_manifest.dart';
import '../../reverse_engineering/heuristics.dart';
import '../../reverse_engineering/models/stream_info_provider.dart';
import '../../reverse_engineering/pages/embed_page.dart';
import '../../reverse_engineering/pages/watch_page.dart';
import '../../reverse_engineering/dash_manifest.dart';
import '../../reverse_engineering/player/player_source.dart';
import '../../reverse_engineering/models/stream_info_provider.dart';
import '../../reverse_engineering/responses/video_info_response.dart';
import '../../reverse_engineering/responses/video_info_client.dart';
import '../../reverse_engineering/youtube_http_client.dart';
import '../video_id.dart';
import 'bitrate.dart';
@ -48,7 +48,7 @@ class StreamsClient {
_httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl);
var cipherOperations = playerSource.getCipherOperations();
var videoInfoResponse = await VideoInfoResponse.get(
var videoInfoResponse = await VideoInfoClient.get(
_httpClient, videoId.toString(), playerSource.sts);
var playerResponse = videoInfoResponse.playerResponse;
@ -224,21 +224,9 @@ class StreamsClient {
/// about available streams in the specified video.
Future<StreamManifest> getManifest(dynamic videoId) async {
videoId = VideoId.fromString(videoId);
// We can try to extract the manifest from two sources:
// get_video_info and the video watch page.
// In some cases one works, in some cases another does.
try {
var context = await _getStreamContextFromVideoInfo(videoId);
return _getManifest(context);
} on YoutubeExplodeException catch (e) {
try {
var context = await _getStreamContextFromWatchPage(videoId);
return _getManifest(context);
} on YoutubeExplodeException catch (e1) {
throw e..combine(e1);
}
}
var context = await _getStreamContextFromWatchPage(videoId);
return _getManifest(context);
}
/// Gets the HTTP Live Stream (HLS) manifest URL

View File

@ -1,8 +1,9 @@
import 'package:youtube_explode_dart/src/reverse_engineering/player/player_response.dart';
import '../channels/channel_id.dart';
import '../common/common.dart';
import '../extensions/helpers_extension.dart';
import '../reverse_engineering/pages/watch_page.dart';
import '../reverse_engineering/responses/video_info_response.dart';
import '../reverse_engineering/youtube_http_client.dart';
import 'closed_captions/closed_caption_client.dart';
import 'comments/comments_client.dart';
@ -29,11 +30,9 @@ class VideoClient {
/// Gets the metadata associated with the specified video.
Future<Video> _getVideoFromWatchPage(VideoId videoId) async {
var videoInfoResponse =
await VideoInfoResponse.get(_httpClient, videoId.value);
var playerResponse = videoInfoResponse.playerResponse;
var watchPage = await WatchPage.get(_httpClient, videoId.value);
final playerResponse = watchPage.playerResponse!;
return Video(
videoId,
playerResponse.videoTitle,

View File

@ -1,6 +1,6 @@
name: youtube_explode_dart
description: A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
version: 1.9.10
version: 1.10.0
homepage: https://github.com/Hexer10/youtube_explode_dart

View File

@ -14,7 +14,7 @@ void main() {
test('Get comments of a video', () async {
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
var video = await yt!.videos.get(VideoId(videoUrl));
var comments = await yt!.videos.commentsClient.getComments(video).toList();
var comments = await yt!.videos.commentsClient.getComments(video);
expect(comments.length, greaterThanOrEqualTo(1));
});
}