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:
parent
ec80924a28
commit
73504b2a44
|
@ -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
|
## 1.9.10
|
||||||
- Close #139: Implement Channel.subscribersCount.
|
- Close #139: Implement Channel.subscribersCount.
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,9 @@ import 'channel_id.dart';
|
||||||
part 'channel.freezed.dart';
|
part 'channel.freezed.dart';
|
||||||
|
|
||||||
/// YouTube channel metadata.
|
/// YouTube channel metadata.
|
||||||
@Freezed()
|
@freezed
|
||||||
class Channel with _$Channel {
|
class Channel with _$Channel {
|
||||||
const Channel._();
|
///
|
||||||
|
|
||||||
const factory Channel(
|
const factory Channel(
|
||||||
/// Channel ID.
|
/// Channel ID.
|
||||||
ChannelId id,
|
ChannelId id,
|
||||||
|
@ -25,4 +24,6 @@ class Channel with _$Channel {
|
||||||
|
|
||||||
/// Channel URL.
|
/// Channel URL.
|
||||||
String get url => 'https://www.youtube.com/channel/$id';
|
String get url => 'https://www.youtube.com/channel/$id';
|
||||||
|
|
||||||
|
const Channel._();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:youtube_explode_dart/src/channels/channel_uploads_list.dart';
|
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/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 '../common/common.dart';
|
||||||
import '../extensions/helpers_extension.dart';
|
import '../extensions/helpers_extension.dart';
|
||||||
|
@ -54,7 +54,7 @@ class ChannelClient {
|
||||||
channelId = ChannelId.fromString(channelId);
|
channelId = ChannelId.fromString(channelId);
|
||||||
|
|
||||||
final aboutPage = await ChannelAboutPage.get(_httpClient, channelId.value);
|
final aboutPage = await ChannelAboutPage.get(_httpClient, channelId.value);
|
||||||
final id = aboutPage.initialData;
|
|
||||||
return ChannelAbout(
|
return ChannelAbout(
|
||||||
aboutPage.description,
|
aboutPage.description,
|
||||||
aboutPage.viewCount,
|
aboutPage.viewCount,
|
||||||
|
@ -76,6 +76,8 @@ class ChannelClient {
|
||||||
|
|
||||||
var channelAboutPage =
|
var channelAboutPage =
|
||||||
await ChannelAboutPage.getByUsername(_httpClient, username.value);
|
await ChannelAboutPage.getByUsername(_httpClient, username.value);
|
||||||
|
|
||||||
|
// TODO: Expose metadata from the [ChannelAboutPage] class.
|
||||||
var id = channelAboutPage.initialData;
|
var id = channelAboutPage.initialData;
|
||||||
return ChannelAbout(
|
return ChannelAbout(
|
||||||
id.description,
|
id.description,
|
||||||
|
@ -94,9 +96,8 @@ class ChannelClient {
|
||||||
/// that uploaded the specified video.
|
/// that uploaded the specified video.
|
||||||
Future<Channel> getByVideo(dynamic videoId) async {
|
Future<Channel> getByVideo(dynamic videoId) async {
|
||||||
videoId = VideoId.fromString(videoId);
|
videoId = VideoId.fromString(videoId);
|
||||||
var videoInfoResponse =
|
var videoInfoResponse = await WatchPage.get(_httpClient, videoId.value);
|
||||||
await VideoInfoResponse.get(_httpClient, videoId.value);
|
var playerResponse = videoInfoResponse.playerResponse!;
|
||||||
var playerResponse = videoInfoResponse.playerResponse;
|
|
||||||
|
|
||||||
var channelId = playerResponse.videoChannelId;
|
var channelId = playerResponse.videoChannelId;
|
||||||
return get(ChannelId(channelId));
|
return get(ChannelId(channelId));
|
||||||
|
|
|
@ -80,10 +80,44 @@ extension StringUtility on String {
|
||||||
|
|
||||||
/// Utility for Strings.
|
/// Utility for Strings.
|
||||||
extension StringUtility2 on String? {
|
extension StringUtility2 on String? {
|
||||||
|
static final RegExp _unitSplit = RegExp(r'^(\d+(?:\.\d)?)(\w)');
|
||||||
|
|
||||||
/// Parses this value as int stripping the non digit characters,
|
/// Parses this value as int stripping the non digit characters,
|
||||||
/// returns null if this fails.
|
/// returns null if this fails.
|
||||||
int? parseInt() => int.tryParse(this?.stripNonDigits() ?? '');
|
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.
|
/// Returns true if the string is null or empty.
|
||||||
bool get isNullOrWhiteSpace {
|
bool get isNullOrWhiteSpace {
|
||||||
if (this == null) {
|
if (this == null) {
|
||||||
|
@ -235,19 +269,18 @@ extension RunsParser on List<dynamic> {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GenericExtract on List<String> {
|
extension GenericExtract on List<String> {
|
||||||
/// Used to extract initial data that start with `var ytInitialData = ` or 'window["ytInitialData"] ='.
|
/// Used to extract initial data.
|
||||||
T extractGenericData<T>(
|
T extractGenericData<T>(List<String> match,
|
||||||
T Function(Map<String, dynamic>) builder, Exception Function() orThrow) {
|
T Function(Map<String, dynamic>) builder, Exception Function() orThrow) {
|
||||||
var initialData =
|
JsonMap? initialData;
|
||||||
firstWhereOrNull((e) => e.contains('var ytInitialData = '))
|
|
||||||
?.extractJson('var ytInitialData = ');
|
|
||||||
initialData ??=
|
|
||||||
firstWhereOrNull((e) => e.contains('window["ytInitialData"] ='))
|
|
||||||
?.extractJson('window["ytInitialData"] =');
|
|
||||||
|
|
||||||
|
for (final m in match) {
|
||||||
|
initialData = firstWhereOrNull((e) => e.contains(m))?.extractJson(m);
|
||||||
if (initialData != null) {
|
if (initialData != null) {
|
||||||
return builder(initialData);
|
return builder(initialData);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw orThrow();
|
throw orThrow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ abstract class YoutubePage<T extends InitialData> {
|
||||||
.map((e) => e.text)
|
.map((e) => e.text)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
return scriptText.extractGenericData(
|
return scriptText.extractGenericData(
|
||||||
|
['var ytInitialData = ', 'window["ytInitialData"] ='],
|
||||||
initialDataBuilder!,
|
initialDataBuilder!,
|
||||||
() => TransientFailureException(
|
() => TransientFailureException(
|
||||||
'Failed to retrieve initial data from $runtimeType, please report this to the project GitHub page.'));
|
'Failed to retrieve initial data from $runtimeType, please report this to the project GitHub page.'));
|
||||||
|
|
|
@ -93,6 +93,8 @@ class WatchPage extends YoutubePage<_InitialData> {
|
||||||
.nullIfWhitespace ??
|
.nullIfWhitespace ??
|
||||||
'0');
|
'0');
|
||||||
|
|
||||||
|
String? get commentsContinuation => initialData.commentsContinuation;
|
||||||
|
|
||||||
static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\})');
|
static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\})');
|
||||||
|
|
||||||
late final WatchPlayerConfig? playerConfig = getPlayerConfig();
|
late final WatchPlayerConfig? playerConfig = getPlayerConfig();
|
||||||
|
@ -111,18 +113,16 @@ class WatchPage extends YoutubePage<_InitialData> {
|
||||||
return WatchPlayerConfig(jsonMap);
|
return WatchPlayerConfig(jsonMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
PlayerResponse? getPlayerResponse() {
|
PlayerResponse? getPlayerResponse() {
|
||||||
final val = root
|
final scriptText = root
|
||||||
.querySelectorAll('script')
|
.querySelectorAll('script')
|
||||||
.map((e) => e.text)
|
.map((e) => e.text)
|
||||||
.map((e) => _playerResponseExp.firstMatch(e)?.group(1))
|
.toList(growable: false);
|
||||||
.firstWhereOrNull((e) => !e.isNullOrWhiteSpace)
|
return scriptText.extractGenericData(
|
||||||
?.extractJson();
|
['var ytInitialPlayerResponse = '],
|
||||||
if (val == null) {
|
(root) => PlayerResponse(root),
|
||||||
return null;
|
() => TransientFailureException(
|
||||||
}
|
'Failed to retrieve initial player response, please report this to the project GitHub page.'));
|
||||||
return PlayerResponse(val);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@ -183,16 +183,15 @@ class _InitialData extends InitialData {
|
||||||
?.getList('contents')
|
?.getList('contents')
|
||||||
?.firstWhere((e) => e['itemSectionRenderer'] != null)
|
?.firstWhere((e) => e['itemSectionRenderer'] != null)
|
||||||
.get('itemSectionRenderer')
|
.get('itemSectionRenderer')
|
||||||
?.getList('continuations')
|
?.getList('contents')
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.get('nextContinuationData');
|
?.get('continuationItemRenderer')
|
||||||
|
?.get('continuationEndpoint')
|
||||||
|
?.get('continuationCommand');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
late final String continuation =
|
late final String commentsContinuation =
|
||||||
getContinuationContext()?.getT<String>('continuation') ?? '';
|
getContinuationContext()?.getT<String>('token') ?? '';
|
||||||
|
|
||||||
late final String clickTrackingParams =
|
|
||||||
getContinuationContext()?.getT<String>('clickTrackingParams') ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import '../../retry.dart';
|
||||||
import '../youtube_http_client.dart';
|
import '../youtube_http_client.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
class ClosedCaptionTrackResponse {
|
class ClosedCaptionClient {
|
||||||
final xml.XmlDocument root;
|
final xml.XmlDocument root;
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@ -13,19 +13,19 @@ class ClosedCaptionTrackResponse {
|
||||||
root.findAllElements('p').map((e) => ClosedCaption._(e));
|
root.findAllElements('p').map((e) => ClosedCaption._(e));
|
||||||
|
|
||||||
///
|
///
|
||||||
ClosedCaptionTrackResponse(this.root);
|
ClosedCaptionClient(this.root);
|
||||||
|
|
||||||
///
|
///
|
||||||
// ignore: deprecated_member_use
|
// 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) {
|
YoutubeHttpClient httpClient, Uri url) {
|
||||||
var formatUrl = url.replaceQueryParameters({'fmt': 'srv3'});
|
final formatUrl = url.replaceQueryParameters({'fmt': 'srv3'});
|
||||||
return retry(() async {
|
return retry(() async {
|
||||||
var raw = await httpClient.getString(formatUrl);
|
var raw = await httpClient.getString(formatUrl);
|
||||||
return ClosedCaptionTrackResponse.parse(raw);
|
return ClosedCaptionClient.parse(raw);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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';
|
||||||
|
}
|
|
@ -8,7 +8,9 @@ import '../player/player_response.dart';
|
||||||
import '../models/stream_info_provider.dart';
|
import '../models/stream_info_provider.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
class VideoInfoResponse {
|
///
|
||||||
|
@deprecated
|
||||||
|
class VideoInfoClient {
|
||||||
final Map<String, String> root;
|
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,
|
YoutubeHttpClient httpClient, String videoId,
|
||||||
[String? sts]) {
|
[String? sts]) {
|
||||||
var eurl = Uri.encodeFull('https://youtube.googleapis.com/v/$videoId');
|
var eurl = Uri.encodeFull('https://youtube.googleapis.com/v/$videoId');
|
||||||
|
@ -71,7 +73,7 @@ class VideoInfoResponse {
|
||||||
|
|
||||||
return retry(() async {
|
return retry(() async {
|
||||||
var raw = await httpClient.getString(url);
|
var raw = await httpClient.getString(url);
|
||||||
var result = VideoInfoResponse.parse(raw);
|
var result = VideoInfoClient.parse(raw);
|
||||||
|
|
||||||
if (!result.isVideoAvailable || !result.playerResponse.isVideoAvailable) {
|
if (!result.isVideoAvailable || !result.playerResponse.isVideoAvailable) {
|
||||||
throw VideoUnplayableException(videoId);
|
throw VideoUnplayableException(videoId);
|
|
@ -1,7 +1,7 @@
|
||||||
import '../../extensions/helpers_extension.dart';
|
import '../../extensions/helpers_extension.dart';
|
||||||
import '../../reverse_engineering/responses/closed_caption_track_response.dart'
|
import '../../reverse_engineering/responses/closed_caption_client.dart' as re
|
||||||
show ClosedCaptionTrackResponse;
|
show ClosedCaptionClient;
|
||||||
import '../../reverse_engineering/responses/video_info_response.dart';
|
import '../../reverse_engineering/responses/video_info_client.dart';
|
||||||
import '../../reverse_engineering/youtube_http_client.dart';
|
import '../../reverse_engineering/youtube_http_client.dart';
|
||||||
import '../videos.dart';
|
import '../videos.dart';
|
||||||
import 'closed_caption.dart';
|
import 'closed_caption.dart';
|
||||||
|
@ -35,7 +35,7 @@ class ClosedCaptionClient {
|
||||||
videoId = VideoId.fromString(videoId);
|
videoId = VideoId.fromString(videoId);
|
||||||
var tracks = <ClosedCaptionTrackInfo>{};
|
var tracks = <ClosedCaptionTrackInfo>{};
|
||||||
var videoInfoResponse =
|
var videoInfoResponse =
|
||||||
await VideoInfoResponse.get(_httpClient, videoId.value);
|
await VideoInfoClient.get(_httpClient, videoId.value);
|
||||||
var playerResponse = videoInfoResponse.playerResponse;
|
var playerResponse = videoInfoResponse.playerResponse;
|
||||||
|
|
||||||
for (final track in playerResponse.closedCaptionTrack) {
|
for (final track in playerResponse.closedCaptionTrack) {
|
||||||
|
@ -54,8 +54,7 @@ class ClosedCaptionClient {
|
||||||
/// Gets the actual closed caption track which is
|
/// Gets the actual closed caption track which is
|
||||||
/// identified by the specified metadata.
|
/// identified by the specified metadata.
|
||||||
Future<ClosedCaptionTrack> get(ClosedCaptionTrackInfo trackInfo) async {
|
Future<ClosedCaptionTrack> get(ClosedCaptionTrackInfo trackInfo) async {
|
||||||
var response =
|
var response = await re.ClosedCaptionClient.get(_httpClient, trackInfo.url);
|
||||||
await ClosedCaptionTrackResponse.get(_httpClient, trackInfo.url);
|
|
||||||
|
|
||||||
var captions = response.closedCaptions
|
var captions = response.closedCaptions
|
||||||
.where((e) => !e.text.isNullOrWhiteSpace)
|
.where((e) => !e.text.isNullOrWhiteSpace)
|
||||||
|
|
|
@ -9,9 +9,6 @@ part 'comment.freezed.dart';
|
||||||
class Comment with _$Comment {
|
class Comment with _$Comment {
|
||||||
/// Initializes an instance of [Comment]
|
/// Initializes an instance of [Comment]
|
||||||
const factory Comment(
|
const factory Comment(
|
||||||
/// Comment id.
|
|
||||||
String commentId,
|
|
||||||
|
|
||||||
/// Comment author name.
|
/// Comment author name.
|
||||||
String author,
|
String author,
|
||||||
|
|
||||||
|
@ -33,9 +30,5 @@ class Comment with _$Comment {
|
||||||
/// Used internally.
|
/// Used internally.
|
||||||
/// Shouldn't be used in the code.
|
/// Shouldn't be used in the code.
|
||||||
@internal String? continuation,
|
@internal String? continuation,
|
||||||
|
|
||||||
/// Used internally.
|
|
||||||
/// Shouldn't be used in the code.
|
|
||||||
@internal String? clicktrackingParams,
|
|
||||||
) = _Comment;
|
) = _Comment;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,18 +16,9 @@ final _privateConstructorUsedError = UnsupportedError(
|
||||||
class _$CommentTearOff {
|
class _$CommentTearOff {
|
||||||
const _$CommentTearOff();
|
const _$CommentTearOff();
|
||||||
|
|
||||||
_Comment call(
|
_Comment call(String author, ChannelId channelId, String text, int likeCount,
|
||||||
String commentId,
|
String publishedTime, int replyCount, @internal String? continuation) {
|
||||||
String author,
|
|
||||||
ChannelId channelId,
|
|
||||||
String text,
|
|
||||||
int likeCount,
|
|
||||||
String publishedTime,
|
|
||||||
int replyCount,
|
|
||||||
@internal String? continuation,
|
|
||||||
@internal String? clicktrackingParams) {
|
|
||||||
return _Comment(
|
return _Comment(
|
||||||
commentId,
|
|
||||||
author,
|
author,
|
||||||
channelId,
|
channelId,
|
||||||
text,
|
text,
|
||||||
|
@ -35,7 +26,6 @@ class _$CommentTearOff {
|
||||||
publishedTime,
|
publishedTime,
|
||||||
replyCount,
|
replyCount,
|
||||||
continuation,
|
continuation,
|
||||||
clicktrackingParams,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,9 +35,6 @@ const $Comment = _$CommentTearOff();
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$Comment {
|
mixin _$Comment {
|
||||||
/// Comment id.
|
|
||||||
String get commentId => throw _privateConstructorUsedError;
|
|
||||||
|
|
||||||
/// Comment author name.
|
/// Comment author name.
|
||||||
String get author => throw _privateConstructorUsedError;
|
String get author => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
@ -71,11 +58,6 @@ mixin _$Comment {
|
||||||
@internal
|
@internal
|
||||||
String? get continuation => throw _privateConstructorUsedError;
|
String? get continuation => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Used internally.
|
|
||||||
/// Shouldn't be used in the code.
|
|
||||||
@internal
|
|
||||||
String? get clicktrackingParams => throw _privateConstructorUsedError;
|
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$CommentCopyWith<Comment> get copyWith => throw _privateConstructorUsedError;
|
$CommentCopyWith<Comment> get copyWith => throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
@ -85,15 +67,13 @@ abstract class $CommentCopyWith<$Res> {
|
||||||
factory $CommentCopyWith(Comment value, $Res Function(Comment) then) =
|
factory $CommentCopyWith(Comment value, $Res Function(Comment) then) =
|
||||||
_$CommentCopyWithImpl<$Res>;
|
_$CommentCopyWithImpl<$Res>;
|
||||||
$Res call(
|
$Res call(
|
||||||
{String commentId,
|
{String author,
|
||||||
String author,
|
|
||||||
ChannelId channelId,
|
ChannelId channelId,
|
||||||
String text,
|
String text,
|
||||||
int likeCount,
|
int likeCount,
|
||||||
String publishedTime,
|
String publishedTime,
|
||||||
int replyCount,
|
int replyCount,
|
||||||
@internal String? continuation,
|
@internal String? continuation});
|
||||||
@internal String? clicktrackingParams});
|
|
||||||
|
|
||||||
$ChannelIdCopyWith<$Res> get channelId;
|
$ChannelIdCopyWith<$Res> get channelId;
|
||||||
}
|
}
|
||||||
|
@ -108,7 +88,6 @@ class _$CommentCopyWithImpl<$Res> implements $CommentCopyWith<$Res> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? commentId = freezed,
|
|
||||||
Object? author = freezed,
|
Object? author = freezed,
|
||||||
Object? channelId = freezed,
|
Object? channelId = freezed,
|
||||||
Object? text = freezed,
|
Object? text = freezed,
|
||||||
|
@ -116,13 +95,8 @@ class _$CommentCopyWithImpl<$Res> implements $CommentCopyWith<$Res> {
|
||||||
Object? publishedTime = freezed,
|
Object? publishedTime = freezed,
|
||||||
Object? replyCount = freezed,
|
Object? replyCount = freezed,
|
||||||
Object? continuation = freezed,
|
Object? continuation = freezed,
|
||||||
Object? clicktrackingParams = freezed,
|
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
commentId: commentId == freezed
|
|
||||||
? _value.commentId
|
|
||||||
: commentId // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
author: author == freezed
|
author: author == freezed
|
||||||
? _value.author
|
? _value.author
|
||||||
: author // ignore: cast_nullable_to_non_nullable
|
: author // ignore: cast_nullable_to_non_nullable
|
||||||
|
@ -151,10 +125,6 @@ class _$CommentCopyWithImpl<$Res> implements $CommentCopyWith<$Res> {
|
||||||
? _value.continuation
|
? _value.continuation
|
||||||
: continuation // ignore: cast_nullable_to_non_nullable
|
: continuation // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
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>;
|
__$CommentCopyWithImpl<$Res>;
|
||||||
@override
|
@override
|
||||||
$Res call(
|
$Res call(
|
||||||
{String commentId,
|
{String author,
|
||||||
String author,
|
|
||||||
ChannelId channelId,
|
ChannelId channelId,
|
||||||
String text,
|
String text,
|
||||||
int likeCount,
|
int likeCount,
|
||||||
String publishedTime,
|
String publishedTime,
|
||||||
int replyCount,
|
int replyCount,
|
||||||
@internal String? continuation,
|
@internal String? continuation});
|
||||||
@internal String? clicktrackingParams});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
$ChannelIdCopyWith<$Res> get channelId;
|
$ChannelIdCopyWith<$Res> get channelId;
|
||||||
|
@ -197,7 +165,6 @@ class __$CommentCopyWithImpl<$Res> extends _$CommentCopyWithImpl<$Res>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? commentId = freezed,
|
|
||||||
Object? author = freezed,
|
Object? author = freezed,
|
||||||
Object? channelId = freezed,
|
Object? channelId = freezed,
|
||||||
Object? text = freezed,
|
Object? text = freezed,
|
||||||
|
@ -205,13 +172,8 @@ class __$CommentCopyWithImpl<$Res> extends _$CommentCopyWithImpl<$Res>
|
||||||
Object? publishedTime = freezed,
|
Object? publishedTime = freezed,
|
||||||
Object? replyCount = freezed,
|
Object? replyCount = freezed,
|
||||||
Object? continuation = freezed,
|
Object? continuation = freezed,
|
||||||
Object? clicktrackingParams = freezed,
|
|
||||||
}) {
|
}) {
|
||||||
return _then(_Comment(
|
return _then(_Comment(
|
||||||
commentId == freezed
|
|
||||||
? _value.commentId
|
|
||||||
: commentId // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String,
|
|
||||||
author == freezed
|
author == freezed
|
||||||
? _value.author
|
? _value.author
|
||||||
: author // ignore: cast_nullable_to_non_nullable
|
: author // ignore: cast_nullable_to_non_nullable
|
||||||
|
@ -240,10 +202,6 @@ class __$CommentCopyWithImpl<$Res> extends _$CommentCopyWithImpl<$Res>
|
||||||
? _value.continuation
|
? _value.continuation
|
||||||
: continuation // ignore: cast_nullable_to_non_nullable
|
: continuation // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
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
|
/// @nodoc
|
||||||
|
|
||||||
class _$_Comment implements _Comment {
|
class _$_Comment implements _Comment {
|
||||||
const _$_Comment(
|
const _$_Comment(this.author, this.channelId, this.text, this.likeCount,
|
||||||
this.commentId,
|
this.publishedTime, this.replyCount, @internal this.continuation);
|
||||||
this.author,
|
|
||||||
this.channelId,
|
|
||||||
this.text,
|
|
||||||
this.likeCount,
|
|
||||||
this.publishedTime,
|
|
||||||
this.replyCount,
|
|
||||||
@internal this.continuation,
|
|
||||||
@internal this.clicktrackingParams);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
||||||
/// Comment id.
|
|
||||||
final String commentId;
|
|
||||||
@override
|
|
||||||
|
|
||||||
/// Comment author name.
|
/// Comment author name.
|
||||||
final String author;
|
final String author;
|
||||||
@override
|
@override
|
||||||
|
@ -296,25 +242,16 @@ class _$_Comment implements _Comment {
|
||||||
/// Shouldn't be used in the code.
|
/// Shouldn't be used in the code.
|
||||||
@internal
|
@internal
|
||||||
final String? continuation;
|
final String? continuation;
|
||||||
@override
|
|
||||||
|
|
||||||
/// Used internally.
|
|
||||||
/// Shouldn't be used in the code.
|
|
||||||
@internal
|
|
||||||
final String? clicktrackingParams;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other is _Comment &&
|
(other is _Comment &&
|
||||||
(identical(other.commentId, commentId) ||
|
|
||||||
const DeepCollectionEquality()
|
|
||||||
.equals(other.commentId, commentId)) &&
|
|
||||||
(identical(other.author, author) ||
|
(identical(other.author, author) ||
|
||||||
const DeepCollectionEquality().equals(other.author, author)) &&
|
const DeepCollectionEquality().equals(other.author, author)) &&
|
||||||
(identical(other.channelId, channelId) ||
|
(identical(other.channelId, channelId) ||
|
||||||
|
@ -333,24 +270,19 @@ class _$_Comment implements _Comment {
|
||||||
.equals(other.replyCount, replyCount)) &&
|
.equals(other.replyCount, replyCount)) &&
|
||||||
(identical(other.continuation, continuation) ||
|
(identical(other.continuation, continuation) ||
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.continuation, continuation)) &&
|
.equals(other.continuation, continuation)));
|
||||||
(identical(other.clicktrackingParams, clicktrackingParams) ||
|
|
||||||
const DeepCollectionEquality()
|
|
||||||
.equals(other.clicktrackingParams, clicktrackingParams)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
runtimeType.hashCode ^
|
runtimeType.hashCode ^
|
||||||
const DeepCollectionEquality().hash(commentId) ^
|
|
||||||
const DeepCollectionEquality().hash(author) ^
|
const DeepCollectionEquality().hash(author) ^
|
||||||
const DeepCollectionEquality().hash(channelId) ^
|
const DeepCollectionEquality().hash(channelId) ^
|
||||||
const DeepCollectionEquality().hash(text) ^
|
const DeepCollectionEquality().hash(text) ^
|
||||||
const DeepCollectionEquality().hash(likeCount) ^
|
const DeepCollectionEquality().hash(likeCount) ^
|
||||||
const DeepCollectionEquality().hash(publishedTime) ^
|
const DeepCollectionEquality().hash(publishedTime) ^
|
||||||
const DeepCollectionEquality().hash(replyCount) ^
|
const DeepCollectionEquality().hash(replyCount) ^
|
||||||
const DeepCollectionEquality().hash(continuation) ^
|
const DeepCollectionEquality().hash(continuation);
|
||||||
const DeepCollectionEquality().hash(clicktrackingParams);
|
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
|
@ -360,22 +292,16 @@ class _$_Comment implements _Comment {
|
||||||
|
|
||||||
abstract class _Comment implements Comment {
|
abstract class _Comment implements Comment {
|
||||||
const factory _Comment(
|
const factory _Comment(
|
||||||
String commentId,
|
|
||||||
String author,
|
String author,
|
||||||
ChannelId channelId,
|
ChannelId channelId,
|
||||||
String text,
|
String text,
|
||||||
int likeCount,
|
int likeCount,
|
||||||
String publishedTime,
|
String publishedTime,
|
||||||
int replyCount,
|
int replyCount,
|
||||||
@internal String? continuation,
|
@internal String? continuation) = _$_Comment;
|
||||||
@internal String? clicktrackingParams) = _$_Comment;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
||||||
/// Comment id.
|
|
||||||
String get commentId => throw _privateConstructorUsedError;
|
|
||||||
@override
|
|
||||||
|
|
||||||
/// Comment author name.
|
/// Comment author name.
|
||||||
String get author => throw _privateConstructorUsedError;
|
String get author => throw _privateConstructorUsedError;
|
||||||
@override
|
@override
|
||||||
|
@ -405,12 +331,6 @@ abstract class _Comment implements Comment {
|
||||||
@internal
|
@internal
|
||||||
String? get continuation => throw _privateConstructorUsedError;
|
String? get continuation => throw _privateConstructorUsedError;
|
||||||
@override
|
@override
|
||||||
|
|
||||||
/// Used internally.
|
|
||||||
/// Shouldn't be used in the code.
|
|
||||||
@internal
|
|
||||||
String? get clicktrackingParams => throw _privateConstructorUsedError;
|
|
||||||
@override
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$CommentCopyWith<_Comment> get copyWith =>
|
_$CommentCopyWith<_Comment> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
|
|
|
@ -1,47 +1,20 @@
|
||||||
import 'dart:convert';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
import '../../channels/channel_id.dart';
|
import '../../channels/channel_id.dart';
|
||||||
import '../../extensions/helpers_extension.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 '../../reverse_engineering/youtube_http_client.dart';
|
||||||
import '../videos.dart';
|
import '../videos.dart';
|
||||||
import 'comment.dart';
|
import 'comment.dart';
|
||||||
|
|
||||||
/// Queries related to comments of YouTube videos.
|
/// Queries related to comments of YouTube videos.
|
||||||
|
@experimental
|
||||||
class CommentsClient {
|
class CommentsClient {
|
||||||
final YoutubeHttpClient _httpClient;
|
final YoutubeHttpClient _httpClient;
|
||||||
|
|
||||||
/// Initializes an instance of [CommentsClient]
|
/// Initializes an instance of [CommentsClient]
|
||||||
CommentsClient(this._httpClient);
|
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.
|
/// Returns a stream emitting all the [video]'s comment.
|
||||||
/// A request is page for every comment page,
|
/// A request is page for every comment page,
|
||||||
/// a page contains at most 20 comments, use .take if you want to limit
|
/// 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.
|
/// The streams doesn't emit any data if [Video.hasWatchPage] is false.
|
||||||
/// Use `videos.get(videoId, forceWatchPage: true)` to assure that the
|
/// Use `videos.get(videoId, forceWatchPage: true)` to assure that the
|
||||||
/// WatchPage is fetched.
|
/// WatchPage is fetched.
|
||||||
Stream<Comment> getComments(Video video) async* {
|
Future<List<Comment>> getComments(Video video) async {
|
||||||
if (video.watchPage == null) {
|
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,
|
final page = await re.CommentsClient.get(_httpClient, video);
|
||||||
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')!;
|
|
||||||
|
|
||||||
continuation = continuationData.getT<String>('continuation');
|
return page?.comments
|
||||||
clickTrackingParams =
|
.map((e) => Comment(
|
||||||
continuationData.getT<String>('clickTrackingParams');
|
e.author,
|
||||||
}
|
ChannelId(e.channelId),
|
||||||
yield Comment(
|
e.text,
|
||||||
commentRaw.getT<String>('commentId')!,
|
e.likeCount ?? 0,
|
||||||
commentRaw.get('authorText')!.getT<String>('simpleText')!,
|
e.publishTime,
|
||||||
ChannelId(commentRaw
|
e.repliesCount ?? 0,
|
||||||
.get('authorEndpoint')!
|
e.continuation))
|
||||||
.get('browseEndpoint')!
|
.toList(growable: false) ??
|
||||||
.getT<String>('browseId')!),
|
const [];
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import '../../exceptions/exceptions.dart';
|
import '../../exceptions/exceptions.dart';
|
||||||
import '../../extensions/helpers_extension.dart';
|
import '../../extensions/helpers_extension.dart';
|
||||||
import '../../reverse_engineering/cipher/cipher_operations.dart';
|
import '../../reverse_engineering/cipher/cipher_operations.dart';
|
||||||
|
import '../../reverse_engineering/dash_manifest.dart';
|
||||||
import '../../reverse_engineering/heuristics.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/embed_page.dart';
|
||||||
import '../../reverse_engineering/pages/watch_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/player/player_source.dart';
|
||||||
import '../../reverse_engineering/models/stream_info_provider.dart';
|
import '../../reverse_engineering/responses/video_info_client.dart';
|
||||||
import '../../reverse_engineering/responses/video_info_response.dart';
|
|
||||||
import '../../reverse_engineering/youtube_http_client.dart';
|
import '../../reverse_engineering/youtube_http_client.dart';
|
||||||
import '../video_id.dart';
|
import '../video_id.dart';
|
||||||
import 'bitrate.dart';
|
import 'bitrate.dart';
|
||||||
|
@ -48,7 +48,7 @@ class StreamsClient {
|
||||||
_httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl);
|
_httpClient, embedPage.sourceUrl ?? playerConfig.sourceUrl);
|
||||||
var cipherOperations = playerSource.getCipherOperations();
|
var cipherOperations = playerSource.getCipherOperations();
|
||||||
|
|
||||||
var videoInfoResponse = await VideoInfoResponse.get(
|
var videoInfoResponse = await VideoInfoClient.get(
|
||||||
_httpClient, videoId.toString(), playerSource.sts);
|
_httpClient, videoId.toString(), playerSource.sts);
|
||||||
var playerResponse = videoInfoResponse.playerResponse;
|
var playerResponse = videoInfoResponse.playerResponse;
|
||||||
|
|
||||||
|
@ -224,21 +224,9 @@ class StreamsClient {
|
||||||
/// about available streams in the specified video.
|
/// about available streams in the specified video.
|
||||||
Future<StreamManifest> getManifest(dynamic videoId) async {
|
Future<StreamManifest> getManifest(dynamic videoId) async {
|
||||||
videoId = VideoId.fromString(videoId);
|
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);
|
var context = await _getStreamContextFromWatchPage(videoId);
|
||||||
return _getManifest(context);
|
return _getManifest(context);
|
||||||
} on YoutubeExplodeException catch (e1) {
|
|
||||||
throw e..combine(e1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the HTTP Live Stream (HLS) manifest URL
|
/// Gets the HTTP Live Stream (HLS) manifest URL
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
import 'package:youtube_explode_dart/src/reverse_engineering/player/player_response.dart';
|
||||||
|
|
||||||
import '../channels/channel_id.dart';
|
import '../channels/channel_id.dart';
|
||||||
import '../common/common.dart';
|
import '../common/common.dart';
|
||||||
import '../extensions/helpers_extension.dart';
|
import '../extensions/helpers_extension.dart';
|
||||||
import '../reverse_engineering/pages/watch_page.dart';
|
import '../reverse_engineering/pages/watch_page.dart';
|
||||||
import '../reverse_engineering/responses/video_info_response.dart';
|
|
||||||
import '../reverse_engineering/youtube_http_client.dart';
|
import '../reverse_engineering/youtube_http_client.dart';
|
||||||
import 'closed_captions/closed_caption_client.dart';
|
import 'closed_captions/closed_caption_client.dart';
|
||||||
import 'comments/comments_client.dart';
|
import 'comments/comments_client.dart';
|
||||||
|
@ -29,11 +30,9 @@ class VideoClient {
|
||||||
|
|
||||||
/// Gets the metadata associated with the specified video.
|
/// Gets the metadata associated with the specified video.
|
||||||
Future<Video> _getVideoFromWatchPage(VideoId videoId) async {
|
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);
|
var watchPage = await WatchPage.get(_httpClient, videoId.value);
|
||||||
|
final playerResponse = watchPage.playerResponse!;
|
||||||
|
|
||||||
return Video(
|
return Video(
|
||||||
videoId,
|
videoId,
|
||||||
playerResponse.videoTitle,
|
playerResponse.videoTitle,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: youtube_explode_dart
|
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.
|
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
|
homepage: https://github.com/Hexer10/youtube_explode_dart
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ void main() {
|
||||||
test('Get comments of a video', () async {
|
test('Get comments of a video', () async {
|
||||||
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
|
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
|
||||||
var video = await yt!.videos.get(VideoId(videoUrl));
|
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));
|
expect(comments.length, greaterThanOrEqualTo(1));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue