Merge pull request #78 from Hexer10/json-types
This commit is contained in:
commit
638e3265de
|
@ -3,6 +3,14 @@
|
|||
- Only throw custom exceptions from the library.
|
||||
- `getUploadsFromPage` no longer throws.
|
||||
|
||||
## 1.6.0
|
||||
- BREAKING CHANGE: Renamed `getVideosAsync` to `getVideos`.
|
||||
- Implemented `getVideosFromPage` which supersedes `queryFromPage`.
|
||||
- Implemented JSON Classes for reverse engineer.
|
||||
- Added `forceWatchPage` to the video client to assure the fetching of the video page. (ATM useful only if using the comments api)
|
||||
- Remove adaptive streams. These are not used anymore.
|
||||
- Implement `channelClient.getAboutPage` and `getAboutPageByUsername` to fetch data from a channel's about page.
|
||||
|
||||
## 1.5.2
|
||||
- Fix extraction for same videos (#76)
|
||||
|
||||
|
@ -12,7 +20,6 @@
|
|||
## 1.5.0
|
||||
- BREAKING CHANGE: Renamed `Container` class to `StreamContainer` to avoid conflicting with Flutter `Container`. See #66
|
||||
|
||||
|
||||
## 1.4.4
|
||||
- Expose HttpClient in APIs
|
||||
- Fix #55: Typo in README.md
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
include: package:effective_dart/analysis_options.yaml
|
||||
|
||||
|
||||
linter:
|
||||
rules:
|
||||
- valid_regexps
|
||||
|
@ -60,3 +61,4 @@ linter:
|
|||
analyzer:
|
||||
exclude:
|
||||
- example\**
|
||||
- lib\src\reverse_engineering\responses\generated\**
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'channel_link.dart';
|
||||
import 'channel_thumbnail.dart';
|
||||
|
||||
/// YouTube channel's about page metadata.
|
||||
class ChannelAbout with EquatableMixin {
|
||||
/// Full channel description.
|
||||
final String description;
|
||||
|
||||
/// Channel view count.
|
||||
final int viewCount;
|
||||
|
||||
/// Channel join date.
|
||||
/// Formatted as: Gen 01, 2000
|
||||
final String joinDate;
|
||||
|
||||
/// Channel title.
|
||||
final String title;
|
||||
|
||||
/// Channel thumbnails.
|
||||
final List<ChannelThumbnail> thumbnails;
|
||||
|
||||
/// Channel country.
|
||||
final String country;
|
||||
|
||||
/// Channel links.
|
||||
final List<ChannelLink> channelLinks;
|
||||
|
||||
/// Initialize an instance of [ChannelAbout]
|
||||
ChannelAbout(this.description, this.viewCount, this.joinDate, this.title,
|
||||
this.thumbnails, this.country, this.channelLinks);
|
||||
|
||||
@override
|
||||
List<Object> get props =>
|
||||
[description, viewCount, joinDate, title, thumbnails];
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
import 'package:youtube_explode_dart/src/channels/channels.dart';
|
||||
import 'package:youtube_explode_dart/src/reverse_engineering/responses/channel_about_page.dart';
|
||||
|
||||
import '../extensions/helpers_extension.dart';
|
||||
import '../playlists/playlists.dart';
|
||||
import '../reverse_engineering/responses/channel_upload_page.dart';
|
||||
|
@ -40,6 +43,51 @@ class ChannelClient {
|
|||
channelPage.channelLogoUrl);
|
||||
}
|
||||
|
||||
/// Gets the info found on a YouTube Channel About page.
|
||||
/// [id] must be either a [ChannelId] or a string
|
||||
/// which is parsed to a [ChannelId]
|
||||
Future<ChannelAbout> getAboutPage(dynamic id) async {
|
||||
id = ChannelId.fromString(id);
|
||||
|
||||
var channelAboutPage = await ChannelAboutPage.get(_httpClient, id.value);
|
||||
var iData = channelAboutPage.initialData;
|
||||
assert(iData != null);
|
||||
return ChannelAbout(
|
||||
id.description,
|
||||
id.viewCount,
|
||||
id.joinDate,
|
||||
id.title,
|
||||
[
|
||||
for (var e in id.avatar)
|
||||
ChannelThumbnail(Uri.parse(e.url), e.height, e.width)
|
||||
],
|
||||
id.country,
|
||||
id.channelLinks);
|
||||
}
|
||||
|
||||
/// Gets the info found on a YouTube Channel About page.
|
||||
/// [username] must be either a [Username] or a string
|
||||
/// which is parsed to a [Username]
|
||||
Future<ChannelAbout> getAboutPageByUsername(dynamic username) async {
|
||||
username = Username.fromString(username);
|
||||
|
||||
var channelAboutPage =
|
||||
await ChannelAboutPage.getByUsername(_httpClient, username.value);
|
||||
var id = channelAboutPage.initialData;
|
||||
assert(id != null);
|
||||
return ChannelAbout(
|
||||
id.description,
|
||||
id.viewCount,
|
||||
id.joinDate,
|
||||
id.title,
|
||||
[
|
||||
for (var e in id.avatar)
|
||||
ChannelThumbnail(Uri.parse(e.url), e.height, e.width)
|
||||
],
|
||||
id.country,
|
||||
id.channelLinks);
|
||||
}
|
||||
|
||||
/// Gets the metadata associated with the channel
|
||||
/// that uploaded the specified video.
|
||||
Future<Channel> getByVideo(dynamic videoId) async {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a channel link.
|
||||
class ChannelLink with EquatableMixin {
|
||||
/// Link title.
|
||||
final String title;
|
||||
|
||||
/// Link URL.
|
||||
/// Already decoded with the YouTube shortener already taken out.
|
||||
final Uri url;
|
||||
|
||||
/// Link Icon URL.
|
||||
final Uri icon;
|
||||
|
||||
/// Initialize an instance of [ChannelLink]
|
||||
ChannelLink(this.title, this.url, this.icon);
|
||||
|
||||
@override
|
||||
List<Object> get props => [title, url, icon];
|
||||
|
||||
@override
|
||||
String toString() => 'Link: $title ($url): $icon';
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represent a channel thumbnail
|
||||
class ChannelThumbnail with EquatableMixin {
|
||||
/// Image url.
|
||||
final Uri url;
|
||||
|
||||
/// Image height.
|
||||
final int height;
|
||||
|
||||
/// Image width.
|
||||
final int width;
|
||||
|
||||
/// Initialize an instance of [ChannelThumbnail].
|
||||
ChannelThumbnail(this.url, this.height, this.width);
|
||||
|
||||
@override
|
||||
List<Object> get props => [url, height, width];
|
||||
}
|
|
@ -4,8 +4,11 @@
|
|||
library youtube_explode.channels;
|
||||
|
||||
export 'channel.dart';
|
||||
export 'channel_about.dart';
|
||||
export 'channel_client.dart';
|
||||
export 'channel_id.dart';
|
||||
export 'channel_link.dart';
|
||||
export 'channel_thumbnail.dart';
|
||||
export 'channel_video.dart';
|
||||
export 'username.dart';
|
||||
export 'video_sorting.dart';
|
||||
|
|
|
@ -20,7 +20,4 @@ If this issue persists, please report it on the project's GitHub page.
|
|||
Request: ${response.request}
|
||||
Response: (${response.statusCode})
|
||||
''';
|
||||
|
||||
@override
|
||||
String toString() => 'FatalFailureException: $message';
|
||||
}
|
||||
|
|
|
@ -21,7 +21,4 @@ Unfortunately, there's nothing the library can do to work around this error.
|
|||
Request: ${response.request}
|
||||
Response: $response
|
||||
''';
|
||||
|
||||
@override
|
||||
String toString() => 'RequestLimitExceeded: $message';
|
||||
}
|
||||
|
|
|
@ -28,7 +28,4 @@ class VideoUnplayableException implements YoutubeExplodeException {
|
|||
VideoUnplayableException.notLiveStream(VideoId videoId)
|
||||
: message = 'Video \'$videoId\' is not an ongoing live stream.\n'
|
||||
'Live stream manifest is not available for this video';
|
||||
|
||||
@override
|
||||
String toString() => 'VideoUnplayableException: $message';
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ abstract class YoutubeExplodeException implements Exception {
|
|||
YoutubeExplodeException(this.message);
|
||||
|
||||
@override
|
||||
String toString();
|
||||
String toString() => '$runtimeType: $message}';
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import '../extensions/helpers_extension.dart';
|
|||
class PlaylistId with EquatableMixin {
|
||||
static final _regMatchExp =
|
||||
RegExp(r'youtube\..+?/playlist.*?list=(.*?)(?:&|/|$)');
|
||||
static final _compositeMatchExp = RegExp(
|
||||
'https://www.youtube.com/watch?v=b8m9zhNAgKs&list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr');
|
||||
static final _compositeMatchExp =
|
||||
RegExp(r'youtube\..+?/watch.*?list=(.*?)(?:&|/|$)');
|
||||
static final _shortCompositeMatchExp =
|
||||
RegExp(r'youtu\.be/.*?/.*?list=(.*?)(?:&|/|$)');
|
||||
static final _embedCompositeMatchExp =
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart' as parser;
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
import '../../exceptions/exceptions.dart';
|
||||
import '../../retry.dart';
|
||||
import '../youtube_http_client.dart';
|
||||
import 'generated/channel_about_page_id.g.dart';
|
||||
import '../../extensions/helpers_extension.dart';
|
||||
|
||||
///
|
||||
class ChannelAboutPage {
|
||||
final Document _root;
|
||||
|
||||
_InitialData _initialData;
|
||||
|
||||
///
|
||||
_InitialData get initialData =>
|
||||
_initialData ??= _InitialData(ChannelAboutPageId.fromRawJson(_extractJson(
|
||||
_root
|
||||
.querySelectorAll('script')
|
||||
.map((e) => e.text)
|
||||
.toList()
|
||||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
||||
'window["ytInitialData"] =')));
|
||||
|
||||
///
|
||||
bool get isOk => initialData != null;
|
||||
|
||||
///
|
||||
String get description => initialData.description;
|
||||
|
||||
String _extractJson(String html, String separator) {
|
||||
return _matchJson(
|
||||
html.substring(html.indexOf(separator) + separator.length));
|
||||
}
|
||||
|
||||
String _matchJson(String str) {
|
||||
var bracketCount = 0;
|
||||
int lastI;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
lastI = i;
|
||||
if (str[i] == '{') {
|
||||
bracketCount++;
|
||||
} else if (str[i] == '}') {
|
||||
bracketCount--;
|
||||
} else if (str[i] == ';') {
|
||||
if (bracketCount == 0) {
|
||||
return str.substring(0, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return str.substring(0, lastI + 1);
|
||||
}
|
||||
|
||||
///
|
||||
ChannelAboutPage(this._root);
|
||||
|
||||
///
|
||||
ChannelAboutPage.parse(String raw) : _root = parser.parse(raw);
|
||||
|
||||
///
|
||||
static Future<ChannelAboutPage> get(YoutubeHttpClient httpClient, String id) {
|
||||
var url = 'https://www.youtube.com/channel/$id/about?hl=en';
|
||||
|
||||
return retry(() async {
|
||||
var raw = await httpClient.getString(url);
|
||||
var result = ChannelAboutPage.parse(raw);
|
||||
|
||||
if (!result.isOk) {
|
||||
throw TransientFailureException('Channel about page is broken');
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
///
|
||||
static Future<ChannelAboutPage> getByUsername(
|
||||
YoutubeHttpClient httpClient, String username) {
|
||||
var url = 'https://www.youtube.com/user/$username/about?hl=en';
|
||||
|
||||
return retry(() async {
|
||||
var raw = await httpClient.getString(url);
|
||||
var result = ChannelAboutPage.parse(raw);
|
||||
|
||||
if (!result.isOk) {
|
||||
throw TransientFailureException('Channel about page is broken');
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final _urlExp = RegExp(r'q=([^=]*)$');
|
||||
|
||||
class _InitialData {
|
||||
// Json parsed class
|
||||
final ChannelAboutPageId root;
|
||||
|
||||
_InitialData(this.root);
|
||||
|
||||
/* Cache results */
|
||||
ChannelAboutFullMetadataRenderer _content;
|
||||
|
||||
ChannelAboutFullMetadataRenderer get content =>
|
||||
_content ??= getContentContext();
|
||||
|
||||
ChannelAboutFullMetadataRenderer getContentContext() {
|
||||
return root
|
||||
.contents
|
||||
.twoColumnBrowseResultsRenderer
|
||||
.tabs[5]
|
||||
.tabRenderer
|
||||
.content
|
||||
.sectionListRenderer
|
||||
.contents
|
||||
.first
|
||||
.itemSectionRenderer
|
||||
.contents
|
||||
.first
|
||||
.channelAboutFullMetadataRenderer;
|
||||
}
|
||||
|
||||
String get description => content.description.simpleText;
|
||||
|
||||
List<ChannelLink> get channelLinks {
|
||||
return content.primaryLinks
|
||||
.map((e) => ChannelLink(
|
||||
e.title.simpleText,
|
||||
extractUrl(e.navigationEndpoint?.commandMetadata?.webCommandMetadata
|
||||
?.url ??
|
||||
e.navigationEndpoint.urlEndpoint.url),
|
||||
Uri.parse(e.icon.thumbnails.first.url)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
int get viewCount =>
|
||||
int.parse(content.viewCountText.simpleText.stripNonDigits());
|
||||
|
||||
String get joinDate => content.joinedDateText.runs[1].text;
|
||||
|
||||
String get title => content.title.simpleText;
|
||||
|
||||
List<AvatarThumbnail> get avatar => content.avatar.thumbnails;
|
||||
|
||||
String get country => content.country.simpleText;
|
||||
|
||||
String parseRuns(List<dynamic> runs) =>
|
||||
runs?.map((e) => e.text)?.join() ?? '';
|
||||
|
||||
Uri extractUrl(String text) =>
|
||||
Uri.parse(Uri.decodeFull(_urlExp.firstMatch(text)?.group(1) ?? ''));
|
||||
}
|
|
@ -5,10 +5,10 @@ import 'package:html/parser.dart' as parser;
|
|||
|
||||
import '../../channels/channel_video.dart';
|
||||
import '../../exceptions/exceptions.dart';
|
||||
import '../../extensions/helpers_extension.dart';
|
||||
import '../../retry.dart';
|
||||
import '../../videos/videos.dart';
|
||||
import '../youtube_http_client.dart';
|
||||
import 'generated/channel_upload_page_id.g.dart';
|
||||
|
||||
///
|
||||
class ChannelUploadPage {
|
||||
|
@ -19,8 +19,8 @@ class ChannelUploadPage {
|
|||
_InitialData _initialData;
|
||||
|
||||
///
|
||||
_InitialData get initialData =>
|
||||
_initialData ??= _InitialData(json.decode(_matchJson(_extractJson(
|
||||
_InitialData get initialData => _initialData ??= _InitialData(
|
||||
ChannelUploadPageId.fromJson(json.decode(_extractJson(
|
||||
_root
|
||||
.querySelectorAll('script')
|
||||
.map((e) => e.text)
|
||||
|
@ -64,8 +64,8 @@ class ChannelUploadPage {
|
|||
'https://www.youtube.com/browse_ajax?ctoken=${initialData.continuation}&continuation=${initialData.continuation}&itct=${initialData.clickTrackingParams}';
|
||||
return retry(() async {
|
||||
var raw = await httpClient.getString(url);
|
||||
return ChannelUploadPage(
|
||||
null, channelId, _InitialData(json.decode(raw)[1]));
|
||||
return ChannelUploadPage(null, channelId,
|
||||
_InitialData(ChannelUploadPageId.fromJson(json.decode(raw)[1])));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -88,9 +88,9 @@ class ChannelUploadPage {
|
|||
|
||||
class _InitialData {
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
final ChannelUploadPageId root;
|
||||
|
||||
_InitialData(this._root);
|
||||
_InitialData(this.root);
|
||||
|
||||
/* Cache results */
|
||||
|
||||
|
@ -98,64 +98,71 @@ class _InitialData {
|
|||
String _continuation;
|
||||
String _clickTrackingParams;
|
||||
|
||||
List<Map<String, dynamic>> getContentContext(Map<String, dynamic> root) {
|
||||
if (root['contents'] != null) {
|
||||
return (_root['contents']['twoColumnBrowseResultsRenderer']['tabs']
|
||||
as List<dynamic>)
|
||||
.map((e) => e['tabRenderer'])
|
||||
.firstWhere((e) => e['selected'] == true)['content']
|
||||
['sectionListRenderer']['contents']
|
||||
.first['itemSectionRenderer']['contents']
|
||||
.first['gridRenderer']['items']
|
||||
.cast<Map<String, dynamic>>();
|
||||
List<GridRendererItem> getContentContext() {
|
||||
if (root.contents != null) {
|
||||
return root.contents.twoColumnBrowseResultsRenderer.tabs
|
||||
.map((e) => e.tabRenderer)
|
||||
.firstWhere((e) => e.selected)
|
||||
.content
|
||||
.sectionListRenderer
|
||||
.contents
|
||||
.first
|
||||
.itemSectionRenderer
|
||||
.contents
|
||||
.first
|
||||
.gridRenderer
|
||||
.items;
|
||||
}
|
||||
if (root['response'] != null) {
|
||||
return _root['response']['continuationContents']['gridContinuation']
|
||||
['items']
|
||||
.cast<Map<String, dynamic>>();
|
||||
if (root.response != null) {
|
||||
return root.response.continuationContents.gridContinuation.items;
|
||||
}
|
||||
throw FatalFailureException('Failed to get initial data context.');
|
||||
}
|
||||
|
||||
Map<String, dynamic> getContinuationContext(Map<String, dynamic> root) {
|
||||
if (_root['contents'] != null) {
|
||||
return (_root['contents']['twoColumnBrowseResultsRenderer']['tabs']
|
||||
as List<dynamic>)
|
||||
?.map((e) => e['tabRenderer'])
|
||||
?.firstWhere((e) => e['selected'] == true)['content']
|
||||
['sectionListRenderer']['contents']
|
||||
?.first['itemSectionRenderer']['contents']
|
||||
?.first['gridRenderer']['continuations']
|
||||
?.first['nextContinuationData']
|
||||
?.cast<String, dynamic>();
|
||||
NextContinuationData getContinuationContext() {
|
||||
if (root.contents != null) {
|
||||
return root.contents.twoColumnBrowseResultsRenderer.tabs
|
||||
.map((e) => e.tabRenderer)
|
||||
.firstWhere((e) => e.selected)
|
||||
.content
|
||||
.sectionListRenderer
|
||||
.contents
|
||||
.first
|
||||
.itemSectionRenderer
|
||||
.contents
|
||||
.first
|
||||
.gridRenderer
|
||||
.continuations
|
||||
.first
|
||||
.nextContinuationData;
|
||||
}
|
||||
if (_root['response'] != null) {
|
||||
return _root['response']['continuationContents']['gridContinuation']
|
||||
['continuations']
|
||||
?.first
|
||||
?.cast<String, dynamic>();
|
||||
if (root.response != null) {
|
||||
return root.response.continuationContents.gridContinuation.continuations
|
||||
.first.nextContinuationData;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ChannelVideo> get uploads => _uploads ??= getContentContext(_root)
|
||||
List<ChannelVideo> get uploads => _uploads ??= getContentContext()
|
||||
?.map(_parseContent)
|
||||
?.where((e) => e != null)
|
||||
?.toList()
|
||||
?.cast<ChannelVideo>();
|
||||
?.toList();
|
||||
|
||||
String get continuation => _continuation ??=
|
||||
getContinuationContext(_root)?.getValue('continuation') ?? '';
|
||||
String get continuation =>
|
||||
_continuation ??= getContinuationContext().continuation ?? '';
|
||||
|
||||
String get clickTrackingParams => _clickTrackingParams ??=
|
||||
getContinuationContext(_root)?.getValue('clickTrackingParams') ?? '';
|
||||
getContinuationContext()?.clickTrackingParams ?? '';
|
||||
|
||||
dynamic _parseContent(content) {
|
||||
if (content == null || content['gridVideoRenderer'] == null) {
|
||||
ChannelVideo _parseContent(GridRendererItem content) {
|
||||
if (content == null || content.gridVideoRenderer == null) {
|
||||
return null;
|
||||
}
|
||||
var video = content['gridVideoRenderer'] as Map<String, dynamic>;
|
||||
var video = content.gridVideoRenderer;
|
||||
return ChannelVideo(
|
||||
VideoId(video['videoId']), video['title']['simpleText']);
|
||||
VideoId(video.videoId),
|
||||
video.title?.simpleText ??
|
||||
video.title?.runs?.map((e) => e.text)?.join() ??
|
||||
'');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,7 @@ import '../youtube_http_client.dart';
|
|||
|
||||
///
|
||||
class EmbedPage {
|
||||
static final _playerConfigExp =
|
||||
RegExp(r"'PLAYER_CONFIG':\s*(\{.*\})\}");
|
||||
static final _playerConfigExp = RegExp(r"'PLAYER_CONFIG':\s*(\{.*\})\}");
|
||||
|
||||
final Document _root;
|
||||
_PlayerConfig _playerConfig;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,237 @@
|
|||
// To parse this JSON data, do
|
||||
//
|
||||
// final playerConfigJson = playerConfigJsonFromJson(jsonString);
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
class PlayerConfigJson {
|
||||
PlayerConfigJson({
|
||||
this.assets,
|
||||
this.attrs,
|
||||
this.args,
|
||||
});
|
||||
|
||||
final Assets assets;
|
||||
final Attrs attrs;
|
||||
final Args args;
|
||||
|
||||
factory PlayerConfigJson.fromRawJson(String str) =>
|
||||
PlayerConfigJson.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory PlayerConfigJson.fromJson(Map<String, dynamic> json) =>
|
||||
PlayerConfigJson(
|
||||
assets: json["assets"] == null ? null : Assets.fromJson(json["assets"]),
|
||||
attrs: json["attrs"] == null ? null : Attrs.fromJson(json["attrs"]),
|
||||
args: json["args"] == null ? null : Args.fromJson(json["args"]),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"assets": assets == null ? null : assets.toJson(),
|
||||
"attrs": attrs == null ? null : attrs.toJson(),
|
||||
"args": args == null ? null : args.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
class Args {
|
||||
Args({
|
||||
this.innertubeApiKey,
|
||||
this.showMiniplayerButton,
|
||||
this.useMiniplayerUi,
|
||||
this.gapiHintParams,
|
||||
this.playerResponse,
|
||||
this.cbrver,
|
||||
this.cbr,
|
||||
this.innertubeApiVersion,
|
||||
this.innertubeContextClientVersion,
|
||||
this.vssHost,
|
||||
this.hostLanguage,
|
||||
this.cr,
|
||||
this.externalFullscreen,
|
||||
this.useFastSizingOnWatchDefault,
|
||||
this.c,
|
||||
this.ps,
|
||||
this.csiPageType,
|
||||
this.cos,
|
||||
this.enablecsi,
|
||||
this.watermark,
|
||||
this.cver,
|
||||
this.transparentBackground,
|
||||
this.hl,
|
||||
this.enablejsapi,
|
||||
this.cosver,
|
||||
this.loaderUrl,
|
||||
});
|
||||
|
||||
final String innertubeApiKey;
|
||||
final String showMiniplayerButton;
|
||||
final String useMiniplayerUi;
|
||||
final String gapiHintParams;
|
||||
final String playerResponse;
|
||||
final String cbrver;
|
||||
final String cbr;
|
||||
final String innertubeApiVersion;
|
||||
final String innertubeContextClientVersion;
|
||||
final String vssHost;
|
||||
final String hostLanguage;
|
||||
final String cr;
|
||||
final bool externalFullscreen;
|
||||
final bool useFastSizingOnWatchDefault;
|
||||
final String c;
|
||||
final String ps;
|
||||
final String csiPageType;
|
||||
final String cos;
|
||||
final String enablecsi;
|
||||
final String watermark;
|
||||
final String cver;
|
||||
final String transparentBackground;
|
||||
final String hl;
|
||||
final String enablejsapi;
|
||||
final String cosver;
|
||||
final String loaderUrl;
|
||||
|
||||
factory Args.fromRawJson(String str) => Args.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory Args.fromJson(Map<String, dynamic> json) => Args(
|
||||
innertubeApiKey: json["innertube_api_key"] == null
|
||||
? null
|
||||
: json["innertube_api_key"],
|
||||
showMiniplayerButton: json["show_miniplayer_button"] == null
|
||||
? null
|
||||
: json["show_miniplayer_button"],
|
||||
useMiniplayerUi: json["use_miniplayer_ui"] == null
|
||||
? null
|
||||
: json["use_miniplayer_ui"],
|
||||
gapiHintParams:
|
||||
json["gapi_hint_params"] == null ? null : json["gapi_hint_params"],
|
||||
playerResponse:
|
||||
json["player_response"] == null ? null : json["player_response"],
|
||||
cbrver: json["cbrver"] == null ? null : json["cbrver"],
|
||||
cbr: json["cbr"] == null ? null : json["cbr"],
|
||||
innertubeApiVersion: json["innertube_api_version"] == null
|
||||
? null
|
||||
: json["innertube_api_version"],
|
||||
innertubeContextClientVersion:
|
||||
json["innertube_context_client_version"] == null
|
||||
? null
|
||||
: json["innertube_context_client_version"],
|
||||
vssHost: json["vss_host"] == null ? null : json["vss_host"],
|
||||
hostLanguage:
|
||||
json["host_language"] == null ? null : json["host_language"],
|
||||
cr: json["cr"] == null ? null : json["cr"],
|
||||
externalFullscreen: json["external_fullscreen"] == null
|
||||
? null
|
||||
: json["external_fullscreen"],
|
||||
useFastSizingOnWatchDefault:
|
||||
json["use_fast_sizing_on_watch_default"] == null
|
||||
? null
|
||||
: json["use_fast_sizing_on_watch_default"],
|
||||
c: json["c"] == null ? null : json["c"],
|
||||
ps: json["ps"] == null ? null : json["ps"],
|
||||
csiPageType:
|
||||
json["csi_page_type"] == null ? null : json["csi_page_type"],
|
||||
cos: json["cos"] == null ? null : json["cos"],
|
||||
enablecsi: json["enablecsi"] == null ? null : json["enablecsi"],
|
||||
watermark: json["watermark"] == null ? null : json["watermark"],
|
||||
cver: json["cver"] == null ? null : json["cver"],
|
||||
transparentBackground: json["transparent_background"] == null
|
||||
? null
|
||||
: json["transparent_background"],
|
||||
hl: json["hl"] == null ? null : json["hl"],
|
||||
enablejsapi: json["enablejsapi"] == null ? null : json["enablejsapi"],
|
||||
cosver: json["cosver"] == null ? null : json["cosver"],
|
||||
loaderUrl: json["loaderUrl"] == null ? null : json["loaderUrl"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"innertube_api_key": innertubeApiKey == null ? null : innertubeApiKey,
|
||||
"show_miniplayer_button":
|
||||
showMiniplayerButton == null ? null : showMiniplayerButton,
|
||||
"use_miniplayer_ui": useMiniplayerUi == null ? null : useMiniplayerUi,
|
||||
"gapi_hint_params": gapiHintParams == null ? null : gapiHintParams,
|
||||
"player_response": playerResponse == null ? null : playerResponse,
|
||||
"cbrver": cbrver == null ? null : cbrver,
|
||||
"cbr": cbr == null ? null : cbr,
|
||||
"innertube_api_version":
|
||||
innertubeApiVersion == null ? null : innertubeApiVersion,
|
||||
"innertube_context_client_version":
|
||||
innertubeContextClientVersion == null
|
||||
? null
|
||||
: innertubeContextClientVersion,
|
||||
"vss_host": vssHost == null ? null : vssHost,
|
||||
"host_language": hostLanguage == null ? null : hostLanguage,
|
||||
"cr": cr == null ? null : cr,
|
||||
"external_fullscreen":
|
||||
externalFullscreen == null ? null : externalFullscreen,
|
||||
"use_fast_sizing_on_watch_default": useFastSizingOnWatchDefault == null
|
||||
? null
|
||||
: useFastSizingOnWatchDefault,
|
||||
"c": c == null ? null : c,
|
||||
"ps": ps == null ? null : ps,
|
||||
"csi_page_type": csiPageType == null ? null : csiPageType,
|
||||
"cos": cos == null ? null : cos,
|
||||
"enablecsi": enablecsi == null ? null : enablecsi,
|
||||
"watermark": watermark == null ? null : watermark,
|
||||
"cver": cver == null ? null : cver,
|
||||
"transparent_background":
|
||||
transparentBackground == null ? null : transparentBackground,
|
||||
"hl": hl == null ? null : hl,
|
||||
"enablejsapi": enablejsapi == null ? null : enablejsapi,
|
||||
"cosver": cosver == null ? null : cosver,
|
||||
"loaderUrl": loaderUrl == null ? null : loaderUrl,
|
||||
};
|
||||
}
|
||||
|
||||
class Assets {
|
||||
Assets({
|
||||
this.playerCanaryState,
|
||||
this.js,
|
||||
this.css,
|
||||
});
|
||||
|
||||
final String playerCanaryState;
|
||||
final String js;
|
||||
final String css;
|
||||
|
||||
factory Assets.fromRawJson(String str) => Assets.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory Assets.fromJson(Map<String, dynamic> json) => Assets(
|
||||
playerCanaryState: json["player_canary_state"] == null
|
||||
? null
|
||||
: json["player_canary_state"],
|
||||
js: json["js"] == null ? null : json["js"],
|
||||
css: json["css"] == null ? null : json["css"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"player_canary_state":
|
||||
playerCanaryState == null ? null : playerCanaryState,
|
||||
"js": js == null ? null : js,
|
||||
"css": css == null ? null : css,
|
||||
};
|
||||
}
|
||||
|
||||
class Attrs {
|
||||
Attrs({
|
||||
this.id,
|
||||
});
|
||||
|
||||
final String id;
|
||||
|
||||
factory Attrs.fromRawJson(String str) => Attrs.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory Attrs.fromJson(Map<String, dynamic> json) => Attrs(
|
||||
id: json["id"] == null ? null : json["id"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id == null ? null : id,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
// To parse this JSON data, do
|
||||
//
|
||||
// final playlistResponseJson = playlistResponseJsonFromJson(jsonString);
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
class PlaylistResponseJson {
|
||||
PlaylistResponseJson({
|
||||
this.title,
|
||||
this.views,
|
||||
this.description,
|
||||
this.video,
|
||||
this.author,
|
||||
this.likes,
|
||||
this.dislikes,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final int views;
|
||||
final String description;
|
||||
final List<Video> video;
|
||||
final String author;
|
||||
final int likes;
|
||||
final int dislikes;
|
||||
|
||||
factory PlaylistResponseJson.fromRawJson(String str) =>
|
||||
PlaylistResponseJson.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory PlaylistResponseJson.fromJson(Map<String, dynamic> json) =>
|
||||
PlaylistResponseJson(
|
||||
title: json["title"] == null ? null : json["title"],
|
||||
views: json["views"] == null ? null : json["views"],
|
||||
description: json["description"] == null ? null : json["description"],
|
||||
video: json["video"] == null
|
||||
? null
|
||||
: List<Video>.from(json["video"].map((x) => Video.fromJson(x))),
|
||||
author: json["author"] == null ? null : json["author"],
|
||||
likes: json["likes"] == null ? null : json["likes"],
|
||||
dislikes: json["dislikes"] == null ? null : json["dislikes"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"title": title == null ? null : title,
|
||||
"views": views == null ? null : views,
|
||||
"description": description == null ? null : description,
|
||||
"video": video == null
|
||||
? null
|
||||
: List<dynamic>.from(video.map((x) => x.toJson())),
|
||||
"author": author == null ? null : author,
|
||||
"likes": likes == null ? null : likes,
|
||||
"dislikes": dislikes == null ? null : dislikes,
|
||||
};
|
||||
}
|
||||
|
||||
class Video {
|
||||
Video({
|
||||
this.sessionData,
|
||||
this.timeCreated,
|
||||
this.ccLicense,
|
||||
this.title,
|
||||
this.rating,
|
||||
this.isHd,
|
||||
this.privacy,
|
||||
this.lengthSeconds,
|
||||
this.keywords,
|
||||
this.views,
|
||||
this.encryptedId,
|
||||
this.likes,
|
||||
this.isCc,
|
||||
this.description,
|
||||
this.thumbnail,
|
||||
this.userId,
|
||||
this.added,
|
||||
this.endscreenAutoplaySessionData,
|
||||
this.comments,
|
||||
this.dislikes,
|
||||
this.categoryId,
|
||||
this.duration,
|
||||
this.author,
|
||||
});
|
||||
|
||||
final SessionData sessionData;
|
||||
final int timeCreated;
|
||||
final bool ccLicense;
|
||||
final String title;
|
||||
final num rating;
|
||||
final bool isHd;
|
||||
final Privacy privacy;
|
||||
final int lengthSeconds;
|
||||
final String keywords;
|
||||
final String views;
|
||||
final String encryptedId;
|
||||
final int likes;
|
||||
final bool isCc;
|
||||
final String description;
|
||||
final String thumbnail;
|
||||
final String userId;
|
||||
final String added;
|
||||
final EndscreenAutoplaySessionData endscreenAutoplaySessionData;
|
||||
final String comments;
|
||||
final int dislikes;
|
||||
final int categoryId;
|
||||
final String duration;
|
||||
final String author;
|
||||
|
||||
factory Video.fromRawJson(String str) => Video.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory Video.fromJson(Map<String, dynamic> json) => Video(
|
||||
sessionData: json["session_data"] == null
|
||||
? null
|
||||
: sessionDataValues.map[json["session_data"]],
|
||||
timeCreated: json["time_created"] == null ? null : json["time_created"],
|
||||
ccLicense: json["cc_license"] == null ? null : json["cc_license"],
|
||||
title: json["title"] == null ? null : json["title"],
|
||||
rating: json["rating"] == null ? null : json["rating"],
|
||||
isHd: json["is_hd"] == null ? null : json["is_hd"],
|
||||
privacy:
|
||||
json["privacy"] == null ? null : privacyValues.map[json["privacy"]],
|
||||
lengthSeconds:
|
||||
json["length_seconds"] == null ? null : json["length_seconds"],
|
||||
keywords: json["keywords"] == null ? null : json["keywords"],
|
||||
views: json["views"] == null ? null : json["views"],
|
||||
encryptedId: json["encrypted_id"] == null ? null : json["encrypted_id"],
|
||||
likes: json["likes"] == null ? null : json["likes"],
|
||||
isCc: json["is_cc"] == null ? null : json["is_cc"],
|
||||
description: json["description"] == null ? null : json["description"],
|
||||
thumbnail: json["thumbnail"] == null ? null : json["thumbnail"],
|
||||
userId: json["user_id"] == null ? null : json["user_id"],
|
||||
added: json["added"] == null ? null : json["added"],
|
||||
endscreenAutoplaySessionData:
|
||||
json["endscreen_autoplay_session_data"] == null
|
||||
? null
|
||||
: endscreenAutoplaySessionDataValues
|
||||
.map[json["endscreen_autoplay_session_data"]],
|
||||
comments: json["comments"] == null ? null : json["comments"],
|
||||
dislikes: json["dislikes"] == null ? null : json["dislikes"],
|
||||
categoryId: json["category_id"] == null ? null : json["category_id"],
|
||||
duration: json["duration"] == null ? null : json["duration"],
|
||||
author: json["author"] == null ? null : json["author"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"session_data":
|
||||
sessionData == null ? null : sessionDataValues.reverse[sessionData],
|
||||
"time_created": timeCreated == null ? null : timeCreated,
|
||||
"cc_license": ccLicense == null ? null : ccLicense,
|
||||
"title": title == null ? null : title,
|
||||
"rating": rating == null ? null : rating,
|
||||
"is_hd": isHd == null ? null : isHd,
|
||||
"privacy": privacy == null ? null : privacyValues.reverse[privacy],
|
||||
"length_seconds": lengthSeconds == null ? null : lengthSeconds,
|
||||
"keywords": keywords == null ? null : keywords,
|
||||
"views": views == null ? null : views,
|
||||
"encrypted_id": encryptedId == null ? null : encryptedId,
|
||||
"likes": likes == null ? null : likes,
|
||||
"is_cc": isCc == null ? null : isCc,
|
||||
"description": description == null ? null : description,
|
||||
"thumbnail": thumbnail == null ? null : thumbnail,
|
||||
"user_id": userId == null ? null : userId,
|
||||
"added": added == null ? null : added,
|
||||
"endscreen_autoplay_session_data": endscreenAutoplaySessionData == null
|
||||
? null
|
||||
: endscreenAutoplaySessionDataValues
|
||||
.reverse[endscreenAutoplaySessionData],
|
||||
"comments": comments == null ? null : comments,
|
||||
"dislikes": dislikes == null ? null : dislikes,
|
||||
"category_id": categoryId == null ? null : categoryId,
|
||||
"duration": duration == null ? null : duration,
|
||||
"author": author == null ? null : author,
|
||||
};
|
||||
}
|
||||
|
||||
enum EndscreenAutoplaySessionData { FEATURE_AUTOPLAY }
|
||||
|
||||
final endscreenAutoplaySessionDataValues = EnumValues(
|
||||
{"feature=autoplay": EndscreenAutoplaySessionData.FEATURE_AUTOPLAY});
|
||||
|
||||
enum Privacy { PUBLIC }
|
||||
|
||||
final privacyValues = EnumValues({"public": Privacy.PUBLIC});
|
||||
|
||||
enum SessionData { FEATURE_PLAYLIST }
|
||||
|
||||
final sessionDataValues =
|
||||
EnumValues({"feature=playlist": SessionData.FEATURE_PLAYLIST});
|
||||
|
||||
class EnumValues<T> {
|
||||
Map<String, T> map;
|
||||
Map<T, String> reverseMap;
|
||||
|
||||
EnumValues(this.map);
|
||||
|
||||
Map<T, String> get reverse {
|
||||
if (reverseMap == null) {
|
||||
reverseMap = map.map((k, v) => new MapEntry(v, k));
|
||||
}
|
||||
return reverseMap;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
Files in this directory where generated using https://app.quicktype.io/ , using as source the youtube api.
|
||||
https://pypi.org/project/jsonmerge/ was used to merge source from different requests.
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,14 +1,18 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:youtube_explode_dart/src/reverse_engineering/responses/generated/player_response.g.dart';
|
||||
|
||||
import '../../extensions/helpers_extension.dart';
|
||||
import 'stream_info_provider.dart';
|
||||
|
||||
///
|
||||
class PlayerResponse {
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
// Json parsed class
|
||||
PlayerResponseJson _root;
|
||||
|
||||
/// Json parsed map
|
||||
final Map<String, dynamic> _rawJson;
|
||||
|
||||
Iterable<StreamInfoProvider> _muxedStreams;
|
||||
Iterable<StreamInfoProvider> _adaptiveStreams;
|
||||
|
@ -17,7 +21,7 @@ class PlayerResponse {
|
|||
String _videoPlayabilityError;
|
||||
|
||||
///
|
||||
String get playabilityStatus => _root['playabilityStatus']['status'];
|
||||
String get playabilityStatus => _root.playabilityStatus.status;
|
||||
|
||||
///
|
||||
bool get isVideoAvailable => playabilityStatus.toLowerCase() != 'error';
|
||||
|
@ -26,41 +30,41 @@ class PlayerResponse {
|
|||
bool get isVideoPlayable => playabilityStatus.toLowerCase() == 'ok';
|
||||
|
||||
///
|
||||
String get videoTitle => _root['videoDetails']['title'];
|
||||
String get videoTitle => _root.videoDetails.title;
|
||||
|
||||
///
|
||||
String get videoAuthor => _root['videoDetails']['author'];
|
||||
String get videoAuthor => _root.videoDetails.author;
|
||||
|
||||
///
|
||||
DateTime get videoUploadDate => DateTime.parse(
|
||||
_root['microformat']['playerMicroformatRenderer']['uploadDate']);
|
||||
DateTime get videoUploadDate =>
|
||||
_root.microformat.playerMicroformatRenderer.uploadDate;
|
||||
|
||||
///
|
||||
String get videoChannelId => _root['videoDetails']['channelId'];
|
||||
String get videoChannelId => _root.videoDetails.channelId;
|
||||
|
||||
///
|
||||
Duration get videoDuration =>
|
||||
Duration(seconds: int.parse(_root['videoDetails']['lengthSeconds']));
|
||||
Duration(seconds: int.parse(_root.videoDetails.lengthSeconds));
|
||||
|
||||
///
|
||||
Iterable<String> get videoKeywords =>
|
||||
_root['videoDetails']['keywords']?.cast<String>() ?? const [];
|
||||
List<String> get videoKeywords => _root.videoDetails.keywords ?? const [];
|
||||
|
||||
///
|
||||
String get videoDescription => _root['videoDetails']['shortDescription'];
|
||||
String get videoDescription => _root.videoDetails.shortDescription;
|
||||
|
||||
///
|
||||
int get videoViewCount => int.parse(_root['videoDetails']['viewCount']);
|
||||
int get videoViewCount => int.parse(_root.videoDetails.viewCount);
|
||||
|
||||
//TODO: Get these types
|
||||
///
|
||||
// Can be null
|
||||
String get previewVideoId =>
|
||||
_root
|
||||
_rawJson
|
||||
.get('playabilityStatus')
|
||||
?.get('errorScreen')
|
||||
?.get('playerLegacyDesktopYpcTrailerRenderer')
|
||||
?.getValue('trailerVideoId') ??
|
||||
Uri.splitQueryString(_root
|
||||
Uri.splitQueryString(_rawJson
|
||||
.get('playabilityStatus')
|
||||
?.get('errorScreen')
|
||||
?.get('')
|
||||
|
@ -69,75 +73,70 @@ class PlayerResponse {
|
|||
'')['video_id'];
|
||||
|
||||
///
|
||||
bool get isLive => _root.get('videoDetails')?.getValue('isLive') ?? false;
|
||||
bool get isLive => _root.videoDetails.isLive ?? false;
|
||||
|
||||
///
|
||||
// Can be null
|
||||
String get hlsManifestUrl =>
|
||||
_root.get('streamingData')?.getValue('hlsManifestUrl');
|
||||
String get hlsManifestUrl => _root.streamingData?.hlsManifestUrl;
|
||||
|
||||
///
|
||||
// Can be null
|
||||
String get dashManifestUrl =>
|
||||
_root.get('streamingData')?.getValue('dashManifestUrl');
|
||||
String get dashManifestUrl => _root.streamingData?.dashManifestUrl;
|
||||
|
||||
///
|
||||
Iterable<StreamInfoProvider> get muxedStreams => _muxedStreams ??= _root
|
||||
?.get('streamingData')
|
||||
?.getValue('formats')
|
||||
?.map((e) => _StreamInfo(e))
|
||||
?.cast<StreamInfoProvider>() ??
|
||||
const <StreamInfoProvider>[];
|
||||
List<StreamInfoProvider> get muxedStreams =>
|
||||
_muxedStreams ??= _root.streamingData?.formats
|
||||
?.map((e) => _StreamInfo(e))
|
||||
?.cast<StreamInfoProvider>()
|
||||
?.toList() ??
|
||||
const <StreamInfoProvider>[];
|
||||
|
||||
///
|
||||
Iterable<StreamInfoProvider> get adaptiveStreams => _adaptiveStreams ??= _root
|
||||
?.get('streamingData')
|
||||
?.getValue('adaptiveFormats')
|
||||
?.map((e) => _StreamInfo(e))
|
||||
?.cast<StreamInfoProvider>() ??
|
||||
const <StreamInfoProvider>[];
|
||||
List<StreamInfoProvider> get adaptiveStreams =>
|
||||
_adaptiveStreams ??= _root.streamingData?.adaptiveFormats
|
||||
?.map((e) => _StreamInfo(e))
|
||||
?.cast<StreamInfoProvider>()
|
||||
?.toList() ??
|
||||
const [];
|
||||
|
||||
///
|
||||
List<StreamInfoProvider> get streams =>
|
||||
_streams ??= [...muxedStreams, ...adaptiveStreams];
|
||||
|
||||
///
|
||||
Iterable<ClosedCaptionTrack> get closedCaptionTrack =>
|
||||
_closedCaptionTrack ??= _root
|
||||
.get('captions')
|
||||
?.get('playerCaptionsTracklistRenderer')
|
||||
?.getValue('captionTracks')
|
||||
List<ClosedCaptionTrack> get closedCaptionTrack => _closedCaptionTrack ??=
|
||||
_root.captions?.playerCaptionsTracklistRenderer?.captionTracks
|
||||
?.map((e) => ClosedCaptionTrack(e))
|
||||
?.cast<ClosedCaptionTrack>() ??
|
||||
?.cast<ClosedCaptionTrack>()
|
||||
?.toList() ??
|
||||
const [];
|
||||
|
||||
///
|
||||
PlayerResponse(this._root);
|
||||
/// Can be null
|
||||
String getVideoPlayabilityError() =>
|
||||
_videoPlayabilityError ??= _root.playabilityStatus.reason;
|
||||
|
||||
///
|
||||
String getVideoPlayabilityError() => _videoPlayabilityError ??=
|
||||
_root.get('playabilityStatus')?.getValue('reason');
|
||||
|
||||
///
|
||||
PlayerResponse.parse(String raw) : _root = json.decode(raw);
|
||||
PlayerResponse.parse(String raw) : _rawJson = json.decode(raw) {
|
||||
_root = PlayerResponseJson.fromJson(_rawJson);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
class ClosedCaptionTrack {
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
// Json parsed class
|
||||
final CaptionTrack _root;
|
||||
|
||||
///
|
||||
String get url => _root['baseUrl'];
|
||||
String get url => _root.baseUrl;
|
||||
|
||||
///
|
||||
String get languageCode => _root['languageCode'];
|
||||
String get languageCode => _root.languageCode;
|
||||
|
||||
///
|
||||
String get languageName => _root['name']['simpleText'];
|
||||
String get languageName => _root.name.simpleText;
|
||||
|
||||
///
|
||||
bool get autoGenerated => _root['vssId'].toLowerCase().startsWith('a.');
|
||||
bool get autoGenerated => _root.vssId.toLowerCase().startsWith('a.');
|
||||
|
||||
///
|
||||
ClosedCaptionTrack(this._root);
|
||||
|
@ -146,8 +145,8 @@ class ClosedCaptionTrack {
|
|||
class _StreamInfo extends StreamInfoProvider {
|
||||
static final _contentLenExp = RegExp(r'[\?&]clen=(\d+)');
|
||||
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
// Json parsed class
|
||||
final Format _root;
|
||||
|
||||
int _bitrate;
|
||||
String _container;
|
||||
|
@ -159,38 +158,38 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
String _url;
|
||||
|
||||
@override
|
||||
int get bitrate => _bitrate ??= _root['bitrate'];
|
||||
int get bitrate => _bitrate ??= _root.bitrate;
|
||||
|
||||
@override
|
||||
String get container => _container ??= mimeType.subtype;
|
||||
|
||||
@override
|
||||
int get contentLength =>
|
||||
_contentLength ??= int.tryParse(_root['contentLength'] ?? '') ??
|
||||
_contentLength ??= int.tryParse(_root.contentLength ?? '') ??
|
||||
_contentLenExp.firstMatch(url)?.group(1);
|
||||
|
||||
@override
|
||||
int get framerate => _framerate ??= _root['fps'];
|
||||
int get framerate => _framerate ??= _root.fps;
|
||||
|
||||
@override
|
||||
String get signature =>
|
||||
_signature ??= Uri.splitQueryString(_root['signatureCipher'] ?? '')['s'];
|
||||
_signature ??= Uri.splitQueryString(_root.signatureCipher ?? '')['s'];
|
||||
|
||||
@override
|
||||
String get signatureParameter => _signatureParameter ??=
|
||||
Uri.splitQueryString(_root['cipher'] ?? '')['sp'] ??
|
||||
Uri.splitQueryString(_root['signatureCipher'] ?? '')['sp'];
|
||||
String get signatureParameter =>
|
||||
_signatureParameter ??= Uri.splitQueryString(_root.cipher ?? '')['sp'] ??
|
||||
Uri.splitQueryString(_root.signatureCipher ?? '')['sp'];
|
||||
|
||||
@override
|
||||
int get tag => _tag ??= _root['itag'];
|
||||
int get tag => _tag ??= _root.itag;
|
||||
|
||||
@override
|
||||
String get url => _url ??= _getUrl();
|
||||
|
||||
String _getUrl() {
|
||||
var url = _root['url'];
|
||||
url ??= Uri.splitQueryString(_root['cipher'] ?? '')['url'];
|
||||
url ??= Uri.splitQueryString(_root['signatureCipher'] ?? '')['url'];
|
||||
var url = _root.url;
|
||||
url ??= Uri.splitQueryString(_root.cipher ?? '')['url'];
|
||||
url ??= Uri.splitQueryString(_root.signatureCipher ?? '')['url'];
|
||||
return url;
|
||||
}
|
||||
|
||||
|
@ -203,17 +202,17 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
isAudioOnly ? null : codecs.split(',').first.trim().nullIfWhitespace;
|
||||
|
||||
@override
|
||||
int get videoHeight => _root['height'];
|
||||
int get videoHeight => _root.height;
|
||||
|
||||
@override
|
||||
String get videoQualityLabel => _root['qualityLabel'];
|
||||
String get videoQualityLabel => _root.qualityLabel;
|
||||
|
||||
@override
|
||||
int get videoWidth => _root['width'];
|
||||
int get videoWidth => _root.width;
|
||||
|
||||
bool get isAudioOnly => _isAudioOnly ??= mimeType.type == 'audio';
|
||||
|
||||
MediaType get mimeType => _mimeType ??= MediaType.parse(_root['mimeType']);
|
||||
MediaType get mimeType => _mimeType ??= MediaType.parse(_root.mimeType);
|
||||
|
||||
String get codecs =>
|
||||
_codecs ??= mimeType?.parameters['codecs']?.toLowerCase();
|
||||
|
|
|
@ -6,47 +6,47 @@ import '../../exceptions/exceptions.dart';
|
|||
import '../../extensions/helpers_extension.dart';
|
||||
import '../../retry.dart';
|
||||
import '../youtube_http_client.dart';
|
||||
import 'generated/playlist_response.g.dart';
|
||||
|
||||
///
|
||||
class PlaylistResponse {
|
||||
Iterable<_Video> _videos;
|
||||
List<_Video> _videos;
|
||||
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
PlaylistResponseJson _root;
|
||||
|
||||
///
|
||||
String get title => _root['title'];
|
||||
String get title => _root.title;
|
||||
|
||||
///
|
||||
String get author => _root['author'];
|
||||
String get author => _root.author;
|
||||
|
||||
///
|
||||
String get description => _root['description'];
|
||||
String get description => _root.description;
|
||||
|
||||
///
|
||||
ThumbnailSet get thumbnails => ThumbnailSet(videos.firstOrNull.id);
|
||||
|
||||
///
|
||||
int get viewCount => _root['views'];
|
||||
int get viewCount => _root.views;
|
||||
|
||||
///
|
||||
int get likeCount => _root['likes'];
|
||||
int get likeCount => _root.likes;
|
||||
|
||||
///
|
||||
int get dislikeCount => _root['dislikes'];
|
||||
int get dislikeCount => _root.dislikes;
|
||||
|
||||
///
|
||||
Iterable<_Video> get videos => _videos ??=
|
||||
_root['video']?.map((e) => _Video(e))?.cast<_Video>() ?? const <_Video>[];
|
||||
List<_Video> get videos =>
|
||||
_videos ??= _root.video.map((e) => _Video(e)).toList();
|
||||
|
||||
///
|
||||
PlaylistResponse(this._root);
|
||||
|
||||
///
|
||||
PlaylistResponse.parse(String raw) : _root = json.tryDecode(raw) {
|
||||
if (_root == null) {
|
||||
PlaylistResponse.parse(String raw) {
|
||||
final t = json.tryDecode(raw);
|
||||
if (t == null) {
|
||||
throw TransientFailureException('Playerlist response is broken.');
|
||||
}
|
||||
_root = PlaylistResponseJson.fromJson(t);
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -80,33 +80,33 @@ class PlaylistResponse {
|
|||
|
||||
class _Video {
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
final Video root;
|
||||
|
||||
_Video(this._root);
|
||||
_Video(this.root);
|
||||
|
||||
String get id => _root['encrypted_id'];
|
||||
String get id => root.encryptedId;
|
||||
|
||||
String get author => _root['author'];
|
||||
String get author => root.author;
|
||||
|
||||
ChannelId get channelId => ChannelId('UC${_root['user_id']}');
|
||||
ChannelId get channelId => ChannelId('UC${root.userId}');
|
||||
|
||||
DateTime get uploadDate =>
|
||||
DateTime.fromMillisecondsSinceEpoch(_root['time_created'] * 1000);
|
||||
DateTime.fromMillisecondsSinceEpoch(root.timeCreated * 1000);
|
||||
|
||||
String get title => _root['title'];
|
||||
String get title => root.title;
|
||||
|
||||
String get description => _root['description'];
|
||||
String get description => root.description;
|
||||
|
||||
Duration get duration => Duration(seconds: _root['length_seconds']);
|
||||
Duration get duration => Duration(seconds: root.lengthSeconds);
|
||||
|
||||
int get viewCount => int.parse((_root['views'] as String).stripNonDigits());
|
||||
int get viewCount => int.parse(root.views.stripNonDigits());
|
||||
|
||||
int get likes => _root['likes'];
|
||||
int get likes => root.likes;
|
||||
|
||||
int get dislikes => _root['dislikes'];
|
||||
int get dislikes => root.dislikes;
|
||||
|
||||
Iterable<String> get keywords => RegExp(r'"[^\"]+"|\S+')
|
||||
.allMatches(_root['keywords'])
|
||||
.allMatches(root.keywords)
|
||||
.map((e) => e.group(0))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
|
|
@ -2,49 +2,64 @@ import 'dart:convert';
|
|||
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart' as parser;
|
||||
import 'package:youtube_explode_dart/src/search/base_search_content.dart';
|
||||
|
||||
import '../../../youtube_explode_dart.dart';
|
||||
import '../../extensions/helpers_extension.dart';
|
||||
import '../../playlists/playlist_id.dart';
|
||||
import '../../retry.dart';
|
||||
import '../../search/related_query.dart';
|
||||
import '../../search/search_playlist.dart';
|
||||
import '../../search/search_video.dart';
|
||||
import '../../videos/videos.dart';
|
||||
import '../youtube_http_client.dart';
|
||||
import 'generated/search_page_id.g.dart' hide PlaylistId;
|
||||
|
||||
///
|
||||
class SearchPage {
|
||||
static final _xsfrTokenExp = RegExp('"XSRF_TOKEN":"(.+?)"');
|
||||
final _apiKeyExp = RegExp(r'"INNERTUBE_API_KEY":"(\w+?)"');
|
||||
|
||||
///
|
||||
final String queryString;
|
||||
final Document _root;
|
||||
|
||||
_InitialData _initialData;
|
||||
String _xsrfToken;
|
||||
String _apiKey;
|
||||
|
||||
///
|
||||
_InitialData get initialData =>
|
||||
_initialData ??= _InitialData(json.decode(_matchJson(_extractJson(
|
||||
_root
|
||||
.querySelectorAll('script')
|
||||
.map((e) => e.text)
|
||||
.toList()
|
||||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
||||
'window["ytInitialData"] ='))));
|
||||
|
||||
///
|
||||
String get xsfrToken => _xsrfToken ??= _xsfrTokenExp
|
||||
String get apiKey => _apiKey ??= _apiKeyExp
|
||||
.firstMatch(_root
|
||||
.querySelectorAll('script')
|
||||
.firstWhere((e) => _xsfrTokenExp.hasMatch(e.text))
|
||||
.firstWhere((e) => e.text.contains('INNERTUBE_API_KEY'))
|
||||
.text)
|
||||
.group(1);
|
||||
|
||||
_InitialData _initialData;
|
||||
|
||||
///
|
||||
_InitialData get initialData {
|
||||
if (_initialData != null) {
|
||||
return _initialData;
|
||||
}
|
||||
var scriptTag = _extractJson(
|
||||
_root.querySelectorAll('script').map((e) => e.text).toList().firstWhere(
|
||||
(e) => e.contains('window["ytInitialData"] ='),
|
||||
orElse: () => null),
|
||||
'window["ytInitialData"] =');
|
||||
scriptTag ??= _extractJson(
|
||||
_root.querySelectorAll('script').map((e) => e.text).toList().firstWhere(
|
||||
(e) => e.contains('var ytInitialData ='),
|
||||
orElse: () => '{}'),
|
||||
'var ytInitialData =');
|
||||
return _initialData ??= _InitialData(SearchPageId.fromRawJson(scriptTag));
|
||||
}
|
||||
|
||||
String _extractJson(String html, String separator) {
|
||||
return _matchJson(
|
||||
html.substring(html.indexOf(separator) + separator.length));
|
||||
if (html == null || separator == null) {
|
||||
return null;
|
||||
}
|
||||
var index = html.indexOf(separator) + separator.length;
|
||||
if (index > html.length) {
|
||||
return null;
|
||||
}
|
||||
return _matchJson(html.substring(index));
|
||||
}
|
||||
|
||||
String _matchJson(String str) {
|
||||
|
@ -67,47 +82,54 @@ class SearchPage {
|
|||
|
||||
///
|
||||
SearchPage(this._root, this.queryString,
|
||||
[_InitialData initalData, String xsfrToken])
|
||||
: _initialData = initalData,
|
||||
_xsrfToken = xsfrToken;
|
||||
[_InitialData initialData, this._apiKey])
|
||||
: _initialData = initialData;
|
||||
|
||||
///
|
||||
// TODO: Replace this in favour of async* when quering;
|
||||
Future<SearchPage> nextPage(YoutubeHttpClient httpClient) async {
|
||||
if (initialData.continuation == '') {
|
||||
if (initialData.continuationToken == '' ||
|
||||
initialData.estimatedResults == 0) {
|
||||
return null;
|
||||
}
|
||||
return get(httpClient, queryString,
|
||||
ctoken: initialData.continuation,
|
||||
itct: initialData.clickTrackingParams,
|
||||
xsrfToken: xsfrToken);
|
||||
token: initialData.continuationToken, key: apiKey);
|
||||
}
|
||||
|
||||
///
|
||||
static Future<SearchPage> get(
|
||||
YoutubeHttpClient httpClient, String queryString,
|
||||
{String ctoken, String itct, String xsrfToken}) {
|
||||
{String token, String key}) {
|
||||
if (token != null) {
|
||||
assert(key != null, 'A key must be supplied along with a token');
|
||||
var url = 'https://www.youtube.com/youtubei/v1/search?key=$key';
|
||||
|
||||
return retry(() async {
|
||||
var body = {
|
||||
'context': const {
|
||||
'client': {
|
||||
'hl': 'en',
|
||||
'clientName': 'WEB',
|
||||
'clientVersion': '2.20200911.04.00'
|
||||
}
|
||||
},
|
||||
'continuation': token
|
||||
};
|
||||
|
||||
var raw = await httpClient.post(url, body: json.encode(body));
|
||||
return SearchPage(null, queryString,
|
||||
_InitialData(SearchPageId.fromJson(json.decode(raw.body))), key);
|
||||
});
|
||||
// Ask for next page,
|
||||
|
||||
}
|
||||
var url =
|
||||
'https://www.youtube.com/results?search_query=${Uri.encodeQueryComponent(queryString)}';
|
||||
if (ctoken != null) {
|
||||
assert(itct != null, 'If ctoken is not null itct cannot be null');
|
||||
url += '&pbj=1';
|
||||
url += '&ctoken=${Uri.encodeQueryComponent(ctoken)}';
|
||||
url += '&continuation=${Uri.encodeQueryComponent(ctoken)}';
|
||||
url += '&itct=${Uri.encodeQueryComponent(itct)}';
|
||||
}
|
||||
return retry(() async {
|
||||
Map<String, String> body;
|
||||
if (xsrfToken != null) {
|
||||
body = {'session_token': xsrfToken};
|
||||
}
|
||||
var raw = await httpClient.postString(url, body: body);
|
||||
if (ctoken != null) {
|
||||
return SearchPage(
|
||||
null, queryString, _InitialData(json.decode(raw)[1]), xsrfToken);
|
||||
}
|
||||
var raw = await httpClient.getString(url);
|
||||
return SearchPage.parse(raw, queryString);
|
||||
});
|
||||
// ask for next page
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -116,115 +138,113 @@ class SearchPage {
|
|||
|
||||
class _InitialData {
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
final SearchPageId root;
|
||||
|
||||
_InitialData(this._root);
|
||||
_InitialData(this.root);
|
||||
|
||||
/* Cache results */
|
||||
|
||||
List<dynamic> _searchContent;
|
||||
List<dynamic> _relatedVideos;
|
||||
List<RelatedQuery> _relatedQueries;
|
||||
String _continuation;
|
||||
String _clickTrackingParams;
|
||||
|
||||
List<Map<String, dynamic>> getContentContext(Map<String, dynamic> root) {
|
||||
if (root['contents'] != null) {
|
||||
return _root['contents']['twoColumnSearchResultsRenderer']
|
||||
['primaryContents']['sectionListRenderer']['contents']
|
||||
.first['itemSectionRenderer']['contents']
|
||||
.cast<Map<String, dynamic>>();
|
||||
List<PurpleContent> getContentContext() {
|
||||
if (root.contents != null) {
|
||||
return root.contents.twoColumnSearchResultsRenderer.primaryContents
|
||||
.sectionListRenderer.contents.first.itemSectionRenderer.contents;
|
||||
}
|
||||
if (root['response'] != null) {
|
||||
return _root['response']['continuationContents']
|
||||
['itemSectionContinuation']['contents']
|
||||
.cast<Map<String, dynamic>>();
|
||||
if (root.onResponseReceivedCommands != null) {
|
||||
return root.onResponseReceivedCommands.first.appendContinuationItemsAction
|
||||
.continuationItems[0].itemSectionRenderer.contents;
|
||||
}
|
||||
throw FatalFailureException('Failed to get initial data context.');
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> getContinuationContext(Map<String, dynamic> root) {
|
||||
if (_root['contents'] != null) {
|
||||
return (_root['contents']['twoColumnSearchResultsRenderer']
|
||||
['primaryContents']['sectionListRenderer']['contents']
|
||||
?.first['itemSectionRenderer']['continuations']
|
||||
?.first as Map)
|
||||
?.getValue('nextContinuationData')
|
||||
?.cast<String, dynamic>();
|
||||
String _getContinuationToken() {
|
||||
if (root.contents != null) {
|
||||
var contents = root.contents.twoColumnSearchResultsRenderer
|
||||
.primaryContents.sectionListRenderer.contents;
|
||||
|
||||
if (contents.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
return contents[1]
|
||||
.continuationItemRenderer
|
||||
.continuationEndpoint
|
||||
.continuationCommand
|
||||
.token;
|
||||
}
|
||||
if (_root['response'] != null) {
|
||||
return _root['response']['continuationContents']
|
||||
['itemSectionContinuation']['continuations']
|
||||
?.first['nextContinuationData']
|
||||
?.cast<String, dynamic>();
|
||||
if (root.onResponseReceivedCommands != null) {
|
||||
return root
|
||||
.onResponseReceivedCommands
|
||||
.first
|
||||
.appendContinuationItemsAction
|
||||
.continuationItems[1]
|
||||
?.continuationItemRenderer
|
||||
?.continuationEndpoint
|
||||
?.continuationCommand
|
||||
?.token ??
|
||||
' ';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Contains only [SearchVideo] or [SearchPlaylist]
|
||||
List<dynamic> get searchContent => _searchContent ??= getContentContext(_root)
|
||||
.map(_parseContent)
|
||||
.where((e) => e != null)
|
||||
.toList();
|
||||
List<BaseSearchContent> get searchContent => _searchContent ??=
|
||||
getContentContext().map(_parseContent).where((e) => e != null).toList();
|
||||
|
||||
List<RelatedQuery> get relatedQueries =>
|
||||
(_relatedQueries ??= getContentContext(_root)
|
||||
?.where((e) => e.containsKey('horizontalCardListRenderer'))
|
||||
?.map((e) => e['horizontalCardListRenderer']['cards'])
|
||||
(_relatedQueries ??= getContentContext()
|
||||
?.where((e) => e.horizontalCardListRenderer != null)
|
||||
?.map((e) => e.horizontalCardListRenderer.cards)
|
||||
?.firstOrNull
|
||||
?.map((e) => e['searchRefinementCardRenderer'])
|
||||
?.map((e) => e.searchRefinementCardRenderer)
|
||||
?.map((e) => RelatedQuery(
|
||||
e['searchEndpoint']['searchEndpoint']['query'],
|
||||
VideoId(Uri.parse(e['thumbnail']['thumbnails'].first['url'])
|
||||
.pathSegments[1])))
|
||||
e.searchEndpoint.searchEndpoint.query,
|
||||
VideoId(
|
||||
Uri.parse(e.thumbnail.thumbnails.first.url).pathSegments[1])))
|
||||
?.toList()
|
||||
?.cast<RelatedQuery>()) ??
|
||||
const [];
|
||||
|
||||
List<dynamic> get relatedVideos =>
|
||||
(_relatedVideos ??= getContentContext(_root)
|
||||
?.where((e) => e.containsKey('shelfRenderer'))
|
||||
?.map((e) =>
|
||||
e['shelfRenderer']['content']['verticalListRenderer']['items'])
|
||||
(_relatedVideos ??= getContentContext()
|
||||
?.where((e) => e.shelfRenderer != null)
|
||||
?.map((e) => e.shelfRenderer.content.verticalListRenderer.items)
|
||||
?.firstOrNull
|
||||
?.map(_parseContent)
|
||||
?.toList()) ??
|
||||
const [];
|
||||
|
||||
String get continuation => _continuation ??=
|
||||
getContinuationContext(_root)?.getValue('continuation') ?? '';
|
||||
String get continuationToken => _getContinuationToken();
|
||||
|
||||
String get clickTrackingParams => _clickTrackingParams ??=
|
||||
getContinuationContext(_root)?.getValue('clickTrackingParams') ?? '';
|
||||
int get estimatedResults => int.parse(root.estimatedResults ?? 0);
|
||||
|
||||
int get estimatedResults => int.parse(_root['estimatedResults'] ?? 0);
|
||||
|
||||
dynamic _parseContent(dynamic content) {
|
||||
BaseSearchContent _parseContent(PurpleContent content) {
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
if (content.containsKey('videoRenderer')) {
|
||||
Map<String, dynamic> renderer = content['videoRenderer'];
|
||||
if (content.videoRenderer != null) {
|
||||
var renderer = content.videoRenderer;
|
||||
//TODO: Add if it's a live
|
||||
return SearchVideo(
|
||||
VideoId(renderer['videoId']),
|
||||
_parseRuns(renderer['title']),
|
||||
_parseRuns(renderer['ownerText']),
|
||||
_parseRuns(renderer['descriptionSnippet']),
|
||||
renderer.get('lengthText')?.getValue('simpleText') ?? '',
|
||||
int.parse(renderer['viewCountText']['simpleText']
|
||||
.toString()
|
||||
.stripNonDigits()
|
||||
.nullIfWhitespace ??
|
||||
VideoId(renderer.videoId),
|
||||
_parseRuns(renderer.title.runs),
|
||||
_parseRuns(renderer.ownerText.runs),
|
||||
_parseRuns(renderer.descriptionSnippet?.runs),
|
||||
renderer.lengthText?.simpleText ?? '',
|
||||
int.parse(renderer.viewCountText?.simpleText
|
||||
?.stripNonDigits()
|
||||
?.nullIfWhitespace ??
|
||||
'0'));
|
||||
}
|
||||
if (content.containsKey('radioRenderer')) {
|
||||
var renderer = content['radioRenderer'];
|
||||
if (content.radioRenderer != null) {
|
||||
var renderer = content.radioRenderer;
|
||||
|
||||
return SearchPlaylist(
|
||||
PlaylistId(renderer['playlistId']),
|
||||
renderer['title']['simpleText'],
|
||||
int.parse(_parseRuns(renderer['videoCountText'])
|
||||
PlaylistId(renderer.playlistId),
|
||||
renderer.title.simpleText,
|
||||
int.parse(_parseRuns(renderer.videoCountText.runs)
|
||||
.stripNonDigits()
|
||||
.nullIfWhitespace ??
|
||||
0));
|
||||
|
@ -233,38 +253,6 @@ class _InitialData {
|
|||
return null;
|
||||
}
|
||||
|
||||
String _parseRuns(Map<dynamic, dynamic> runs) =>
|
||||
runs?.getValue('runs')?.map((e) => e['text'])?.join() ?? '';
|
||||
String _parseRuns(List<dynamic> runs) =>
|
||||
runs?.map((e) => e.text)?.join() ?? '';
|
||||
}
|
||||
|
||||
// ['contents']['twoColumnSearchResultsRenderer']['primaryContents']
|
||||
// ['sectionListRenderer']['contents'].first['itemSectionRenderer']
|
||||
//
|
||||
//
|
||||
// ['contents'] -> @See ContentsList
|
||||
// ['continuations'] -> Data to see more
|
||||
|
||||
//ContentsList:
|
||||
// Key -> 'videoRenderer'
|
||||
// videoId --> VideoId
|
||||
// title['runs'].loop -> ['text'] -> concatenate --> "Video Title"
|
||||
// descriptionSnippet['runs'].loop -> ['text'] -> concatenate
|
||||
// --> "Video Description snippet"
|
||||
// ownerText['runs'].first -> ['text'] --> "Video Author"
|
||||
// lengthText['simpleText'] -> Parse format H:M:S -> "Video Duration"
|
||||
// viewCountText['simpleText'] -> Strip non digit -> int.parse
|
||||
// --> "Video View Count"
|
||||
//
|
||||
// Key -> 'radioRenderer'
|
||||
// playlistId -> PlaylistId
|
||||
// title['simpleText'] --> "Playlist Title"
|
||||
//
|
||||
// Key -> 'horizontalCardListRenderer' // Queries related to this search
|
||||
// cards --> List of Maps -> loop -> ['searchRefinementCardRenderer'].first
|
||||
// thumbnail -> ['thumbnails'].first -> ['url']
|
||||
// --> "Thumbnail url" -> Find video id from id.
|
||||
// searchEndpoint -> ['searchEndpoint']
|
||||
// -> ['query'] -> "Related query string"
|
||||
//
|
||||
// Key -> 'shelfRenderer' // Videos related to this search
|
||||
// contents -> ['verticalListRenderer']['items'] -> loop -> parseContent
|
||||
|
|
|
@ -2,15 +2,15 @@ import 'dart:convert';
|
|||
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart' as parser;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import '../../../youtube_explode_dart.dart';
|
||||
import '../../extensions/helpers_extension.dart';
|
||||
import '../../retry.dart';
|
||||
import '../../videos/video_id.dart';
|
||||
import '../youtube_http_client.dart';
|
||||
import 'generated/player_response_json.g.dart';
|
||||
import 'generated/watch_page_id.g.dart';
|
||||
import 'player_response.dart';
|
||||
import 'stream_info_provider.dart';
|
||||
|
||||
///
|
||||
class WatchPage {
|
||||
|
@ -37,13 +37,13 @@ class WatchPage {
|
|||
|
||||
///
|
||||
_InitialData get initialData =>
|
||||
_initialData ??= _InitialData(json.decode(_matchJson(_extractJson(
|
||||
_initialData ??= _InitialData(WatchPageId.fromRawJson(_extractJson(
|
||||
_root
|
||||
.querySelectorAll('script')
|
||||
.map((e) => e.text)
|
||||
.toList()
|
||||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
||||
'window["ytInitialData"] ='))));
|
||||
'window["ytInitialData"] =')));
|
||||
|
||||
///
|
||||
String get xsfrToken => _xsfrToken ??= _xsfrTokenExp
|
||||
|
@ -89,8 +89,8 @@ class WatchPage {
|
|||
static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\}\});');
|
||||
|
||||
///
|
||||
_PlayerConfig get playerConfig =>
|
||||
_playerConfig ??= _PlayerConfig(json.decode(_playerConfigExp
|
||||
_PlayerConfig get playerConfig => _playerConfig ??= _PlayerConfig(
|
||||
PlayerConfigJson.fromRawJson(_playerConfigExp
|
||||
.firstMatch(_root.getElementsByTagName('html').first.text)
|
||||
?.group(1)));
|
||||
|
||||
|
@ -147,99 +147,21 @@ class WatchPage {
|
|||
}
|
||||
}
|
||||
|
||||
class _StreamInfo extends StreamInfoProvider {
|
||||
final Map<String, String> _root;
|
||||
|
||||
_StreamInfo(this._root);
|
||||
|
||||
@override
|
||||
int get bitrate => int.parse(_root['bitrate']);
|
||||
|
||||
@override
|
||||
int get tag => int.parse(_root['itag']);
|
||||
|
||||
@override
|
||||
String get url => _root['url'];
|
||||
|
||||
@override
|
||||
String get signature => _root['s'];
|
||||
|
||||
@override
|
||||
String get signatureParameter => _root['sp'];
|
||||
|
||||
@override
|
||||
int get contentLength => int.tryParse(_root['clen'] ??
|
||||
StreamInfoProvider.contentLenExp
|
||||
.firstMatch(url)
|
||||
.group(1)
|
||||
.nullIfWhitespace ??
|
||||
'');
|
||||
|
||||
MediaType get mimeType => MediaType.parse(_root['mimeType']);
|
||||
|
||||
@override
|
||||
String get container => mimeType.subtype;
|
||||
|
||||
bool get isAudioOnly => mimeType.type == 'audio';
|
||||
|
||||
@override
|
||||
String get audioCodec => codecs.last;
|
||||
|
||||
@override
|
||||
String get videoCodec => isAudioOnly ? null : codecs.first;
|
||||
|
||||
List<String> get codecs =>
|
||||
mimeType.parameters['codecs'].split(',').map((e) => e.trim());
|
||||
|
||||
@override
|
||||
String get videoQualityLabel => _root['quality_label'];
|
||||
|
||||
List<int> get _size =>
|
||||
_root['size'].split(',').map((e) => int.tryParse(e ?? ''));
|
||||
|
||||
@override
|
||||
int get videoWidth => _size.first;
|
||||
|
||||
@override
|
||||
int get videoHeight => _size.last;
|
||||
|
||||
@override
|
||||
int get framerate => int.tryParse(_root['fps'] ?? '');
|
||||
}
|
||||
|
||||
class _PlayerConfig {
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> _root;
|
||||
final PlayerConfigJson root;
|
||||
|
||||
_PlayerConfig(this._root);
|
||||
_PlayerConfig(this.root);
|
||||
|
||||
String get sourceUrl => 'https://youtube.com${_root['assets']['js']}';
|
||||
String get sourceUrl => 'https://youtube.com${root.assets.js}';
|
||||
|
||||
PlayerResponse get playerResponse =>
|
||||
PlayerResponse.parse(_root['args']['player_response']);
|
||||
|
||||
List<_StreamInfo> get muxedStreams =>
|
||||
_root
|
||||
.get('args')
|
||||
?.getValue('url_encoded_fmt_stream_map')
|
||||
?.split(',')
|
||||
?.map((e) => _StreamInfo(Uri.splitQueryString(e))) ??
|
||||
const [];
|
||||
|
||||
List<_StreamInfo> get adaptiveStreams =>
|
||||
_root
|
||||
.get('args')
|
||||
?.getValue('adaptive_fmts')
|
||||
?.split(',')
|
||||
?.map((e) => _StreamInfo(Uri.splitQueryString(e))) ??
|
||||
const [];
|
||||
|
||||
List<_StreamInfo> get streams => [...muxedStreams, ...adaptiveStreams];
|
||||
PlayerResponse.parse(root.args.playerResponse);
|
||||
}
|
||||
|
||||
class _InitialData {
|
||||
// Json parsed map
|
||||
final Map<String, dynamic> root;
|
||||
final WatchPageId root;
|
||||
|
||||
_InitialData(this.root);
|
||||
|
||||
|
@ -248,26 +170,21 @@ class _InitialData {
|
|||
String _continuation;
|
||||
String _clickTrackingParams;
|
||||
|
||||
Map<String, dynamic> getContinuationContext(Map<String, dynamic> root) {
|
||||
if (root['contents'] != null) {
|
||||
return (root['contents']['twoColumnWatchNextResults']['results']
|
||||
['results']['contents'] as List<dynamic>)
|
||||
?.firstWhere((e) => e.containsKey('itemSectionRenderer'))[
|
||||
'itemSectionRenderer']['continuations']
|
||||
?.first['nextContinuationData']
|
||||
?.cast<String, dynamic>();
|
||||
}
|
||||
if (root['response'] != null) {
|
||||
return root['response']['itemSectionContinuation']['continuations']
|
||||
?.first['nextContinuationData']
|
||||
?.cast<String, dynamic>();
|
||||
NextContinuationData getContinuationContext() {
|
||||
if (root.contents != null) {
|
||||
return root.contents.twoColumnWatchNextResults.results.results.contents
|
||||
.firstWhere((e) => e.itemSectionRenderer != null)
|
||||
.itemSectionRenderer
|
||||
.continuations
|
||||
.first
|
||||
.nextContinuationData;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String get continuation => _continuation ??=
|
||||
getContinuationContext(root)?.getValue('continuation') ?? '';
|
||||
String get continuation =>
|
||||
_continuation ??= getContinuationContext()?.continuation ?? '';
|
||||
|
||||
String get clickTrackingParams => _clickTrackingParams ??=
|
||||
getContinuationContext(root)?.getValue('clickTrackingParams') ?? '';
|
||||
getContinuationContext()?.clickTrackingParams ?? '';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
///
|
||||
abstract class BaseSearchContent {
|
||||
///
|
||||
const BaseSearchContent();
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../videos/video_id.dart';
|
||||
|
||||
///
|
||||
class RelatedQuery {
|
||||
class RelatedQuery with EquatableMixin {
|
||||
/// Query related to a search query.
|
||||
final String query;
|
||||
|
||||
|
@ -10,4 +12,10 @@ class RelatedQuery {
|
|||
|
||||
/// Initialize a [RelatedQuery] instance.
|
||||
RelatedQuery(this.query, this.videoId);
|
||||
|
||||
@override
|
||||
String toString() => 'RelatedQuery($videoId): $query';
|
||||
|
||||
@override
|
||||
List<Object> get props => [query, videoId];
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import 'package:youtube_explode_dart/src/reverse_engineering/responses/search_page.dart';
|
||||
|
||||
import '../common/common.dart';
|
||||
import '../reverse_engineering/responses/playlist_response.dart';
|
||||
import '../reverse_engineering/youtube_http_client.dart';
|
||||
import '../videos/video.dart';
|
||||
import '../videos/video_id.dart';
|
||||
import 'base_search_content.dart';
|
||||
import 'search_query.dart';
|
||||
|
||||
/// YouTube search queries.
|
||||
|
@ -13,7 +16,8 @@ class SearchClient {
|
|||
SearchClient(this._httpClient);
|
||||
|
||||
/// Enumerates videos returned by the specified search query.
|
||||
Stream<Video> getVideosAsync(String searchQuery) async* {
|
||||
/// (from the YouTube Embedded API)
|
||||
Stream<Video> getVideos(String searchQuery) async* {
|
||||
var encounteredVideoIds = <String>{};
|
||||
|
||||
for (var page = 0; page < double.maxFinite; page++) {
|
||||
|
@ -49,7 +53,42 @@ class SearchClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enumerates videos returned by the specified search query
|
||||
/// (from the video search page).
|
||||
/// Contains only instances of [SearchVideo] or [SearchPlaylist]
|
||||
Stream<BaseSearchContent> getVideosFromPage(String searchQuery) async* {
|
||||
var page = await SearchPage.get(_httpClient, searchQuery);
|
||||
yield* Stream.fromIterable(page.initialData.searchContent);
|
||||
|
||||
// ignore: literal_only_boolean_expressions
|
||||
while (true) {
|
||||
page = await page.nextPage(_httpClient);
|
||||
if (page == null) {
|
||||
return;
|
||||
}
|
||||
yield* Stream.fromIterable(page.initialData.searchContent);
|
||||
}
|
||||
}
|
||||
|
||||
/// Queries to YouTube to get the results.
|
||||
@Deprecated('Use getVideosFromPage instead - '
|
||||
'Should be used only to get related videos')
|
||||
Future<SearchQuery> queryFromPage(String searchQuery) =>
|
||||
SearchQuery.search(_httpClient, searchQuery);
|
||||
}
|
||||
|
||||
/*
|
||||
channelId = ChannelId.fromString(channelId);
|
||||
var page = await ChannelUploadPage.get(
|
||||
_httpClient, channelId.value, videoSorting.code);
|
||||
yield* Stream.fromIterable(page.initialData.uploads);
|
||||
|
||||
// ignore: literal_only_boolean_expressions
|
||||
while (true) {
|
||||
page = await page.nextPage(_httpClient);
|
||||
if (page == null) {
|
||||
return;
|
||||
}
|
||||
yield* Stream.fromIterable(page.initialData.uploads);
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../playlists/playlist_id.dart';
|
||||
import 'base_search_content.dart';
|
||||
|
||||
/// Metadata related to a search query result (playlist)
|
||||
class SearchPlaylist with EquatableMixin {
|
||||
class SearchPlaylist extends BaseSearchContent with EquatableMixin {
|
||||
/// PlaylistId.
|
||||
final PlaylistId playlistId;
|
||||
|
||||
|
@ -17,7 +18,7 @@ class SearchPlaylist with EquatableMixin {
|
|||
SearchPlaylist(this.playlistId, this.playlistTitle, this.playlistVideoCount);
|
||||
|
||||
@override
|
||||
String toString() => '(Playlist) $playlistTitle ($playlistId)';
|
||||
String toString() => '[Playlist] $playlistTitle ($playlistId)';
|
||||
|
||||
@override
|
||||
List<Object> get props => [playlistId];
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import '../videos/video_id.dart';
|
||||
import 'base_search_content.dart';
|
||||
|
||||
/// Metadata related to a search query result (video).
|
||||
class SearchVideo {
|
||||
class SearchVideo extends BaseSearchContent {
|
||||
/// VideoId.
|
||||
final VideoId videoId;
|
||||
|
||||
|
|
|
@ -45,6 +45,8 @@ class CommentsClient {
|
|||
/// the results.
|
||||
///
|
||||
/// The streams doesn't emit any data if [Video.hasWatchPage] is false.
|
||||
/// Use `videos.get(videoId, forceWatchPage: true)` to assure that the
|
||||
/// WatchPage is fetched.
|
||||
Stream<Comment> getComments(Video video) async* {
|
||||
if (video.watchPage == null) {
|
||||
return;
|
||||
|
|
|
@ -104,10 +104,7 @@ class StreamsClient {
|
|||
throw VideoUnplayableException.liveStream(videoId);
|
||||
}
|
||||
|
||||
var streamInfoProviders = <StreamInfoProvider>[
|
||||
...playerConfig.streams,
|
||||
...playerResponse.streams
|
||||
];
|
||||
var streamInfoProviders = <StreamInfoProvider>[...playerResponse.streams];
|
||||
|
||||
var dashManifestUrl = playerResponse.dashManifestUrl;
|
||||
if (!dashManifestUrl.isNullOrWhiteSpace) {
|
||||
|
|
|
@ -71,9 +71,13 @@ class VideoClient {
|
|||
}
|
||||
|
||||
/// Get a [Video] instance from a [videoId]
|
||||
Future<Video> get(dynamic videoId) async {
|
||||
Future<Video> get(dynamic videoId, {forceWatchPage = false}) async {
|
||||
videoId = VideoId.fromString(videoId);
|
||||
|
||||
if (forceWatchPage) {
|
||||
return _getVideoFromWatchPage(videoId);
|
||||
}
|
||||
|
||||
try {
|
||||
return await _getVideoFromFixPlaylist(videoId);
|
||||
} on YoutubeExplodeException {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: youtube_explode_dart
|
||||
description: A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
|
||||
version: 1.5.2
|
||||
version: 1.6.0
|
||||
homepage: https://github.com/Hexer10/youtube_explode_dart
|
||||
|
||||
environment:
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
|
||||
test('Get a channel about page', () async {
|
||||
var channelUrl = 'https://www.youtube.com/user/FavijTV';
|
||||
var channel = await yt.channels.getAboutPageByUsername(channelUrl);
|
||||
expect(channel.country, 'Italy');
|
||||
expect(channel.thumbnails, isNotEmpty);
|
||||
expect(channel.channelLinks, isNotEmpty);
|
||||
expect(channel.description, isNotEmpty);
|
||||
expect(channel.joinDate, isNotEmpty);
|
||||
expect(channel.title, 'FavijTV');
|
||||
expect(channel.viewCount, greaterThanOrEqualTo(3631224938));
|
||||
});
|
||||
}
|
|
@ -2,34 +2,58 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('ChannelId', () {
|
||||
test('ValidChannelId', () {
|
||||
var channel1 = ChannelId('UCEnBXANsKmyj2r9xVyKoDiQ');
|
||||
var channel2 = ChannelId('UC46807r_RiRjH8IU-h_DrDQ');
|
||||
group('These are valid channel ids', () {
|
||||
for (var val in <dynamic>{
|
||||
[ChannelId('UCEnBXANsKmyj2r9xVyKoDiQ'), 'UCEnBXANsKmyj2r9xVyKoDiQ'],
|
||||
[ChannelId('UC46807r_RiRjH8IU-h_DrDQ'), 'UC46807r_RiRjH8IU-h_DrDQ'],
|
||||
}) {
|
||||
test('ChannelID - ${val[0]}', () {
|
||||
expect(val[0].value, val[1]);
|
||||
});
|
||||
}
|
||||
});
|
||||
group('These are valid channel urls', () {
|
||||
for (var val in <dynamic>{
|
||||
[
|
||||
ChannelId('youtube.com/channel/UC3xnGqlcL3y-GXz5N3wiTJQ'),
|
||||
'UC3xnGqlcL3y-GXz5N3wiTJQ'
|
||||
],
|
||||
[
|
||||
ChannelId('youtube.com/channel/UCkQO3QsgTpNTsOw6ujimT5Q'),
|
||||
'UCkQO3QsgTpNTsOw6ujimT5Q'
|
||||
],
|
||||
[
|
||||
ChannelId('youtube.com/channel/UCQtjJDOYluum87LA4sI6xcg'),
|
||||
'UCQtjJDOYluum87LA4sI6xcg'
|
||||
]
|
||||
}) {
|
||||
test('ChannelURL - ${val[0]}', () {
|
||||
expect(val[0].value, val[1]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
expect(channel1.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
|
||||
expect(channel2.value, 'UC46807r_RiRjH8IU-h_DrDQ');
|
||||
});
|
||||
test('ValidChannelUrl', () {
|
||||
var channel1 = ChannelId('youtube.com/channel/UC3xnGqlcL3y-GXz5N3wiTJQ');
|
||||
var channel2 = ChannelId('youtube.com/channel/UCkQO3QsgTpNTsOw6ujimT5Q');
|
||||
var channel3 = ChannelId('youtube.com/channel/UCQtjJDOYluum87LA4sI6xcg');
|
||||
group('These are not valid channel ids', () {
|
||||
for (var val in {
|
||||
'',
|
||||
'UC3xnGqlcL3y-GXz5N3wiTJ',
|
||||
'UC3xnGqlcL y-GXz5N3wiTJQ'
|
||||
}) {
|
||||
test('ChannelID - $val', () {
|
||||
expect(() => ChannelId(val), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
expect(channel1.value, 'UC3xnGqlcL3y-GXz5N3wiTJQ');
|
||||
expect(channel2.value, 'UCkQO3QsgTpNTsOw6ujimT5Q');
|
||||
expect(channel3.value, 'UCQtjJDOYluum87LA4sI6xcg');
|
||||
});
|
||||
test('InvalidChannelId', () {
|
||||
expect(() => ChannelId(''), throwsArgumentError);
|
||||
expect(() => ChannelId('UC3xnGqlcL3y-GXz5N3wiTJ'), throwsArgumentError);
|
||||
expect(() => ChannelId('UC3xnGqlcL y-GXz5N3wiTJQ'), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('InvalidChannelUrl', () {
|
||||
expect(() => ChannelId('youtube.com/?channel=UCUC3xnGqlcL3y-GXz5N3wiTJQ'),
|
||||
throwsArgumentError);
|
||||
expect(() => ChannelId('youtube.com/channel/asd'), throwsArgumentError);
|
||||
expect(() => ChannelId('youtube.com/'), throwsArgumentError);
|
||||
});
|
||||
group('These are not valid channel urls', () {
|
||||
for (var val in {
|
||||
'youtube.com/?channel=UCUC3xnGqlcL3y-GXz5N3wiTJQ',
|
||||
'youtube.com/channel/asd',
|
||||
'youtube.com/'
|
||||
}) {
|
||||
test('ChannelURL - $val', () {
|
||||
expect(() => ChannelId(val), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,81 +2,74 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('Channel', () {
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
|
||||
test('GetMetadataOfChannel', () async {
|
||||
var channelUrl =
|
||||
'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ';
|
||||
var channel = await yt.channels.get(ChannelId(channelUrl));
|
||||
expect(channel.url, channelUrl);
|
||||
expect(channel.title, 'Tyrrrz');
|
||||
expect(channel.logoUrl, isNotEmpty);
|
||||
expect(channel.logoUrl, isNot(equalsIgnoringWhitespace('')));
|
||||
});
|
||||
test('Get metadata of a channel', () async {
|
||||
var channelUrl = 'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ';
|
||||
var channel = await yt.channels.get(ChannelId(channelUrl));
|
||||
expect(channel.url, channelUrl);
|
||||
expect(channel.title, 'Tyrrrz');
|
||||
expect(channel.logoUrl, isNotEmpty);
|
||||
expect(channel.logoUrl, isNot(equalsIgnoringWhitespace('')));
|
||||
});
|
||||
|
||||
test('GetMetadataOfAnyChannel', () async {
|
||||
var channelId = ChannelId('UC46807r_RiRjH8IU-h_DrDQ');
|
||||
var channel = await yt.channels.get(channelId);
|
||||
expect(channel.id, channelId);
|
||||
group('Get metadata of any channel', () {
|
||||
for (var val in {
|
||||
'UC46807r_RiRjH8IU-h_DrDQ',
|
||||
'UCJ6td3C9QlPO9O_J5dF4ZzA',
|
||||
'UCiGm_E4ZwYSHV3bcW1pnSeQ'
|
||||
}) {
|
||||
test('Channel - $val', () async {
|
||||
var channelId = ChannelId(val);
|
||||
var channel = await yt.channels.get(channelId);
|
||||
expect(channel.id, channelId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
channelId = ChannelId('UCJ6td3C9QlPO9O_J5dF4ZzA');
|
||||
channel = await yt.channels.get(channelId);
|
||||
expect(channel.id, channelId);
|
||||
test('Get metadata of a channel by username', () async {
|
||||
var channel = await yt.channels.getByUsername(Username('TheTyrrr'));
|
||||
expect(channel.id.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
|
||||
});
|
||||
|
||||
channelId = ChannelId('UCiGm_E4ZwYSHV3bcW1pnSeQ');
|
||||
channel = await yt.channels.get(channelId);
|
||||
expect(channel.id, channelId);
|
||||
});
|
||||
test('Get metadata of a channel by a video', () async {
|
||||
var channel = await yt.channels.getByVideo(VideoId('5NmxuoNyDss'));
|
||||
expect(channel.id.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
|
||||
});
|
||||
|
||||
test('GetMetadataOfAnyChannelByUser', () async {
|
||||
var channel = await yt.channels.getByUsername(Username('TheTyrrr'));
|
||||
expect(channel.id.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
|
||||
});
|
||||
test('Get the videos of a youtube channel', () async {
|
||||
var videos = await yt.channels
|
||||
.getUploads(ChannelId(
|
||||
'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ'))
|
||||
.toList();
|
||||
expect(videos.length, greaterThanOrEqualTo(80));
|
||||
});
|
||||
|
||||
test('GetMetadataOfAnyChannelByVideo', () async {
|
||||
var channel = await yt.channels.getByVideo(VideoId('5NmxuoNyDss'));
|
||||
expect(channel.id.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
|
||||
});
|
||||
group('Get the videos of any youtube channel', () {
|
||||
for (var val in {
|
||||
'UC46807r_RiRjH8IU-h_DrDQ',
|
||||
'UCJ6td3C9QlPO9O_J5dF4ZzA',
|
||||
'UCiGm_E4ZwYSHV3bcW1pnSeQ'
|
||||
}) {
|
||||
test('Channel - $val', () async {
|
||||
var videos = await yt.channels.getUploads(ChannelId(val)).toList();
|
||||
expect(videos, isNotEmpty);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('GetVideosOfYoutubeChannel', () async {
|
||||
var videos = await yt.channels
|
||||
.getUploads(ChannelId(
|
||||
'https://www.youtube.com/channel/UCEnBXANsKmyj2r9xVyKoDiQ'))
|
||||
.toList();
|
||||
expect(videos.length, greaterThanOrEqualTo(80));
|
||||
});
|
||||
|
||||
test('GetVideosOfAnyYoutubeChannel', () async {
|
||||
var videos = await yt.channels
|
||||
.getUploads(ChannelId('UC46807r_RiRjH8IU-h_DrDQ'))
|
||||
.toList();
|
||||
expect(videos, isNotEmpty);
|
||||
|
||||
videos = await yt.channels
|
||||
.getUploads(ChannelId('UCJ6td3C9QlPO9O_J5dF4ZzA'))
|
||||
.toList();
|
||||
expect(videos, isNotEmpty);
|
||||
|
||||
videos = await yt.channels
|
||||
.getUploads(ChannelId('UCiGm_E4ZwYSHV3bcW1pnSeQ'))
|
||||
.toList();
|
||||
expect(videos, isNotEmpty);
|
||||
});
|
||||
|
||||
test('GetVideosOfYoutubeChannelFromUploadPage', () async {
|
||||
var videos = await yt.channels
|
||||
.getUploadsFromPage('UCEnBXANsKmyj2r9xVyKoDiQ')
|
||||
.take(30)
|
||||
.toList();
|
||||
expect(videos, hasLength(30));
|
||||
});
|
||||
test('Get videos of a youtube channel from the uploads page', () async {
|
||||
var videos = await yt.channels
|
||||
.getUploadsFromPage('UCEnBXANsKmyj2r9xVyKoDiQ')
|
||||
.take(30)
|
||||
.toList();
|
||||
expect(videos, hasLength(30));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,41 +2,38 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('Search', () {
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
|
||||
test('GetClosedCaptionTracksOfAnyVideo', () async {
|
||||
var manifest = await yt.videos.closedCaptions.getManifest('WOxr2dmLHLo');
|
||||
expect(manifest.tracks, isNotEmpty);
|
||||
});
|
||||
test('GetClosedCaptionTrackOfAnyVideoSpecific', () async {
|
||||
var manifest = await yt.videos.closedCaptions.getManifest('WOxr2dmLHLo');
|
||||
var trackInfo = manifest.tracks.first;
|
||||
var track = await yt.videos.closedCaptions.get(trackInfo);
|
||||
test('Get closed captions of a video', () async {
|
||||
var manifest = await yt.videos.closedCaptions.getManifest('WOxr2dmLHLo');
|
||||
expect(manifest.tracks, isNotEmpty);
|
||||
});
|
||||
test('Get closed caption track of a video', () async {
|
||||
var manifest = await yt.videos.closedCaptions.getManifest('WOxr2dmLHLo');
|
||||
var trackInfo = manifest.tracks.first;
|
||||
var track = await yt.videos.closedCaptions.get(trackInfo);
|
||||
|
||||
expect(track.captions, isNotEmpty);
|
||||
});
|
||||
test('GetClosedCaptionTrackAtSpecificTime', () async {
|
||||
var manifest = await yt.videos.closedCaptions
|
||||
.getManifest('https://www.youtube.com/watch?v=ppJy5uGZLi4');
|
||||
var trackInfo = manifest.getByLanguage('en');
|
||||
var track = await yt.videos.closedCaptions.get(trackInfo);
|
||||
var caption =
|
||||
track.getByTime(const Duration(hours: 0, minutes: 13, seconds: 22));
|
||||
var captionPart =
|
||||
caption.getPartByTime(const Duration(milliseconds: 200));
|
||||
expect(track.captions, isNotEmpty);
|
||||
});
|
||||
test('Get closed caption track at a specific time', () async {
|
||||
var manifest = await yt.videos.closedCaptions
|
||||
.getManifest('https://www.youtube.com/watch?v=ppJy5uGZLi4');
|
||||
var trackInfo = manifest.getByLanguage('en');
|
||||
var track = await yt.videos.closedCaptions.get(trackInfo);
|
||||
var caption =
|
||||
track.getByTime(const Duration(hours: 0, minutes: 13, seconds: 22));
|
||||
var captionPart = caption.getPartByTime(const Duration(milliseconds: 200));
|
||||
|
||||
expect(caption, isNotNull);
|
||||
expect(captionPart, isNotNull);
|
||||
expect(caption.text, 'how about this black there are some');
|
||||
expect(captionPart.text, ' about');
|
||||
});
|
||||
expect(caption, isNotNull);
|
||||
expect(captionPart, isNotNull);
|
||||
expect(caption.text, 'how about this black there are some');
|
||||
expect(captionPart.text, ' about');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,21 +2,19 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('Comments', () {
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
|
||||
test('GetCommentOfVideo', () async {
|
||||
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
|
||||
var video = await yt.videos.get(VideoId(videoUrl));
|
||||
var comments = await yt.videos.commentsClient.getComments(video).toList();
|
||||
expect(comments.length, greaterThanOrEqualTo(1));
|
||||
}, skip: 'This may fail on some environments');
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
|
||||
test('Get comments of a video', () async {
|
||||
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
|
||||
var video = await yt.videos.get(VideoId(videoUrl), forceWatchPage: true);
|
||||
var comments = await yt.videos.commentsClient.getComments(video).toList();
|
||||
expect(comments.length, greaterThanOrEqualTo(1));
|
||||
}, skip: 'This may fail on some environments');
|
||||
}
|
||||
|
|
|
@ -2,59 +2,85 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('PlaylistId', () {
|
||||
test('ValidPlaylistId', () {
|
||||
var data = const {
|
||||
'PL601B2E69B03FAB9D',
|
||||
'PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e',
|
||||
'PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk',
|
||||
'OLAK5uy_mtOdjCW76nDvf5yOzgcAVMYpJ5gcW5uKU',
|
||||
'RD1hu8-y6fKg0',
|
||||
'RDMMU-ty-2B02VY',
|
||||
'RDCLAK5uy_lf8okgl2ygD075nhnJVjlfhwp8NsUgEbs',
|
||||
'ULl6WWX-BgIiE',
|
||||
'UUTMt7iMWa7jy0fNXIktwyLA',
|
||||
'OLAK5uy_lLeonUugocG5J0EUAEDmbskX4emejKwcM',
|
||||
'FLEnBXANsKmyj2r9xVyKoDiQ'
|
||||
};
|
||||
// ignore: avoid_function_literals_in_foreach_calls
|
||||
data.forEach((playlistId) {
|
||||
var playlist = PlaylistId(playlistId);
|
||||
expect(playlist.value, playlistId);
|
||||
group('These are valid playlist ids', () {
|
||||
for (var val in {
|
||||
'PL601B2E69B03FAB9D',
|
||||
'PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e',
|
||||
'PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk',
|
||||
'OLAK5uy_mtOdjCW76nDvf5yOzgcAVMYpJ5gcW5uKU',
|
||||
'RD1hu8-y6fKg0',
|
||||
'RDMMU-ty-2B02VY',
|
||||
'RDCLAK5uy_lf8okgl2ygD075nhnJVjlfhwp8NsUgEbs',
|
||||
'ULl6WWX-BgIiE',
|
||||
'UUTMt7iMWa7jy0fNXIktwyLA',
|
||||
'FLEnBXANsKmyj2r9xVyKoDiQ'
|
||||
}) {
|
||||
test('PlaylistID - $val', () {
|
||||
var playlist = PlaylistId(val);
|
||||
expect(playlist.value, val);
|
||||
});
|
||||
});
|
||||
test('ValidPlaylistUrl', () {
|
||||
var data = const {
|
||||
'youtube.com/playlist?list=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H':
|
||||
'PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H',
|
||||
'youtube.com/watch?v=b8m9zhNAgKs&list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr':
|
||||
'PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr',
|
||||
'youtu.be/b8m9zhNAgKs/?list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr':
|
||||
'PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr',
|
||||
'youtube.com/embed/b8m9zhNAgKs/?list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr':
|
||||
'PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr',
|
||||
'youtube.com/watch?v=x2ZRoWQ0grU&list=RDEMNJhLy4rECJ_fG8NL-joqsg':
|
||||
'RDEMNJhLy4rECJ_fG8NL-joqsg'
|
||||
};
|
||||
data.forEach((url, playlistId) {
|
||||
var playlist = PlaylistId(playlistId);
|
||||
expect(playlist.value, playlistId);
|
||||
}
|
||||
});
|
||||
|
||||
group('These are valid playlist urls', () {
|
||||
for (var val in <dynamic>{
|
||||
[
|
||||
PlaylistId(
|
||||
'youtube.com/playlist?list=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H'),
|
||||
'PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H'
|
||||
],
|
||||
[
|
||||
PlaylistId(
|
||||
'youtube.com/watch?v=b8m9zhNAgKs&list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr'),
|
||||
'PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr'
|
||||
],
|
||||
[
|
||||
PlaylistId(
|
||||
'youtu.be/b8m9zhNAgKs/?list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr'),
|
||||
'PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr'
|
||||
],
|
||||
[
|
||||
PlaylistId(
|
||||
'youtube.com/embed/b8m9zhNAgKs/?list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr'),
|
||||
'PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr'
|
||||
],
|
||||
[
|
||||
PlaylistId(
|
||||
'youtube.com/watch?v=x2ZRoWQ0grU&list=RDEMNJhLy4rECJ_fG8NL-joqsg'),
|
||||
'RDEMNJhLy4rECJ_fG8NL-joqsg'
|
||||
],
|
||||
[
|
||||
PlaylistId(
|
||||
'youtube.com/watch?v=b8m9zhNAgKs&list=PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr'),
|
||||
'PL9tY0BWXOZFuFEG_GtOBZ8-8wbkH-NVAr'
|
||||
],
|
||||
}) {
|
||||
test('PlaylistID - ${val[0]}', () {
|
||||
expect(val[0].value, val[1]);
|
||||
});
|
||||
});
|
||||
test('InvalidPlaylistId', () {
|
||||
expect(() => PlaylistId('PLm_3vnTS-pvmZFuF L1Pyhqf8kTTYVKjW'),
|
||||
throwsArgumentError);
|
||||
expect(() => PlaylistId('PLm_3vnTS-pvmZFuF3L=Pyhqf8kTTYVKjW'),
|
||||
throwsArgumentError);
|
||||
});
|
||||
test('InvalidPlaylistUrl', () {
|
||||
expect(
|
||||
() => PlaylistId(
|
||||
'youtube.com/playlist?lisp=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H'),
|
||||
throwsArgumentError);
|
||||
expect(() => PlaylistId('youtube.com/playlist?list=asd'),
|
||||
throwsArgumentError);
|
||||
expect(() => PlaylistId('youtube.com/'), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('These are not valid playlist ids', () {
|
||||
for (var val in {
|
||||
'PLm_3vnTS-pvmZFuF L1Pyhqf8kTTYVKjW',
|
||||
'PLm_3vnTS-pvmZFuF3L=Pyhqf8kTTYVKjW'
|
||||
}) {
|
||||
test('PlaylistID - $val', () {
|
||||
expect(() => PlaylistId(val), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('These are not valid playlist urls', () {
|
||||
for (var val in {
|
||||
'youtube.com/playlist?lisp=PLOU2XLYxmsIJGErt5rrCqaSGTMyyqNt2H',
|
||||
'youtube.com/playlist?list=asd'
|
||||
'youtube.com/'
|
||||
}) {
|
||||
test('PlaylistURL - $val', () {
|
||||
expect(() => PlaylistId(val), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,84 +2,82 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('Playlist', () {
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
|
||||
test('GetMetadataOfPlaylist', () async {
|
||||
var playlistUrl =
|
||||
'https://www.youtube.com/playlist?list=PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd';
|
||||
var playlist = await yt.playlists.get(PlaylistId(playlistUrl));
|
||||
expect(playlist.id.value, 'PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd');
|
||||
expect(playlist.url, playlistUrl);
|
||||
expect(playlist.title, 'osu! Highlights');
|
||||
expect(playlist.author, 'Tyrrrz');
|
||||
expect(playlist.description, 'My best osu! plays');
|
||||
expect(playlist.engagement.viewCount, greaterThanOrEqualTo(133));
|
||||
expect(playlist.engagement.likeCount, greaterThanOrEqualTo(0));
|
||||
expect(playlist.engagement.dislikeCount, greaterThanOrEqualTo(0));
|
||||
expect(playlist.thumbnails.lowResUrl, isNotEmpty);
|
||||
expect(playlist.thumbnails.mediumResUrl, isNotEmpty);
|
||||
expect(playlist.thumbnails.highResUrl, isNotEmpty);
|
||||
expect(playlist.thumbnails.standardResUrl, isNotEmpty);
|
||||
expect(playlist.thumbnails.maxResUrl, isNotEmpty);
|
||||
});
|
||||
test('GetMetadataOfAnyPlaylist', () async {
|
||||
var data = {
|
||||
'PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e',
|
||||
'RD1hu8-y6fKg0',
|
||||
'RDMMU-ty-2B02VY',
|
||||
'RDCLAK5uy_lf8okgl2ygD075nhnJVjlfhwp8NsUgEbs',
|
||||
'OLAK5uy_lLeonUugocG5J0EUAEDmbskX4emejKwcM',
|
||||
'PL601B2E69B03FAB9D'
|
||||
};
|
||||
for (var playlistId in data) {
|
||||
var playlist = await yt.playlists.get(PlaylistId(playlistId));
|
||||
expect(playlist.id.value, playlistId);
|
||||
}
|
||||
});
|
||||
test('GetVideosInPlaylist', () async {
|
||||
var videos = await yt.playlists
|
||||
.getVideos(PlaylistId(
|
||||
'https://www.youtube.com/playlist?list=PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd'))
|
||||
.toList();
|
||||
expect(videos.length, greaterThanOrEqualTo(19));
|
||||
expect(
|
||||
videos.map((e) => e.id.value).toList(),
|
||||
containsAll([
|
||||
'B6N8-_rBTh8',
|
||||
'F1bvjgTckMc',
|
||||
'kMBzljXOb9g',
|
||||
'LsNPjFXIPT8',
|
||||
'fXYPMPglYTs',
|
||||
'AI7ULzgf8RU',
|
||||
'Qzu-fTdjeFY'
|
||||
]));
|
||||
});
|
||||
var data = const {
|
||||
'PL601B2E69B03FAB9D',
|
||||
'PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e',
|
||||
'PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk',
|
||||
'OLAK5uy_mtOdjCW76nDvf5yOzgcAVMYpJ5gcW5uKU',
|
||||
'RD1hu8-y6fKg0',
|
||||
'RDMMU-ty-2B02VY',
|
||||
'RDCLAK5uy_lf8okgl2ygD075nhnJVjlfhwp8NsUgEbs',
|
||||
'ULl6WWX-BgIiE',
|
||||
'UUTMt7iMWa7jy0fNXIktwyLA',
|
||||
'OLAK5uy_lLeonUugocG5J0EUAEDmbskX4emejKwcM',
|
||||
'FLEnBXANsKmyj2r9xVyKoDiQ'
|
||||
};
|
||||
for (var playlistId in data) {
|
||||
test('GetVideosInAnyPlaylist - $playlistId', () async {
|
||||
var videos =
|
||||
await yt.playlists.getVideos(PlaylistId(playlistId)).toList();
|
||||
expect(videos, isNotEmpty);
|
||||
test('Get metadata of a playlist', () async {
|
||||
var playlistUrl =
|
||||
'https://www.youtube.com/playlist?list=PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd';
|
||||
var playlist = await yt.playlists.get(PlaylistId(playlistUrl));
|
||||
expect(playlist.id.value, 'PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd');
|
||||
expect(playlist.url, playlistUrl);
|
||||
expect(playlist.title, 'osu! Highlights');
|
||||
expect(playlist.author, 'Tyrrrz');
|
||||
expect(playlist.description, 'My best osu! plays');
|
||||
expect(playlist.engagement.viewCount, greaterThanOrEqualTo(133));
|
||||
expect(playlist.engagement.likeCount, greaterThanOrEqualTo(0));
|
||||
expect(playlist.engagement.dislikeCount, greaterThanOrEqualTo(0));
|
||||
expect(playlist.thumbnails.lowResUrl, isNotEmpty);
|
||||
expect(playlist.thumbnails.mediumResUrl, isNotEmpty);
|
||||
expect(playlist.thumbnails.highResUrl, isNotEmpty);
|
||||
expect(playlist.thumbnails.standardResUrl, isNotEmpty);
|
||||
expect(playlist.thumbnails.maxResUrl, isNotEmpty);
|
||||
});
|
||||
group('Get metadata of any playlist', () {
|
||||
for (var val in {
|
||||
PlaylistId('PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e'),
|
||||
PlaylistId('RD1hu8-y6fKg0'),
|
||||
PlaylistId('RDMMU-ty-2B02VY'),
|
||||
PlaylistId('RDCLAK5uy_lf8okgl2ygD075nhnJVjlfhwp8NsUgEbs'),
|
||||
PlaylistId('PL601B2E69B03FAB9D')
|
||||
}) {
|
||||
test('PlaylistID - ${val.value}', () async {
|
||||
var playlist = await yt.playlists.get(val);
|
||||
expect(playlist.id.value, val.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Get videos in a playlist', () async {
|
||||
var videos = await yt.playlists
|
||||
.getVideos(PlaylistId(
|
||||
'https://www.youtube.com/playlist?list=PLr-IftNTIujSF-8tlGbZBQyGIT6TCF6Yd'))
|
||||
.toList();
|
||||
expect(videos.length, greaterThanOrEqualTo(19));
|
||||
expect(
|
||||
videos.map((e) => e.id.value).toList(),
|
||||
containsAll([
|
||||
'B6N8-_rBTh8',
|
||||
'F1bvjgTckMc',
|
||||
'kMBzljXOb9g',
|
||||
'LsNPjFXIPT8',
|
||||
'fXYPMPglYTs',
|
||||
'AI7ULzgf8RU',
|
||||
'Qzu-fTdjeFY'
|
||||
]));
|
||||
});
|
||||
|
||||
group('Get videos in any playlist', () {
|
||||
for (var val in {
|
||||
PlaylistId('PL601B2E69B03FAB9D'),
|
||||
PlaylistId('PLI5YfMzCfRtZ8eV576YoY3vIYrHjyVm_e'),
|
||||
PlaylistId('PLWwAypAcFRgKFlxtLbn_u14zddtDJj3mk'),
|
||||
PlaylistId('OLAK5uy_mtOdjCW76nDvf5yOzgcAVMYpJ5gcW5uKU'),
|
||||
PlaylistId('RD1hu8-y6fKg0'),
|
||||
PlaylistId('RDMMU-ty-2B02VY'),
|
||||
PlaylistId('RDCLAK5uy_lf8okgl2ygD075nhnJVjlfhwp8NsUgEbs'),
|
||||
PlaylistId('ULl6WWX-BgIiE'),
|
||||
PlaylistId('UUTMt7iMWa7jy0fNXIktwyLA'),
|
||||
PlaylistId('FLEnBXANsKmyj2r9xVyKoDiQ')
|
||||
}) {
|
||||
test('PlaylistID - ${val.value}', () async {
|
||||
expect(yt.playlists.getVideos(val), emits(isNotNull));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,39 +2,40 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('Search', () {
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
|
||||
test('SearchYouTubeVideosFromApi', () async {
|
||||
var videos = await yt.search
|
||||
.getVideosAsync('undead corporation megalomania')
|
||||
.toList();
|
||||
expect(videos, isNotEmpty);
|
||||
}, skip: 'Endpoint removed from YouTube');
|
||||
test('Search a youtube video from the api', () async {
|
||||
var videos =
|
||||
await yt.search.getVideos('undead corporation megalomania').toList();
|
||||
expect(videos, isNotEmpty);
|
||||
});
|
||||
|
||||
//TODO: Find out why this fails
|
||||
test('SearchYouTubeVideosFromPage', () async {
|
||||
var searchQuery = await yt.search.queryFromPage('hello');
|
||||
expect(searchQuery.content, isNotEmpty);
|
||||
expect(searchQuery.relatedVideos, isNotEmpty);
|
||||
expect(searchQuery.relatedQueries, isNotEmpty);
|
||||
}, skip: 'This may fail on some environments');
|
||||
test('Search a youtube videos from the search page', () async {
|
||||
var searchQuery = await yt.search.queryFromPage('hello');
|
||||
expect(searchQuery.content, isNotEmpty);
|
||||
expect(searchQuery.relatedVideos, isNotEmpty);
|
||||
expect(searchQuery.relatedQueries, isNotEmpty);
|
||||
});
|
||||
|
||||
test('SearchNoResults', () async {
|
||||
var query =
|
||||
await yt.search.queryFromPage('g;jghEOGHJeguEPOUIhjegoUEHGOGHPSASG');
|
||||
expect(query.content, isEmpty);
|
||||
expect(query.relatedQueries, isEmpty);
|
||||
expect(query.relatedVideos, isEmpty);
|
||||
var nextPage = await query.nextPage();
|
||||
expect(nextPage, isNull);
|
||||
});
|
||||
test('Search with no results', () async {
|
||||
var query =
|
||||
await yt.search.queryFromPage('g;jghEOGHJeguEPOUIhjegoUEHGOGHPSASG');
|
||||
expect(query.content, isEmpty);
|
||||
expect(query.relatedQueries, isEmpty);
|
||||
expect(query.relatedVideos, isEmpty);
|
||||
var nextPage = await query.nextPage();
|
||||
expect(nextPage, isNull);
|
||||
});
|
||||
|
||||
test('Search youtube videos from search page (stream)', () async {
|
||||
var query = await yt.search.getVideosFromPage('hello').take(30).toList();
|
||||
expect(query, hasLength(30));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,69 +2,65 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('Streams', () {
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
|
||||
var data = {
|
||||
'9bZkp7q19f0',
|
||||
'SkRSXFQerZs',
|
||||
'hySoCSoH-g8',
|
||||
'_kmeFXjjGfk',
|
||||
'MeJVWBSsPAY',
|
||||
'5VGm0dczmHc',
|
||||
'ZGdLIwrGHG8',
|
||||
'rsAAeyAr-9Y',
|
||||
'AI7ULzgf8RU'
|
||||
};
|
||||
for (var videoId in data) {
|
||||
test('GetStreamsOfAnyVideo - $videoId', () async {
|
||||
var manifest =
|
||||
await yt.videos.streamsClient.getManifest(VideoId(videoId));
|
||||
group('Get streams of any video', () {
|
||||
for (var val in {
|
||||
VideoId('9bZkp7q19f0'), // very popular
|
||||
VideoId('SkRSXFQerZs'), // age-restricted
|
||||
VideoId('hySoCSoH-g8'),
|
||||
VideoId('_kmeFXjjGfk'),
|
||||
VideoId('MeJVWBSsPAY'),
|
||||
VideoId('5VGm0dczmHc'), // rating is not allowed
|
||||
VideoId('ZGdLIwrGHG8'), // unlisted
|
||||
VideoId('rsAAeyAr-9Y'),
|
||||
VideoId('AI7ULzgf8RU')
|
||||
}) {
|
||||
test('VideoId - ${val.value}', () async {
|
||||
var manifest = await yt.videos.streamsClient.getManifest(val);
|
||||
expect(manifest.streams, isNotEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
test('GetStreamOfUnplayableVideo', () async {
|
||||
expect(yt.videos.streamsClient.getManifest(VideoId('5qap5aO4i9A')),
|
||||
throwsA(const TypeMatcher<VideoUnplayableException>()));
|
||||
});
|
||||
test('GetStreamOfPurchaseVideo', () async {
|
||||
expect(yt.videos.streamsClient.getManifest(VideoId('p3dDcKOFXQg')),
|
||||
throwsA(const TypeMatcher<VideoRequiresPurchaseException>()));
|
||||
});
|
||||
//TODO: Fix this with VideoRequiresPurchaseException.
|
||||
test('GetStreamOfPurchaseVideo', () async {
|
||||
expect(yt.videos.streamsClient.getManifest(VideoId('qld9w0b-1ao')),
|
||||
throwsA(const TypeMatcher<VideoUnavailableException>()));
|
||||
expect(yt.videos.streamsClient.getManifest(VideoId('pb_hHv3fByo')),
|
||||
throwsA(const TypeMatcher<VideoUnavailableException>()));
|
||||
});
|
||||
test('GetStreamOfAnyPlayableVideo', () async {
|
||||
var data = {
|
||||
'9bZkp7q19f0',
|
||||
'SkRSXFQerZs',
|
||||
'hySoCSoH-g8',
|
||||
'_kmeFXjjGfk',
|
||||
'MeJVWBSsPAY',
|
||||
'5VGm0dczmHc',
|
||||
'ZGdLIwrGHG8',
|
||||
'rsAAeyAr-9Y',
|
||||
};
|
||||
for (var videoId in data) {
|
||||
var manifest =
|
||||
await yt.videos.streamsClient.getManifest(VideoId(videoId));
|
||||
for (var streamInfo in manifest.streams) {
|
||||
var stream = await yt.videos.streamsClient.get(streamInfo).toList();
|
||||
expect(stream, isNotEmpty);
|
||||
}
|
||||
}
|
||||
}, timeout: const Timeout(Duration(minutes: 10)), skip: 'Takes too long.');
|
||||
});
|
||||
|
||||
test('Stream of paid videos throw VideoRequiresPurchaseException', () {
|
||||
expect(yt.videos.streamsClient.getManifest(VideoId('p3dDcKOFXQg')),
|
||||
throwsA(const TypeMatcher<VideoRequiresPurchaseException>()));
|
||||
});
|
||||
|
||||
group('Stream of unavailable videos throws VideoUnavailableException', () {
|
||||
for (var val in {VideoId('qld9w0b-1ao'), VideoId('pb_hHv3fByo')}) {
|
||||
test('VideoId - ${val.value}', () {
|
||||
expect(yt.videos.streamsClient.getManifest(val),
|
||||
throwsA(const TypeMatcher<VideoUnavailableException>()));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('Get stream of any playable video', () {
|
||||
for (var val in {
|
||||
VideoId('9bZkp7q19f0'),
|
||||
VideoId('SkRSXFQerZs'),
|
||||
VideoId('hySoCSoH-g8'),
|
||||
VideoId('_kmeFXjjGfk'),
|
||||
VideoId('MeJVWBSsPAY'),
|
||||
VideoId('5VGm0dczmHc'),
|
||||
VideoId('ZGdLIwrGHG8'),
|
||||
VideoId('rsAAeyAr-9Y'),
|
||||
}) {
|
||||
test('VideoId - ${val.value}', () async {
|
||||
var manifest = await yt.videos.streamsClient.getManifest(val);
|
||||
for (var streamInfo in manifest.streams) {
|
||||
expect(yt.videos.streamsClient.get(streamInfo), emits(isNotNull));
|
||||
}
|
||||
});
|
||||
}
|
||||
}, skip: 'Occasionally may fail with certain videos');
|
||||
}
|
||||
|
|
|
@ -2,34 +2,44 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('Username', () {
|
||||
test('ValidUsername', () {
|
||||
var data = const {'TheTyrrr', 'KannibalenRecords', 'JClayton1994'};
|
||||
// ignore: avoid_function_literals_in_foreach_calls
|
||||
data.forEach((usernameStr) {
|
||||
var username = Username(usernameStr);
|
||||
expect(username.value, usernameStr);
|
||||
group('These are valid usernames', () {
|
||||
for (var val in {'TheTyrrr', 'KannibalenRecords', 'JClayton1994'}) {
|
||||
test('Username - $val', () {
|
||||
expect(Username(val).value, val);
|
||||
});
|
||||
});
|
||||
test('ValidUsernameUrl', () {
|
||||
var data = const {
|
||||
'youtube.com/user/ProZD': 'ProZD',
|
||||
'youtube.com/user/TheTyrrr': 'TheTyrrr',
|
||||
};
|
||||
data.forEach((url, usernameStr) {
|
||||
var username = Username(url);
|
||||
expect(username.value, usernameStr);
|
||||
}
|
||||
});
|
||||
group('These are valid username urls', () {
|
||||
for (var val in {
|
||||
['youtube.com/user/ProZD', 'ProZD'],
|
||||
['youtube.com/user/TheTyrrr', 'TheTyrrr'],
|
||||
}) {
|
||||
test('UsernameURL - $val', () {
|
||||
expect(Username(val[0]).value, val[1]);
|
||||
});
|
||||
});
|
||||
test('InvalidUsername', () {
|
||||
expect(() => Username('The_Tyrrr'), throwsArgumentError);
|
||||
expect(() => Username('0123456789ABCDEFGHIJK'), throwsArgumentError);
|
||||
expect(() => Username('A1B2C3-'), throwsArgumentError);
|
||||
expect(() => Username('=0123456789ABCDEF'), throwsArgumentError);
|
||||
});
|
||||
test('InvalidUsernameUrl', () {
|
||||
expect(() => Username('youtube.com/user/P_roZD'), throwsArgumentError);
|
||||
expect(() => Username('youtube.com/user/P_roZD'), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
group('These are invalid usernames', () {
|
||||
for (var val in {
|
||||
'The_Tyrrr',
|
||||
'0123456789ABCDEFGHIJK',
|
||||
'A1B2C3-',
|
||||
'=0123456789ABCDEF'
|
||||
}) {
|
||||
test('Username - $val', () {
|
||||
expect(() => Username(val), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('These are not valid username urls', () {
|
||||
for (var val in {
|
||||
'youtube.com/user/P_roZD',
|
||||
'example.com/user/ProZD',
|
||||
}) {
|
||||
test('UsernameURL - $val', () {
|
||||
expect(() => Username(val), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,40 +2,40 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('VideoId', () {
|
||||
test('ValidVideoId', () {
|
||||
var data = const {
|
||||
'9bZkp7q19f0',
|
||||
'_kmeFXjjGfk',
|
||||
'AI7ULzgf8RU',
|
||||
};
|
||||
// ignore: avoid_function_literals_in_foreach_calls
|
||||
data.forEach((videoId) {
|
||||
var video = VideoId(videoId);
|
||||
expect(video.value, videoId);
|
||||
group('These are valid video ids', () {
|
||||
for (var val in {'9bZkp7q19f0', '_kmeFXjjGfk', 'AI7ULzgf8RU'}) {
|
||||
test('VideoID - $val', () {
|
||||
expect(VideoId(val).value, val);
|
||||
});
|
||||
});
|
||||
test('ValidVideoUrl', () {
|
||||
var data = const {
|
||||
'youtube.com/watch?v=yIVRs6YSbOM': 'yIVRs6YSbOM',
|
||||
'youtu.be/yIVRs6YSbOM': 'yIVRs6YSbOM',
|
||||
'youtube.com/embed/yIVRs6YSbOM': 'yIVRs6YSbOM',
|
||||
};
|
||||
data.forEach((url, videoId) {
|
||||
var video = VideoId(url);
|
||||
expect(video.value, videoId);
|
||||
}
|
||||
});
|
||||
group('These are valid video urls', () {
|
||||
for (var val in {
|
||||
['youtube.com/watch?v=yIVRs6YSbOM', 'yIVRs6YSbOM'],
|
||||
['youtu.be/yIVRs6YSbOM', 'yIVRs6YSbOM'],
|
||||
['youtube.com/embed/yIVRs6YSbOM', 'yIVRs6YSbOM'],
|
||||
}) {
|
||||
test('Video - $val', () {
|
||||
expect(VideoId(val[0]).value, val[1]);
|
||||
});
|
||||
});
|
||||
test('InvalidVideoId', () {
|
||||
expect(() => VideoId(''), throwsArgumentError);
|
||||
expect(() => VideoId('pI2I2zqzeK'), throwsArgumentError);
|
||||
expect(() => VideoId('pI2I2z zeKg'), throwsArgumentError);
|
||||
});
|
||||
test('InvalidVideoUrl', () {
|
||||
expect(
|
||||
() => VideoId('youtube.com/xxx?v=pI2I2zqzeKg'), throwsArgumentError);
|
||||
expect(() => VideoId('youtu.be/watch?v=xxx'), throwsArgumentError);
|
||||
expect(() => VideoId('youtube.com/embed/'), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
group('These are not valid video ids', () {
|
||||
for (var val in {'', 'pI2I2zqzeK', 'pI2I2z zeKg'}) {
|
||||
test('VideoID - $val', () {
|
||||
expect(() => VideoId(val), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
group('These are not valid video urls', () {
|
||||
for (var val in {
|
||||
'youtube.com/xxx?v=pI2I2zqzeKg',
|
||||
'youtu.be/watch?v=xxx',
|
||||
'youtube.com/embed'
|
||||
}) {
|
||||
test('VideoURL - $val', () {
|
||||
expect(() => VideoId(val), throwsArgumentError);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,60 +2,61 @@ import 'package:test/test.dart';
|
|||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
void main() {
|
||||
group('Video', () {
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
YoutubeExplode yt;
|
||||
setUp(() {
|
||||
yt = YoutubeExplode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
tearDown(() {
|
||||
yt.close();
|
||||
});
|
||||
|
||||
test('GetMetadataOfVideo', () async {
|
||||
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
|
||||
var video = await yt.videos.get(VideoId(videoUrl));
|
||||
expect(video.id.value, 'AI7ULzgf8RU');
|
||||
expect(video.url, videoUrl);
|
||||
expect(video.title, 'Aka no Ha [Another] +HDHR');
|
||||
expect(video.channelId.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
|
||||
expect(video.author, 'Tyrrrz');
|
||||
var rangeMs = DateTime(2017, 09, 30, 17, 15, 26).millisecondsSinceEpoch;
|
||||
// 1day margin since the uploadDate could differ from timezones
|
||||
expect(video.uploadDate.millisecondsSinceEpoch,
|
||||
inInclusiveRange(rangeMs - 86400000, rangeMs + 86400000));
|
||||
expect(video.description, contains('246pp'));
|
||||
expect(video.duration, const Duration(minutes: 1, seconds: 48));
|
||||
expect(video.thumbnails.lowResUrl, isNotEmpty);
|
||||
expect(video.thumbnails.mediumResUrl, isNotEmpty);
|
||||
expect(video.thumbnails.highResUrl, isNotEmpty);
|
||||
expect(video.thumbnails.standardResUrl, isNotEmpty);
|
||||
expect(video.thumbnails.maxResUrl, isNotEmpty);
|
||||
expect(video.keywords, orderedEquals(['osu', 'mouse', '"rhythm game"']));
|
||||
expect(video.engagement.viewCount, greaterThanOrEqualTo(134));
|
||||
expect(video.engagement.likeCount, greaterThanOrEqualTo(5));
|
||||
expect(video.engagement.dislikeCount, greaterThanOrEqualTo(0));
|
||||
});
|
||||
test('Get metadata of a video', () async {
|
||||
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
|
||||
var video = await yt.videos.get(VideoId(videoUrl));
|
||||
expect(video.id.value, 'AI7ULzgf8RU');
|
||||
expect(video.url, videoUrl);
|
||||
expect(video.title, 'Aka no Ha [Another] +HDHR');
|
||||
expect(video.channelId.value, 'UCEnBXANsKmyj2r9xVyKoDiQ');
|
||||
expect(video.author, 'Tyrrrz');
|
||||
var rangeMs = DateTime(2017, 09, 30, 17, 15, 26).millisecondsSinceEpoch;
|
||||
// 1day margin since the uploadDate could differ from timezones
|
||||
expect(video.uploadDate.millisecondsSinceEpoch,
|
||||
inInclusiveRange(rangeMs - 86400000, rangeMs + 86400000));
|
||||
expect(video.description, contains('246pp'));
|
||||
expect(video.duration, const Duration(minutes: 1, seconds: 48));
|
||||
expect(video.thumbnails.lowResUrl, isNotEmpty);
|
||||
expect(video.thumbnails.mediumResUrl, isNotEmpty);
|
||||
expect(video.thumbnails.highResUrl, isNotEmpty);
|
||||
expect(video.thumbnails.standardResUrl, isNotEmpty);
|
||||
expect(video.thumbnails.maxResUrl, isNotEmpty);
|
||||
expect(video.keywords, orderedEquals(['osu', 'mouse', 'rhythm game']));
|
||||
expect(video.engagement.viewCount, greaterThanOrEqualTo(134));
|
||||
expect(video.engagement.likeCount, greaterThanOrEqualTo(5));
|
||||
expect(video.engagement.dislikeCount, greaterThanOrEqualTo(0));
|
||||
});
|
||||
|
||||
test('GetMetadataOfAnyVideo', () async {
|
||||
var data = {
|
||||
'9bZkp7q19f0',
|
||||
'SkRSXFQerZs',
|
||||
'5VGm0dczmHc',
|
||||
'ZGdLIwrGHG8',
|
||||
'5qap5aO4i9A'
|
||||
};
|
||||
for (var videoId in data) {
|
||||
var video = await yt.videos.get(VideoId(videoId));
|
||||
expect(video.id.value, videoId);
|
||||
}
|
||||
});
|
||||
group('Get metadata of any video', () {
|
||||
for (var val in {
|
||||
VideoId('9bZkp7q19f0'),
|
||||
VideoId('SkRSXFQerZs'),
|
||||
VideoId('5VGm0dczmHc'),
|
||||
VideoId('ZGdLIwrGHG8'),
|
||||
VideoId('5qap5aO4i9A')
|
||||
}) {
|
||||
test('VideoId - ${val.value}', () async {
|
||||
var video = await yt.videos.get(val);
|
||||
expect(video.id.value, val.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('GetMetadataOfInvalidVideo', () async {
|
||||
expect(() async => yt.videos.get(VideoId('qld9w0b-1ao')),
|
||||
throwsA(const TypeMatcher<VideoUnplayableException>()));
|
||||
expect(() async => yt.videos.get(VideoId('pb_hHv3fByo')),
|
||||
throwsA(const TypeMatcher<VideoUnplayableException>()));
|
||||
});
|
||||
group('Get metadata of invalid videos throws VideoUnplayableException', () {
|
||||
for (var val in {VideoId('qld9w0b-1ao'), VideoId('pb_hHv3fByo')}) {
|
||||
test('VideoId - $val', () {
|
||||
expect(() async => yt.videos.get(val),
|
||||
throwsA(const TypeMatcher<VideoUnplayableException>()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue