diff --git a/CHANGELOG.md b/CHANGELOG.md index e91bea8..fdca3b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ +## 1.10.2 +- Better comments API: Implemented API to fetch more comments & replies. + ## 1.10.1 - Fix issue #146: Closed Captions couldn't be extracted anymore. - Code cleanup. -- + ## 1.10.0 - Fix issue #144: get_video_info was removed from yt. diff --git a/lib/src/common/engagement.dart b/lib/src/common/engagement.dart index 1a3afb0..0a3d9bf 100644 --- a/lib/src/common/engagement.dart +++ b/lib/src/common/engagement.dart @@ -5,7 +5,6 @@ part 'engagement.freezed.dart'; /// User activity statistics. @freezed class Engagement with _$Engagement { - const factory Engagement( /// View count. int viewCount, diff --git a/lib/src/exceptions/youtube_explode_exception.dart b/lib/src/exceptions/youtube_explode_exception.dart index fb18a9f..ac015ad 100644 --- a/lib/src/exceptions/youtube_explode_exception.dart +++ b/lib/src/exceptions/youtube_explode_exception.dart @@ -1,3 +1,5 @@ +import 'package:meta/meta.dart'; + /// Parent class for domain exceptions thrown by [YoutubeExplode] abstract class YoutubeExplodeException implements Exception { /// Generic message. @@ -13,6 +15,7 @@ abstract class YoutubeExplodeException implements Exception { YoutubeExplodeException(this.message); @override + @nonVirtual String toString() { if (_others.isEmpty) { return '$runtimeType: $message'; diff --git a/lib/src/extensions/helpers_extension.dart b/lib/src/extensions/helpers_extension.dart index 2a6631e..aa093d7 100644 --- a/lib/src/extensions/helpers_extension.dart +++ b/lib/src/extensions/helpers_extension.dart @@ -80,7 +80,7 @@ extension StringUtility on String { /// Utility for Strings. extension StringUtility2 on String? { - static final RegExp _unitSplit = RegExp(r'^(\d+(?:\.\d)?)(\w)'); + static final RegExp _unitSplit = RegExp(r'^(\d+(?:\.\d+)?)(\w)?'); /// Parses this value as int stripping the non digit characters, /// returns null if this fails. @@ -104,9 +104,6 @@ extension StringUtility2 on String? { } final multiplierText = match.group(2); - if (multiplierText == null) { - return null; - } var multiplier = 1; if (multiplierText == 'K') { @@ -238,10 +235,17 @@ extension GetOrNullMap on Map { } /// Get a List>> from a map. - List>? getList(String key) { + List>? getList(String key, [String? orKey]) { var v = this[key]; if (v == null) { - return null; + if (orKey != null) { + v = this[orKey]; + if (v == null) { + return null; + } + } else { + return null; + } } if (v is! List) { throw Exception('Invalid type: ${v.runtimeType} should be of type List'); diff --git a/lib/src/reverse_engineering/pages/watch_page.dart b/lib/src/reverse_engineering/pages/watch_page.dart index 1baa5eb..395cb92 100644 --- a/lib/src/reverse_engineering/pages/watch_page.dart +++ b/lib/src/reverse_engineering/pages/watch_page.dart @@ -22,7 +22,6 @@ class WatchPage extends YoutubePage<_InitialData> { RegExp('VISITOR_INFO1_LIVE=([^;]+)'); static final RegExp _yscExp = RegExp('YSC=([^;]+)'); - @override // Overridden to be non-nullable. // ignore: overridden_fields diff --git a/lib/src/reverse_engineering/player/player_response.dart b/lib/src/reverse_engineering/player/player_response.dart index 9a1c209..c135dee 100644 --- a/lib/src/reverse_engineering/player/player_response.dart +++ b/lib/src/reverse_engineering/player/player_response.dart @@ -148,8 +148,7 @@ class ClosedCaptionTrack { String get languageCode => root.getT('languageCode')!; /// - String? get languageName => - root.get('name')!.getT('simpleText'); + String? get languageName => root.get('name')!.getT('simpleText'); /// bool get autoGenerated => diff --git a/lib/src/reverse_engineering/responses/comments_client.dart b/lib/src/reverse_engineering/responses/comments_client.dart index 67d3a97..dec7af5 100644 --- a/lib/src/reverse_engineering/responses/comments_client.dart +++ b/lib/src/reverse_engineering/responses/comments_client.dart @@ -14,6 +14,8 @@ class CommentsClient { late final List<_Comment> comments = _commentRenderers.map((e) => _Comment(e)).toList(growable: false); + late final String? _continuationToken = _getContinuationToken(); + CommentsClient(this.root); /// @@ -32,21 +34,82 @@ class CommentsClient { return CommentsClient(data); } + /// + static Future getReplies( + YoutubeHttpClient httpClient, String token) async { + final data = await httpClient.sendPost('next', token); + return CommentsClient(data); + } + List _getCommentRenderers() { return root - .getList('onResponseReceivedEndpoints')![1] - .get('reloadContinuationItemsCommand')! - .getList('continuationItems')! - .where((e) => e['commentThreadRenderer'] != null) - .map((e) => e.get('commentThreadRenderer')!) - .toList(growable: false); + .getList('onResponseReceivedEndpoints')! + .last + .get('appendContinuationItemsAction') + ?.getList('continuationItems') + ?.where((e) => e['commentRenderer'] != null) + .toList(growable: false) /* Used for the replies */ ?? + root + .getList('onResponseReceivedEndpoints')! + .last + .get('reloadContinuationItemsCommand')! + .getList('continuationItems', 'appendContinuationItemsAction')! + .where((e) => e['commentThreadRenderer'] != null) + .map((e) => e.get('commentThreadRenderer')!) + .toList(growable: false); + } + + String? _getContinuationToken() { + return root + .getList('onResponseReceivedEndpoints')! + .last + .get('appendContinuationItemsAction') + ?.getList('continuationItems') + ?.firstWhereOrNull((e) => e['continuationItemRenderer'] != null) + ?.get('continuationItemRenderer') + ?.get('button') + ?.get('buttonRenderer') + ?.get('command') + ?.get('continuationCommand') + ?.getT('token') /* Used for the replies */ ?? + root + .getList('onResponseReceivedEndpoints')! + .last + .get('reloadContinuationItemsCommand')! + .getList('continuationItems', 'appendContinuationItemsAction')! + .firstWhereOrNull((e) => e['continuationItemRenderer'] != null) + ?.get('continuationItemRenderer') + ?.get('continuationEndpoint') + ?.get('continuationCommand') + ?.getT('token'); + } + + int getCommentsCount() => root + .getList('onResponseReceivedEndpoints')![1] + .get('reloadContinuationItemsCommand')! + .getList('continuationItems')! + .first + .get('commentsHeaderRenderer')! + .get('commentsCount')! + .getList('runs')! + .first + .getT('text') + .parseIntWithUnits()!; + + Future nextPage(YoutubeHttpClient httpClient) async { + if (_continuationToken == null) { + return null; + } + + final data = await httpClient.sendPost('next', _continuationToken!); + return CommentsClient(data); } } class _Comment { final JsonMap root; - late final JsonMap _commentRenderer = + late final JsonMap _commentRenderer = root.get('commentRenderer') ?? root.get('comment')!.get('commentRenderer')!; late final JsonMap? _commentRepliesRenderer = @@ -61,14 +124,7 @@ class _Comment { ?.get('continuationCommand') ?.getT('token'); - late final int? repliesCount = _commentRepliesRenderer - ?.get('viewReplies') - ?.get('buttonRenderer') - ?.get('text') - ?.getList('runs') - ?.elementAtSafe(2) - ?.getT('text') - ?.parseIntWithUnits(); + late final int? repliesCount = _commentRenderer.getT('replyCount'); late final String author = _commentRenderer.get('authorText')!.getT('simpleText')!; @@ -95,18 +151,8 @@ class _Comment { .first .getT('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') + .get('voteCount') ?.getT('simpleText') ?.parseIntWithUnits(); diff --git a/lib/src/reverse_engineering/youtube_http_client.dart b/lib/src/reverse_engineering/youtube_http_client.dart index af656cf..1da973a 100644 --- a/lib/src/reverse_engineering/youtube_http_client.dart +++ b/lib/src/reverse_engineering/youtube_http_client.dart @@ -105,9 +105,11 @@ class YoutubeHttpClient extends http.BaseClient { var bytesCount = start; for (var i = start; i < streamInfo.size.totalBytes; i += 9898989) { try { - final request = http.Request('get', url); - request.headers['range'] = 'bytes=$i-${i + 9898989 - 1}'; - final response = await retry(() => send(request)); + final response = await retry(() { + final request = http.Request('get', url); + request.headers['range'] = 'bytes=$i-${i + 9898989 - 1}'; + return send(request); + }); if (validate) { _validateResponse(response, response.statusCode); } diff --git a/lib/src/search/search_list.dart b/lib/src/search/search_list.dart index f05f6b5..2fc6810 100644 --- a/lib/src/search/search_list.dart +++ b/lib/src/search/search_list.dart @@ -7,7 +7,7 @@ import '../../youtube_explode_dart.dart'; import '../extensions/helpers_extension.dart'; /// This list contains search videos. -/// /// This behaves like a [List] but has the [SearchList.nextPage] to get the next batch of videos. +///This behaves like a [List] but has the [SearchList.nextPage] to get the next batch of videos. class SearchList extends DelegatingList