Code refactoring

This commit is contained in:
Mattia 2021-07-21 02:06:02 +02:00
parent 3b530c7ee4
commit ec80924a28
40 changed files with 778 additions and 421 deletions

View File

@ -1,11 +1,12 @@
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 '../common/common.dart';
import '../extensions/helpers_extension.dart';
import '../playlists/playlists.dart';
import '../reverse_engineering/responses/channel_about_page.dart';
import '../reverse_engineering/responses/channel_upload_page.dart';
import '../reverse_engineering/responses/responses.dart';
import '../reverse_engineering/pages/channel_about_page.dart';
import '../reverse_engineering/pages/channel_upload_page.dart';
import '../reverse_engineering/youtube_http_client.dart';
import '../videos/video.dart';
import '../videos/video_id.dart';
@ -55,16 +56,16 @@ class ChannelClient {
final aboutPage = await ChannelAboutPage.get(_httpClient, channelId.value);
final id = aboutPage.initialData;
return ChannelAbout(
id.description,
id.viewCount,
id.joinDate,
id.title,
aboutPage.description,
aboutPage.viewCount,
aboutPage.joinDate,
aboutPage.title,
[
for (var e in id.avatar)
for (var e in aboutPage.avatar)
Thumbnail(Uri.parse(e['url']), e['height'], e['width'])
],
id.country,
id.channelLinks);
aboutPage.country,
aboutPage.channelLinks);
}
/// Gets the info found on a YouTube Channel About page.
@ -125,7 +126,7 @@ class ChannelClient {
final channel = await get(channelId);
return ChannelUploadsList(
page.initialData.uploads
page.uploads
.map((e) => Video(
e.videoId,
e.videoTitle,

View File

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/responses/channel_upload_page.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/pages/channel_upload_page.dart';
import '../../youtube_explode_dart.dart';
import '../extensions/helpers_extension.dart';

View File

@ -5,6 +5,8 @@ import 'package:collection/collection.dart';
import '../reverse_engineering/cipher/cipher_operations.dart';
typedef JsonMap = Map<String, dynamic>;
/// Utility for Strings.
extension StringUtility on String {
/// Returns null if this string is whitespace.

View File

@ -1,4 +1,3 @@
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../common/common.dart';

View File

@ -1,5 +1,5 @@
import 'package:youtube_explode_dart/src/channels/channel_id.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/responses/playlist_page.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/pages/playlist_page.dart';
import '../common/common.dart';
import '../reverse_engineering/youtube_http_client.dart';
@ -22,25 +22,51 @@ class PlaylistClient {
var response = await PlaylistPage.get(_httpClient, id.value);
return Playlist(
id,
response.initialData.title ?? '',
response.initialData.author ?? '',
response.initialData.description ?? '',
response.title ?? '',
response.author ?? '',
response.description ?? '',
ThumbnailSet(id.value),
Engagement(response.initialData.viewCount ?? 0, null, null));
Engagement(response.viewCount ?? 0, null, null));
}
/// Enumerates videos included in the specified playlist.
Stream<Video> getVideos(dynamic id) async* {
id = PlaylistId.fromString(id);
var encounteredVideoIds = <String>{};
String? continuationToken = '';
final encounteredVideoIds = <String>{};
// ignore: literal_only_boolean_expressions
while (true) {
var response = await PlaylistPage.get(_httpClient, id.value,
token: continuationToken);
PlaylistPage? page = await PlaylistPage.get(_httpClient, id.value);
for (final video in response.initialData.playlistVideos) {
for (final video in page.videos) {
var videoId = video.id;
// Already added
if (!encounteredVideoIds.add(videoId)) {
continue;
}
if (video.channelId.isEmpty) {
continue;
}
yield Video(
VideoId(videoId),
video.title,
video.author,
ChannelId(video.channelId),
null,
null,
video.description,
video.duration,
ThumbnailSet(videoId),
null,
Engagement(video.viewCount, null, null),
false);
}
page = await page.nextPage(_httpClient);
while (page != null) {
for (final video in page.videos) {
var videoId = video.id;
// Already added
@ -66,10 +92,6 @@ class PlaylistClient {
Engagement(video.viewCount, null, null),
false);
}
continuationToken = response.initialData.continuationToken;
if (response.initialData.continuationToken?.isEmpty ?? true) {
break;
}
}
}
}

View File

@ -1,8 +1,8 @@
import 'package:xml/xml.dart' as xml;
import '../../retry.dart';
import '../youtube_http_client.dart';
import 'stream_info_provider.dart';
import '../retry.dart';
import 'youtube_http_client.dart';
import 'models/stream_info_provider.dart';
///
class DashManifest {

View File

@ -0,0 +1,10 @@
import 'package:meta/meta.dart';
import '../../extensions/helpers_extension.dart';
abstract class InitialData {
@protected
final JsonMap root;
InitialData(this.root);
}

View File

@ -0,0 +1,34 @@
import 'package:html/dom.dart';
import 'package:meta/meta.dart';
import '../../../youtube_explode_dart.dart';
import '../../extensions/helpers_extension.dart';
import 'initial_data.dart';
abstract class YoutubePage<T extends InitialData> {
@protected
final Document? root;
@protected
late final T initialData = _defaultInitialData ?? _getInitialData();
final T? _defaultInitialData;
@protected
final T Function(JsonMap)? initialDataBuilder;
T _getInitialData() {
final scriptText = root!
.querySelectorAll('script')
.map((e) => e.text)
.toList(growable: false);
return scriptText.extractGenericData(
initialDataBuilder!,
() => TransientFailureException(
'Failed to retrieve initial data from $runtimeType, please report this to the project GitHub page.'));
}
YoutubePage(this.root, this.initialDataBuilder, [this._defaultInitialData])
: assert((root != null && initialDataBuilder != null) ||
_defaultInitialData != null);
}

View File

@ -1,39 +1,39 @@
import 'package:collection/collection.dart';
import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser;
import 'package:youtube_explode_dart/src/reverse_engineering/models/youtube_page.dart';
import '../../../youtube_explode_dart.dart';
import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../models/initial_data.dart';
import '../youtube_http_client.dart';
///
class ChannelAboutPage {
final Document _root;
///
late final _InitialData initialData = _getInitialData();
_InitialData _getInitialData() {
final scriptText = _root
.querySelectorAll('script')
.map((e) => e.text)
.toList(growable: false);
return scriptText.extractGenericData(
(obj) => _InitialData(obj),
() => TransientFailureException(
'Failed to retrieve initial data from the channel about page, please report this to the project GitHub page.'));
}
class ChannelAboutPage extends YoutubePage<_InitialData> {
///
String get description => initialData.description;
///
ChannelAboutPage(this._root);
int get viewCount => initialData.viewCount;
///
ChannelAboutPage.parse(String raw) : _root = parser.parse(raw);
String get joinDate => initialData.joinDate;
///
String get title => initialData.title;
///
List<JsonMap> get avatar => initialData.avatar;
///
String get country => initialData.country;
///
List<ChannelLink> get channelLinks => initialData.channelLinks;
///
ChannelAboutPage.parse(String raw)
: super(parser.parse(raw), (root) => _InitialData(root));
///
static Future<ChannelAboutPage> get(YoutubeHttpClient httpClient, String id) {
@ -63,15 +63,12 @@ class ChannelAboutPage {
final _urlExp = RegExp(r'q=([^=]*)$');
class _InitialData {
// Json parsed map
final Map<String, dynamic> root;
class _InitialData extends InitialData {
late final JsonMap content = _getContentContext();
_InitialData(this.root);
_InitialData(JsonMap root) : super(root);
late final Map<String, dynamic> content = _getContentContext();
Map<String, dynamic> _getContentContext() {
JsonMap _getContentContext() {
return root
.get('contents')!
.get('twoColumnBrowseResultsRenderer')!
@ -123,7 +120,7 @@ class _InitialData {
late final String title = content.get('title')!.getT<String>('simpleText')!;
late final List<Map<String, dynamic>> avatar =
late final List<JsonMap> avatar =
content.get('avatar')!.getList('thumbnails')!;
late final String country =

View File

@ -1,21 +1,20 @@
import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser;
import 'package:youtube_explode_dart/src/reverse_engineering/models/youtube_page.dart';
import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../youtube_http_client.dart';
import '../models/initial_data.dart';
///
class ChannelPage {
final Document _root;
class ChannelPage extends YoutubePage<_InitialData> {
///
bool get isOk => _root.querySelector('meta[property="og:url"]') != null;
bool get isOk => root!.querySelector('meta[property="og:url"]') != null;
///
String get channelUrl =>
_root.querySelector('meta[property="og:url"]')?.attributes['content'] ??
root!.querySelector('meta[property="og:url"]')?.attributes['content'] ??
'';
///
@ -23,35 +22,19 @@ class ChannelPage {
///
String get channelTitle =>
_root.querySelector('meta[property="og:title"]')?.attributes['content'] ??
root!.querySelector('meta[property="og:title"]')?.attributes['content'] ??
'';
///
String get channelLogoUrl =>
_root.querySelector('meta[property="og:image"]')?.attributes['content'] ??
root!.querySelector('meta[property="og:image"]')?.attributes['content'] ??
'';
int? get subscribersCount => initialData.subscribersCount;
///
late final _InitialData initialData = _getInitialData();
_InitialData _getInitialData() {
final scriptText = _root
.querySelectorAll('script')
.map((e) => e.text)
.toList(growable: false);
return scriptText.extractGenericData(
(obj) => _InitialData(obj),
() => TransientFailureException(
'Failed to retrieve initial data from the channel about page, please report this to the project GitHub page.'));
}
///
ChannelPage(this._root);
///
ChannelPage.parse(String raw) : _root = parser.parse(raw);
ChannelPage.parse(String raw)
: super(parser.parse(raw), (root) => _InitialData(root));
///
static Future<ChannelPage> get(YoutubeHttpClient httpClient, String id) {
@ -85,13 +68,10 @@ class ChannelPage {
}
}
class _InitialData {
class _InitialData extends InitialData {
static final RegExp _subCountExp = RegExp(r'(\d+(?:\.\d+)?)(K|M|\s)');
// Json parsed map
final Map<String, dynamic> root;
_InitialData(this.root);
_InitialData(JsonMap root) : super(root);
int? get subscribersCount {
final renderer = root.get('header')?.get('c4TabbedHeaderRenderer');

View File

@ -1,8 +1,7 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser;
import 'package:youtube_explode_dart/src/reverse_engineering/models/youtube_page.dart';
import '../../channels/channel_video.dart';
import '../../exceptions/exceptions.dart';
@ -10,60 +9,27 @@ import '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../../videos/videos.dart';
import '../youtube_http_client.dart';
import '../models/initial_data.dart';
///
class ChannelUploadPage {
class ChannelUploadPage extends YoutubePage<_InitialData> {
///
final String channelId;
final Document? _root;
late final _InitialData initialData = _getInitialData();
_InitialData? _initialData;
late final List<ChannelVideo> uploads = initialData.uploads;
/// InitialData
ChannelUploadPage.id(this.channelId, _InitialData? initialData)
: super(null, null, initialData);
///
_InitialData _getInitialData() {
if (_initialData != null) {
return _initialData!;
}
final scriptText = _root!
.querySelectorAll('script')
.map((e) => e.text)
.toList(growable: false);
return scriptText.extractGenericData(
(obj) => _InitialData(obj),
() => TransientFailureException(
'Failed to retrieve initial data from the channel upload page, please report this to the project GitHub page.'));
}
///
ChannelUploadPage(this._root, this.channelId, [_InitialData? initialData])
: _initialData = initialData;
///
Future<ChannelUploadPage?> nextPage(YoutubeHttpClient httpClient) {
Future<ChannelUploadPage?> nextPage(YoutubeHttpClient httpClient) async {
if (initialData.token.isEmpty) {
return Future.value(null);
return null;
}
final url = Uri.parse(
'https://www.youtube.com/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8');
final body = {
'context': const {
'client': {
'hl': 'en',
'clientName': 'WEB',
'clientVersion': '2.20200911.04.00'
}
},
'continuation': initialData.token
};
return retry(() async {
var raw = await httpClient.post(url, body: json.encode(body));
return ChannelUploadPage(
null, channelId, _InitialData(json.decode(raw.body)));
});
final data = await httpClient.sendPost('browse', initialData.token);
return ChannelUploadPage.id(channelId, _InitialData(data));
}
///
@ -79,17 +45,13 @@ class ChannelUploadPage {
///
ChannelUploadPage.parse(String raw, this.channelId)
: _root = parser.parse(raw);
: super(parser.parse(raw), (root) => _InitialData(root));
}
class _InitialData {
// Json parsed map
final Map<String, dynamic> root;
class _InitialData extends InitialData {
_InitialData(JsonMap root) : super(root);
_InitialData(this.root);
late final Map<String, dynamic>? continuationContext =
getContinuationContext();
late final JsonMap? continuationContext = getContinuationContext();
late final String token = continuationContext?.getT<String>('token') ?? '';
@ -103,15 +65,15 @@ class _InitialData {
return content.map(_parseContent).whereNotNull().toList();
}
List<Map<String, dynamic>> getContentContext() {
List<Map<String, dynamic>>? context;
List<JsonMap> getContentContext() {
List<JsonMap>? context;
if (root.containsKey('contents')) {
final render = root
.get('contents')
?.get('twoColumnBrowseResultsRenderer')
?.getList('tabs')
?.map((e) => e['tabRenderer'])
.cast<Map<String, dynamic>>()
.cast<JsonMap>()
.firstWhereOrNull((e) => e['selected'] as bool)
?.get('content')
?.get('sectionListRenderer')
@ -122,10 +84,8 @@ class _InitialData {
?.firstOrNull;
if (render?.containsKey('gridRenderer') ?? false) {
context = render
?.get('gridRenderer')
?.getList('items')
?.cast<Map<String, dynamic>>();
context =
render?.get('gridRenderer')?.getList('items')?.cast<JsonMap>();
} else if (render?.containsKey('messageRenderer') ?? false) {
// Workaround for no-videos.
context = const [];
@ -137,7 +97,7 @@ class _InitialData {
?.firstOrNull
?.get('appendContinuationItemsAction')
?.getList('continuationItems')
?.cast<Map<String, dynamic>>();
?.cast<JsonMap>();
}
if (context == null) {
throw FatalFailureException('Failed to get initial data context.');
@ -145,14 +105,14 @@ class _InitialData {
return context;
}
Map<String, dynamic>? getContinuationContext() {
JsonMap? getContinuationContext() {
if (root.containsKey('contents')) {
return root
.get('contents')
?.get('twoColumnBrowseResultsRenderer')
?.getList('tabs')
?.map((e) => e['tabRenderer'])
.cast<Map<String, dynamic>>()
.cast<JsonMap>()
.firstWhereOrNull((e) => e['selected'] as bool)
?.get('content')
?.get('sectionListRenderer')
@ -182,7 +142,7 @@ class _InitialData {
return null;
}
ChannelVideo? _parseContent(Map<String, dynamic>? content) {
ChannelVideo? _parseContent(JsonMap? content) {
if (content == null || !content.containsKey('gridVideoRenderer')) {
return null;
}

View File

@ -79,9 +79,9 @@ class EmbedPage {
}
/// Used internally
class EmbedPlayerConfig implements PlayerConfigBase<Map<String, dynamic>> {
class EmbedPlayerConfig implements PlayerConfigBase {
@override
final Map<String, dynamic> root;
final JsonMap root;
///
EmbedPlayerConfig(this.root);

View File

@ -1,7 +1,9 @@
import '../../extensions/helpers_extension.dart';
/// Base class for PlayerConfig.
abstract class PlayerConfigBase<T> {
abstract class PlayerConfigBase {
/// Root node.
final T root;
final JsonMap root;
///
PlayerConfigBase(this.root);

View File

@ -1,95 +1,63 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser;
import 'package:youtube_explode_dart/src/reverse_engineering/models/youtube_page.dart';
import '../../../youtube_explode_dart.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../youtube_http_client.dart';
import '../models/initial_data.dart';
///
class PlaylistPage {
class PlaylistPage extends YoutubePage<_InitialData> {
///
final String playlistId;
final Document? root;
late final _InitialData initialData = getInitialData();
_InitialData? _initialData;
late final List<_Video> videos = initialData.playlistVideos;
///
_InitialData getInitialData() {
if (_initialData != null) {
return _initialData!;
}
late final String? title = initialData.title;
final scriptText = root!
.querySelectorAll('script')
.map((e) => e.text)
.toList(growable: false);
late final String? description = initialData.description;
return scriptText.extractGenericData(
(obj) => _InitialData(obj),
() => TransientFailureException(
'Failed to retrieve initial data from the search page, please report this to the project GitHub page.'));
}
late final String? author = initialData.author;
///
PlaylistPage(this.root, this.playlistId, [_InitialData? initialData])
: _initialData = initialData;
late final int? viewCount = initialData.viewCount;
/// InitialData
PlaylistPage.id(this.playlistId, _InitialData initialData)
: super(null, null, initialData);
///
Future<PlaylistPage?> nextPage(YoutubeHttpClient httpClient) async {
if (initialData.continuationToken == null) {
if (initialData.continuationToken?.isEmpty == null) {
return null;
}
return get(httpClient, playlistId, token: initialData.continuationToken);
final data =
await httpClient.sendPost('browse', initialData.continuationToken!);
return PlaylistPage.id(playlistId, _InitialData(data));
}
///
static Future<PlaylistPage> get(YoutubeHttpClient httpClient, String id,
{String? token}) {
if (token != null && token.isNotEmpty) {
var url =
'https://www.youtube.com/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
return retry(() async {
var body = {
'context': const {
'client': {
'hl': 'en',
'clientName': 'WEB',
'clientVersion': '2.20200911.04.00'
}
},
'continuation': token
};
var raw =
await httpClient.post(Uri.parse(url), body: json.encode(body));
return PlaylistPage(null, id, _InitialData(json.decode(raw.body)));
});
// Ask for next page,
}
static Future<PlaylistPage> get(
YoutubeHttpClient httpClient,
String id,
) async {
var url = 'https://www.youtube.com/playlist?list=$id&hl=en&persist_hl=1';
return retry(() async {
var raw = await httpClient.getString(url);
return PlaylistPage.parse(raw, id);
});
// ask for next page
}
///
PlaylistPage.parse(String raw, this.playlistId) : root = parser.parse(raw);
PlaylistPage.parse(String raw, this.playlistId)
: super(parser.parse(raw), (root) => _InitialData(root));
}
class _InitialData {
// Json parsed map
final Map<String, dynamic> root;
_InitialData(this.root);
class _InitialData extends InitialData {
_InitialData(JsonMap root) : super(root);
late final String? title = root
.get('metadata')
@ -132,7 +100,7 @@ class _InitialData {
?.get('continuationCommand')
?.getT<String>('token');
List<Map<String, dynamic>>? get playlistVideosContent =>
List<JsonMap>? get playlistVideosContent =>
root
.get('contents')
?.get('twoColumnBrowseResultsRenderer')
@ -154,7 +122,7 @@ class _InitialData {
?.get('appendContinuationItemsAction')
?.getList('continuationItems');
late final List<Map<String, dynamic>>? videosContent = root
late final List<JsonMap>? videosContent = root
.get('contents')
?.get('twoColumnSearchResultsRenderer')
?.get('primaryContents')
@ -173,19 +141,19 @@ class _InitialData {
.toList() ??
const [];
List<_Video> get videos =>
/* List<_Video> get videos =>
videosContent?.firstOrNull
?.get('itemSectionRenderer')
?.getList('contents')
?.where((e) => e['videoRenderer'] != null)
.map((e) => _Video(e))
.toList() ??
const [];
const [];*/
}
class _Video {
// Json parsed map
final Map<String, dynamic> root;
final JsonMap root;
_Video(this.root);

View File

@ -1,8 +1,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser;
import 'package:youtube_explode_dart/src/reverse_engineering/models/youtube_page.dart';
import 'package:youtube_explode_dart/src/search/search_channel.dart';
import '../../../youtube_explode_dart.dart';
@ -14,72 +12,32 @@ import '../../search/search_filter.dart';
import '../../search/search_video.dart';
import '../../videos/videos.dart';
import '../youtube_http_client.dart';
import '../models/initial_data.dart';
///
class SearchPage {
class SearchPage extends YoutubePage<_InitialData> {
///
final String queryString;
final Document? root;
late final _InitialData initialData = getInitialData();
_InitialData? _initialData;
///
_InitialData getInitialData() {
if (_initialData != null) {
return _initialData!;
}
final scriptText = root!
.querySelectorAll('script')
.map((e) => e.text)
.toList(growable: false);
return scriptText.extractGenericData(
(obj) => _InitialData(obj),
() => TransientFailureException(
'Failed to retrieve initial data from the search page, please report this to the project GitHub page.'));
}
///
SearchPage(this.root, this.queryString, [_InitialData? initialData])
: _initialData = initialData;
/// InitialData
SearchPage.id(this.queryString, _InitialData initialData)
: super(null, null, initialData);
Future<SearchPage?> nextPage(YoutubeHttpClient httpClient) async {
if (initialData.continuationToken == '' ||
if (initialData.continuationToken?.isEmpty == null ||
initialData.estimatedResults == 0) {
return null;
}
return get(httpClient, queryString, token: initialData.continuationToken);
var data =
await httpClient.sendPost('search', initialData.continuationToken!);
return SearchPage.id(queryString, _InitialData(data));
}
///
static Future<SearchPage> get(
YoutubeHttpClient httpClient, String queryString,
{String? token, SearchFilter filter = const SearchFilter('')}) {
if (token != null) {
var url =
'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
return retry(() async {
var body = {
'context': const {
'client': {
'hl': 'en',
'clientName': 'WEB',
'clientVersion': '2.20200911.04.00'
}
},
'continuation': token
};
var raw =
await httpClient.post(Uri.parse(url), body: json.encode(body));
return SearchPage(
null, queryString, _InitialData(json.decode(raw.body)));
});
// Ask for next page,
}
{SearchFilter filter = const SearchFilter('')}) {
var url =
'https://www.youtube.com/results?search_query=${Uri.encodeQueryComponent(queryString)}&sp=${filter.value}';
return retry(() async {
@ -90,16 +48,14 @@ class SearchPage {
}
///
SearchPage.parse(String raw, this.queryString) : root = parser.parse(raw);
SearchPage.parse(String raw, this.queryString)
: super(parser.parse(raw), (root) => _InitialData(root));
}
class _InitialData {
// Json parsed map
final Map<String, dynamic> root;
class _InitialData extends InitialData {
_InitialData(JsonMap root) : super(root);
_InitialData(this.root);
List<Map<String, dynamic>>? getContentContext() {
List<JsonMap>? getContentContext() {
if (root['contents'] != null) {
return root
.get('contents')
@ -196,7 +152,7 @@ class _InitialData {
late final int estimatedResults =
int.parse(root.getT<String>('estimatedResults') ?? '0');
BaseSearchContent? _parseContent(Map<String, dynamic>? content) {
BaseSearchContent? _parseContent(JsonMap? content) {
if (content == null) {
return null;
}

View File

@ -1,17 +1,19 @@
import 'package:collection/collection.dart';
import 'package:html/dom.dart';
import 'package:html/parser.dart' as parser;
import 'package:youtube_explode_dart/src/reverse_engineering/models/youtube_page.dart';
import '../../../youtube_explode_dart.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../../videos/video_id.dart';
import '../player/player_response.dart';
import '../youtube_http_client.dart';
import 'player_config_base.dart';
import 'player_response.dart';
import '../models/initial_data.dart';
///
class WatchPage {
class WatchPage extends YoutubePage<_InitialData> {
static final RegExp _videoLikeExp =
RegExp(r'"label"\s*:\s*"([\d,\.]+) likes"');
static final RegExp _videoDislikeExp =
@ -24,6 +26,7 @@ class WatchPage {
static final _xsfrTokenExp = RegExp(r'"XSRF_TOKEN"\s*:\s*"(.+?)"');
@override
final Document root;
///
@ -32,8 +35,6 @@ class WatchPage {
///
final String ysc;
_InitialData? _initialData;
///
String? get sourceUrl {
var url = root
@ -47,24 +48,6 @@ class WatchPage {
return 'https://youtube.com$url';
}
late final _InitialData initialData = getInitialData();
///
_InitialData getInitialData() {
if (_initialData != null) {
return _initialData!;
}
final scriptText = root
.querySelectorAll('script')
.map((e) => e.text)
.toList(growable: false);
return scriptText.extractGenericData(
(obj) => _InitialData(obj),
() => TransientFailureException(
'Failed to retrieve initial data from the watch page, please report this to the project GitHub page.'));
}
late final String xsfrToken = getXsfrToken()!.replaceAll(r'\u003d', '=');
///
@ -142,12 +125,10 @@ class WatchPage {
return PlayerResponse(val);
}
///
WatchPage(this.root, this.visitorInfoLive, this.ysc);
///
WatchPage.parse(String raw, this.visitorInfoLive, this.ysc)
: root = parser.parse(raw);
: root = parser.parse(raw),
super(parser.parse(raw), (root) => _InitialData(root));
///
static Future<WatchPage> get(YoutubeHttpClient httpClient, String videoId) {
@ -173,9 +154,9 @@ class WatchPage {
}
/// Used internally
class WatchPlayerConfig implements PlayerConfigBase<Map<String, dynamic>> {
class WatchPlayerConfig implements PlayerConfigBase {
@override
final Map<String, dynamic> root;
final JsonMap root;
///
WatchPlayerConfig(this.root);
@ -189,13 +170,10 @@ class WatchPlayerConfig implements PlayerConfigBase<Map<String, dynamic>> {
PlayerResponse.parse(root.get('args')!.getT<String>('playerResponse')!);
}
class _InitialData {
// Json parsed map
final Map<String, dynamic> root;
class _InitialData extends InitialData {
_InitialData(JsonMap root) : super(root);
_InitialData(this.root);
Map<String, dynamic>? getContinuationContext() {
JsonMap? getContinuationContext() {
if (root['contents'] != null) {
return root
.get('contents')

View File

@ -4,12 +4,12 @@ import 'package:collection/collection.dart';
import 'package:http_parser/http_parser.dart';
import '../../extensions/helpers_extension.dart';
import 'stream_info_provider.dart';
import '../models/stream_info_provider.dart';
///
class PlayerResponse {
// Json parsed map
Map<String, dynamic> root;
JsonMap root;
///
late final String playabilityStatus =
@ -139,7 +139,7 @@ class PlayerResponse {
///
class ClosedCaptionTrack {
// Json parsed class
final Map<String, dynamic> root;
final JsonMap root;
///
String get url => root.getT<String>('baseUrl')!;
@ -163,7 +163,7 @@ class _StreamInfo extends StreamInfoProvider {
static final _contentLenExp = RegExp(r'[\?&]clen=(\d+)');
/// Json parsed map
final Map<String, dynamic> root;
final JsonMap root;
@override
late final int? bitrate = root.getT<int>('bitrate');

View File

@ -1,12 +0,0 @@
library _youtube_explode.responses;
export 'channel_page.dart';
export 'closed_caption_track_response.dart';
export 'dash_manifest.dart';
export 'embed_page.dart';
export 'player_response.dart';
export 'player_source.dart';
export 'playlist_page.dart';
export 'stream_info_provider.dart';
export 'video_info_response.dart';
export 'watch_page.dart';

View File

@ -4,8 +4,8 @@ import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../youtube_http_client.dart';
import 'player_response.dart';
import 'stream_info_provider.dart';
import '../player/player_response.dart';
import '../models/stream_info_provider.dart';
///
class VideoInfoResponse {

View File

@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:youtube_explode_dart/src/retry.dart';
import '../exceptions/exceptions.dart';
import '../extensions/helpers_extension.dart';
import '../videos/streams/streams.dart';
/// HttpClient wrapper for YouTube
@ -143,6 +145,30 @@ class YoutubeHttpClient extends http.BaseClient {
return int.tryParse(response.headers['content-length'] ?? '');
}
/// Sends a call to the youtube api endpoint.
Future<JsonMap> sendPost(String action, String token) async {
assert(action == 'next' || action == 'browse' || action == 'search');
final body = {
'context': const {
'client': {
'hl': 'en',
'clientName': 'WEB',
'clientVersion': '2.20200911.04.00'
}
},
'continuation': token
};
final url = Uri.parse(
'https://www.youtube.com/youtubei/v1/$action?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8');
return retry<JsonMap>(() async {
final raw = await post(url, body: json.encode(body));
return json.decode(raw.body);
});
}
@override
void close() => _httpClient.close();

View File

@ -1,4 +1,3 @@
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../channels/channel_id.dart';

View File

@ -3,7 +3,7 @@ import 'dart:convert';
import '../../youtube_explode_dart.dart';
import '../extensions/helpers_extension.dart';
import '../retry.dart';
import '../reverse_engineering/responses/search_page.dart';
import '../reverse_engineering/pages/search_page.dart';
import '../reverse_engineering/youtube_http_client.dart';
import 'base_search_content.dart';
import 'search_filter.dart';

View File

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/responses/search_page.dart';
import 'package:youtube_explode_dart/src/reverse_engineering/pages/search_page.dart';
import '../../youtube_explode_dart.dart';
import '../extensions/helpers_extension.dart';

View File

@ -1,4 +1,4 @@
import '../reverse_engineering/responses/search_page.dart';
import '../reverse_engineering/pages/search_page.dart';
import '../reverse_engineering/youtube_http_client.dart';
import 'related_query.dart';

View File

@ -1,6 +1,7 @@
import '../../extensions/helpers_extension.dart';
import '../../reverse_engineering/responses/responses.dart'
hide ClosedCaption, ClosedCaptionPart, ClosedCaptionTrack;
import '../../reverse_engineering/responses/closed_caption_track_response.dart'
show ClosedCaptionTrackResponse;
import '../../reverse_engineering/responses/video_info_response.dart';
import '../../reverse_engineering/youtube_http_client.dart';
import '../videos.dart';
import 'closed_caption.dart';

View File

@ -1,30 +1,33 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../extensions/helpers_extension.dart';
import 'closed_caption_format.dart';
import 'language.dart';
part 'closed_caption_track_info.freezed.dart';
part 'closed_caption_track_info.g.dart';
/// Metadata associated with a certain [ClosedCaptionTrack]
@JsonSerializable()
class ClosedCaptionTrackInfo extends Equatable {
/// Manifest URL of the associated track.
final Uri url;
/// Language of the associated track.
final Language language;
/// Whether the associated track was automatically generated.
final bool isAutoGenerated;
/// Track format
final ClosedCaptionFormat format;
@freezed
class ClosedCaptionTrackInfo with _$ClosedCaptionTrackInfo {
/// Initializes an instance of [ClosedCaptionTrackInfo]
const ClosedCaptionTrackInfo(this.url, this.language,
{this.isAutoGenerated = false, required this.format});
const factory ClosedCaptionTrackInfo(
/// Manifest URL of the associated track.
Uri url,
/// Language of the associated track.
Language language,
{
/// Whether the associated track was automatically generated.
@Default(false) bool isAutoGenerated,
/// Track format
required ClosedCaptionFormat format}) = _ClosedCaptionTrackInfo;
const ClosedCaptionTrackInfo._();
/// Returns this auto-translated to another language.
/// Keeping the same format.
@ -37,13 +40,7 @@ class ClosedCaptionTrackInfo extends Equatable {
@override
String toString() => 'CC Track ($language)';
@override
List<Object> get props => [url, language, isAutoGenerated];
///
factory ClosedCaptionTrackInfo.fromJson(Map<String, dynamic> json) =>
_$ClosedCaptionTrackInfoFromJson(json);
///
Map<String, dynamic> toJson() => _$ClosedCaptionTrackInfoToJson(this);
}

View File

@ -0,0 +1,269 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
part of 'closed_caption_track_info.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
ClosedCaptionTrackInfo _$ClosedCaptionTrackInfoFromJson(
Map<String, dynamic> json) {
return _ClosedCaptionTrackInfo.fromJson(json);
}
/// @nodoc
class _$ClosedCaptionTrackInfoTearOff {
const _$ClosedCaptionTrackInfoTearOff();
_ClosedCaptionTrackInfo call(Uri url, Language language,
{bool isAutoGenerated = false, required ClosedCaptionFormat format}) {
return _ClosedCaptionTrackInfo(
url,
language,
isAutoGenerated: isAutoGenerated,
format: format,
);
}
ClosedCaptionTrackInfo fromJson(Map<String, Object> json) {
return ClosedCaptionTrackInfo.fromJson(json);
}
}
/// @nodoc
const $ClosedCaptionTrackInfo = _$ClosedCaptionTrackInfoTearOff();
/// @nodoc
mixin _$ClosedCaptionTrackInfo {
/// Manifest URL of the associated track.
Uri get url => throw _privateConstructorUsedError;
/// Language of the associated track.
Language get language => throw _privateConstructorUsedError;
/// Whether the associated track was automatically generated.
bool get isAutoGenerated => throw _privateConstructorUsedError;
/// Track format
ClosedCaptionFormat get format => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ClosedCaptionTrackInfoCopyWith<ClosedCaptionTrackInfo> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ClosedCaptionTrackInfoCopyWith<$Res> {
factory $ClosedCaptionTrackInfoCopyWith(ClosedCaptionTrackInfo value,
$Res Function(ClosedCaptionTrackInfo) then) =
_$ClosedCaptionTrackInfoCopyWithImpl<$Res>;
$Res call(
{Uri url,
Language language,
bool isAutoGenerated,
ClosedCaptionFormat format});
$LanguageCopyWith<$Res> get language;
}
/// @nodoc
class _$ClosedCaptionTrackInfoCopyWithImpl<$Res>
implements $ClosedCaptionTrackInfoCopyWith<$Res> {
_$ClosedCaptionTrackInfoCopyWithImpl(this._value, this._then);
final ClosedCaptionTrackInfo _value;
// ignore: unused_field
final $Res Function(ClosedCaptionTrackInfo) _then;
@override
$Res call({
Object? url = freezed,
Object? language = freezed,
Object? isAutoGenerated = freezed,
Object? format = freezed,
}) {
return _then(_value.copyWith(
url: url == freezed
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as Uri,
language: language == freezed
? _value.language
: language // ignore: cast_nullable_to_non_nullable
as Language,
isAutoGenerated: isAutoGenerated == freezed
? _value.isAutoGenerated
: isAutoGenerated // ignore: cast_nullable_to_non_nullable
as bool,
format: format == freezed
? _value.format
: format // ignore: cast_nullable_to_non_nullable
as ClosedCaptionFormat,
));
}
@override
$LanguageCopyWith<$Res> get language {
return $LanguageCopyWith<$Res>(_value.language, (value) {
return _then(_value.copyWith(language: value));
});
}
}
/// @nodoc
abstract class _$ClosedCaptionTrackInfoCopyWith<$Res>
implements $ClosedCaptionTrackInfoCopyWith<$Res> {
factory _$ClosedCaptionTrackInfoCopyWith(_ClosedCaptionTrackInfo value,
$Res Function(_ClosedCaptionTrackInfo) then) =
__$ClosedCaptionTrackInfoCopyWithImpl<$Res>;
@override
$Res call(
{Uri url,
Language language,
bool isAutoGenerated,
ClosedCaptionFormat format});
@override
$LanguageCopyWith<$Res> get language;
}
/// @nodoc
class __$ClosedCaptionTrackInfoCopyWithImpl<$Res>
extends _$ClosedCaptionTrackInfoCopyWithImpl<$Res>
implements _$ClosedCaptionTrackInfoCopyWith<$Res> {
__$ClosedCaptionTrackInfoCopyWithImpl(_ClosedCaptionTrackInfo _value,
$Res Function(_ClosedCaptionTrackInfo) _then)
: super(_value, (v) => _then(v as _ClosedCaptionTrackInfo));
@override
_ClosedCaptionTrackInfo get _value => super._value as _ClosedCaptionTrackInfo;
@override
$Res call({
Object? url = freezed,
Object? language = freezed,
Object? isAutoGenerated = freezed,
Object? format = freezed,
}) {
return _then(_ClosedCaptionTrackInfo(
url == freezed
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as Uri,
language == freezed
? _value.language
: language // ignore: cast_nullable_to_non_nullable
as Language,
isAutoGenerated: isAutoGenerated == freezed
? _value.isAutoGenerated
: isAutoGenerated // ignore: cast_nullable_to_non_nullable
as bool,
format: format == freezed
? _value.format
: format // ignore: cast_nullable_to_non_nullable
as ClosedCaptionFormat,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_ClosedCaptionTrackInfo extends _ClosedCaptionTrackInfo {
const _$_ClosedCaptionTrackInfo(this.url, this.language,
{this.isAutoGenerated = false, required this.format})
: super._();
factory _$_ClosedCaptionTrackInfo.fromJson(Map<String, dynamic> json) =>
_$$_ClosedCaptionTrackInfoFromJson(json);
@override
/// Manifest URL of the associated track.
final Uri url;
@override
/// Language of the associated track.
final Language language;
@JsonKey(defaultValue: false)
@override
/// Whether the associated track was automatically generated.
final bool isAutoGenerated;
@override
/// Track format
final ClosedCaptionFormat format;
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other is _ClosedCaptionTrackInfo &&
(identical(other.url, url) ||
const DeepCollectionEquality().equals(other.url, url)) &&
(identical(other.language, language) ||
const DeepCollectionEquality()
.equals(other.language, language)) &&
(identical(other.isAutoGenerated, isAutoGenerated) ||
const DeepCollectionEquality()
.equals(other.isAutoGenerated, isAutoGenerated)) &&
(identical(other.format, format) ||
const DeepCollectionEquality().equals(other.format, format)));
}
@override
int get hashCode =>
runtimeType.hashCode ^
const DeepCollectionEquality().hash(url) ^
const DeepCollectionEquality().hash(language) ^
const DeepCollectionEquality().hash(isAutoGenerated) ^
const DeepCollectionEquality().hash(format);
@JsonKey(ignore: true)
@override
_$ClosedCaptionTrackInfoCopyWith<_ClosedCaptionTrackInfo> get copyWith =>
__$ClosedCaptionTrackInfoCopyWithImpl<_ClosedCaptionTrackInfo>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_ClosedCaptionTrackInfoToJson(this);
}
}
abstract class _ClosedCaptionTrackInfo extends ClosedCaptionTrackInfo {
const factory _ClosedCaptionTrackInfo(Uri url, Language language,
{bool isAutoGenerated,
required ClosedCaptionFormat format}) = _$_ClosedCaptionTrackInfo;
const _ClosedCaptionTrackInfo._() : super._();
factory _ClosedCaptionTrackInfo.fromJson(Map<String, dynamic> json) =
_$_ClosedCaptionTrackInfo.fromJson;
@override
/// Manifest URL of the associated track.
Uri get url => throw _privateConstructorUsedError;
@override
/// Language of the associated track.
Language get language => throw _privateConstructorUsedError;
@override
/// Whether the associated track was automatically generated.
bool get isAutoGenerated => throw _privateConstructorUsedError;
@override
/// Track format
ClosedCaptionFormat get format => throw _privateConstructorUsedError;
@override
@JsonKey(ignore: true)
_$ClosedCaptionTrackInfoCopyWith<_ClosedCaptionTrackInfo> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -6,9 +6,9 @@ part of 'closed_caption_track_info.dart';
// JsonSerializableGenerator
// **************************************************************************
ClosedCaptionTrackInfo _$ClosedCaptionTrackInfoFromJson(
_$_ClosedCaptionTrackInfo _$$_ClosedCaptionTrackInfoFromJson(
Map<String, dynamic> json) =>
ClosedCaptionTrackInfo(
_$_ClosedCaptionTrackInfo(
Uri.parse(json['url'] as String),
Language.fromJson(json['language'] as Map<String, dynamic>),
isAutoGenerated: json['isAutoGenerated'] as bool? ?? false,
@ -16,8 +16,8 @@ ClosedCaptionTrackInfo _$ClosedCaptionTrackInfoFromJson(
ClosedCaptionFormat.fromJson(json['format'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ClosedCaptionTrackInfoToJson(
ClosedCaptionTrackInfo instance) =>
Map<String, dynamic> _$$_ClosedCaptionTrackInfoToJson(
_$_ClosedCaptionTrackInfo instance) =>
<String, dynamic>{
'url': instance.url.toString(),
'language': instance.language,

View File

@ -1,22 +1,22 @@
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'language.g.dart';
part 'language.freezed.dart';
/// Language information.
@JsonSerializable()
class Language extends Equatable {
/// ISO 639-1 code of this language.
final String code;
/// Full English name of this language.
final String name;
@freezed
class Language with _$Language {
/// Initializes an instance of [Language]
const Language(this.code, this.name);
const factory Language(
@override
List<Object> get props => [code, name];
/// ISO 639-1 code of this language.
String code,
/// Full English name of this language.
String name) = _Language;
const Language._();
@override
String toString() => 'Language: $name';
@ -24,7 +24,4 @@ class Language extends Equatable {
///
factory Language.fromJson(Map<String, dynamic> json) =>
_$LanguageFromJson(json);
///
Map<String, dynamic> toJson() => _$LanguageToJson(this);
}

View File

@ -0,0 +1,182 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
part of 'language.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
Language _$LanguageFromJson(Map<String, dynamic> json) {
return _Language.fromJson(json);
}
/// @nodoc
class _$LanguageTearOff {
const _$LanguageTearOff();
_Language call(String code, String name) {
return _Language(
code,
name,
);
}
Language fromJson(Map<String, Object> json) {
return Language.fromJson(json);
}
}
/// @nodoc
const $Language = _$LanguageTearOff();
/// @nodoc
mixin _$Language {
/// ISO 639-1 code of this language.
String get code => throw _privateConstructorUsedError;
/// Full English name of this language.
String get name => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LanguageCopyWith<Language> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LanguageCopyWith<$Res> {
factory $LanguageCopyWith(Language value, $Res Function(Language) then) =
_$LanguageCopyWithImpl<$Res>;
$Res call({String code, String name});
}
/// @nodoc
class _$LanguageCopyWithImpl<$Res> implements $LanguageCopyWith<$Res> {
_$LanguageCopyWithImpl(this._value, this._then);
final Language _value;
// ignore: unused_field
final $Res Function(Language) _then;
@override
$Res call({
Object? code = freezed,
Object? name = freezed,
}) {
return _then(_value.copyWith(
code: code == freezed
? _value.code
: code // ignore: cast_nullable_to_non_nullable
as String,
name: name == freezed
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
abstract class _$LanguageCopyWith<$Res> implements $LanguageCopyWith<$Res> {
factory _$LanguageCopyWith(_Language value, $Res Function(_Language) then) =
__$LanguageCopyWithImpl<$Res>;
@override
$Res call({String code, String name});
}
/// @nodoc
class __$LanguageCopyWithImpl<$Res> extends _$LanguageCopyWithImpl<$Res>
implements _$LanguageCopyWith<$Res> {
__$LanguageCopyWithImpl(_Language _value, $Res Function(_Language) _then)
: super(_value, (v) => _then(v as _Language));
@override
_Language get _value => super._value as _Language;
@override
$Res call({
Object? code = freezed,
Object? name = freezed,
}) {
return _then(_Language(
code == freezed
? _value.code
: code // ignore: cast_nullable_to_non_nullable
as String,
name == freezed
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_Language extends _Language {
const _$_Language(this.code, this.name) : super._();
factory _$_Language.fromJson(Map<String, dynamic> json) =>
_$$_LanguageFromJson(json);
@override
/// ISO 639-1 code of this language.
final String code;
@override
/// Full English name of this language.
final String name;
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other is _Language &&
(identical(other.code, code) ||
const DeepCollectionEquality().equals(other.code, code)) &&
(identical(other.name, name) ||
const DeepCollectionEquality().equals(other.name, name)));
}
@override
int get hashCode =>
runtimeType.hashCode ^
const DeepCollectionEquality().hash(code) ^
const DeepCollectionEquality().hash(name);
@JsonKey(ignore: true)
@override
_$LanguageCopyWith<_Language> get copyWith =>
__$LanguageCopyWithImpl<_Language>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_LanguageToJson(this);
}
}
abstract class _Language extends Language {
const factory _Language(String code, String name) = _$_Language;
const _Language._() : super._();
factory _Language.fromJson(Map<String, dynamic> json) = _$_Language.fromJson;
@override
/// ISO 639-1 code of this language.
String get code => throw _privateConstructorUsedError;
@override
/// Full English name of this language.
String get name => throw _privateConstructorUsedError;
@override
@JsonKey(ignore: true)
_$LanguageCopyWith<_Language> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -6,12 +6,13 @@ part of 'language.dart';
// JsonSerializableGenerator
// **************************************************************************
Language _$LanguageFromJson(Map<String, dynamic> json) => Language(
_$_Language _$$_LanguageFromJson(Map<String, dynamic> json) => _$_Language(
json['code'] as String,
json['name'] as String,
);
Map<String, dynamic> _$LanguageToJson(Language instance) => <String, dynamic>{
Map<String, dynamic> _$$_LanguageToJson(_$_Language instance) =>
<String, dynamic>{
'code': instance.code,
'name': instance.name,
};

View File

@ -16,7 +16,6 @@ class CommentsClient {
/// Returns the json parsed comments map.
Future<Map<String, dynamic>> _getCommentJson(
String service,
String continuation,
String clickTrackingParams,
String xsfrToken,
@ -25,14 +24,9 @@ class CommentsClient {
final url = Uri(
scheme: 'https',
host: 'www.youtube.com',
path: '/comment_service_ajax',
path: '/next',
queryParameters: {
service: '1',
'pbj': '1',
'ctoken': continuation,
'continuation': continuation,
'itct': clickTrackingParams,
'type': 'next',
'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
});
return retry(() async {
@ -70,8 +64,9 @@ class CommentsClient {
Stream<Comment> _getComments(String continuation, String clickTrackingParams,
String xsfrToken, String visitorInfoLive, String ysc) async* {
var data = await _getCommentJson('action_get_comments', continuation,
clickTrackingParams, xsfrToken, visitorInfoLive, ysc);
// 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')
@ -142,24 +137,11 @@ class CommentsClient {
}
}
//TODO: Implement replies
/* Stream<Comment> getReplies(Video video, Comment comment) async* {
if (video.watchPage == null || comment.continuation == null
|| comment.clicktrackingParams == null) {
Stream<Comment> getReplies(Video video, Comment comment) async* {
if (video.watchPage == null ||
comment.continuation == null ||
comment.clicktrackingParams == null) {
return;
}
yield* _getReplies(
video.watchPage.initialData.continuation,
video.watchPage.initialData.clickTrackingParams,
video.watchPage.xsfrToken,
video.watchPage.visitorInfoLive,
video.watchPage.ysc);
}
Stream<Comment> _getReplies(String continuation, String clickTrackingParams,
String xsfrToken, String visitorInfoLive, String ysc) async* {
var data = await _getCommentJson('action_get_comment_replies', continuation,
clickTrackingParams, xsfrToken, visitorInfoLive, ysc);
print(data);
}*/
}

View File

@ -1,5 +1,5 @@
import '../../reverse_engineering/cipher/cipher_operations.dart';
import '../../reverse_engineering/responses/responses.dart';
import '../../reverse_engineering/models/stream_info_provider.dart';
///
class StreamContext {

View File

@ -2,7 +2,12 @@ import '../../exceptions/exceptions.dart';
import '../../extensions/helpers_extension.dart';
import '../../reverse_engineering/cipher/cipher_operations.dart';
import '../../reverse_engineering/heuristics.dart';
import '../../reverse_engineering/responses/responses.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/youtube_http_client.dart';
import '../video_id.dart';
import 'bitrate.dart';

View File

@ -5,7 +5,7 @@ import 'package:meta/meta.dart';
import '../channels/channel_id.dart';
import '../common/common.dart';
import '../reverse_engineering/responses/responses.dart';
import '../reverse_engineering/pages/watch_page.dart';
import 'video_id.dart';
part 'video.freezed.dart';

View File

@ -1,7 +1,8 @@
import '../channels/channel_id.dart';
import '../common/common.dart';
import '../extensions/helpers_extension.dart';
import '../reverse_engineering/responses/responses.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';

View File

@ -5,7 +5,7 @@ version: 1.9.10
homepage: https://github.com/Hexer10/youtube_explode_dart
environment:
sdk: '>=2.12.0 <3.0.0'
sdk: '>=2.13.0 <3.0.0'
dependencies:
collection: ^1.15.0