Merge branch 'master' into master

This commit is contained in:
Mattia 2020-10-17 22:24:56 +02:00 committed by GitHub
commit e8fd6bae06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 32454 additions and 928 deletions

View File

@ -3,13 +3,23 @@
- 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)
## 1.5.1
- Fix Video Search: https://github.com/Tyrrrz/YoutubeExplode/issues/438
## 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

View File

@ -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\**

View File

@ -13,14 +13,11 @@ class Channel with EquatableMixin {
/// Channel title.
final String title;
/// Channel description
final String description;
/// URL of the channel's logo image.
final String logoUrl;
/// Initializes an instance of [Channel]
Channel(this.id, this.title, this.description, this.logoUrl);
Channel(this.id, this.title, this.logoUrl);
@override
String toString() => 'Channel ($title)';

View File

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

View File

@ -1,6 +1,8 @@
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_about_page.dart';
import '../reverse_engineering/responses/channel_upload_page.dart';
import '../reverse_engineering/responses/responses.dart';
import '../reverse_engineering/youtube_http_client.dart';
@ -25,10 +27,8 @@ class ChannelClient {
Future<Channel> get(dynamic id) async {
id = ChannelId.fromString(id);
var channelPage = await ChannelPage.get(_httpClient, id.value);
var channelAboutPage = await ChannelAboutPage.get(_httpClient, id.value);
return Channel(id, channelPage.channelTitle, channelAboutPage.description,
channelPage.channelLogoUrl);
return Channel(id, channelPage.channelTitle, channelPage.channelLogoUrl);
}
/// Gets the metadata associated with the channel of the specified user.
@ -39,11 +39,53 @@ class ChannelClient {
var channelPage =
await ChannelPage.getByUsername(_httpClient, username.value);
return Channel(ChannelId(channelPage.channelId), channelPage.channelTitle,
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);
return Channel(ChannelId(channelPage.channelId), channelPage.channelTitle,
channelAboutPage.description, channelPage.channelLogoUrl);
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,5 +7,5 @@ abstract class YoutubeExplodeException implements Exception {
YoutubeExplodeException(this.message);
@override
String toString();
String toString() => '$runtimeType: $message}';
}

View File

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

View File

@ -1,12 +1,12 @@
import 'dart:convert';
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 '../../extensions/helpers_extension.dart';
import '../../retry.dart';
import '../youtube_http_client.dart';
import 'generated/channel_about_page_id.g.dart';
import '../../extensions/helpers_extension.dart';
///
class ChannelAboutPage {
@ -16,13 +16,13 @@ class ChannelAboutPage {
///
_InitialData get initialData =>
_initialData ??= _InitialData(json.decode(_matchJson(_extractJson(
_initialData ??= _InitialData(ChannelAboutPageId.fromRawJson(_extractJson(
_root
.querySelectorAll('script')
.map((e) => e.text)
.toList()
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
'window["ytInitialData"] ='))));
'window["ytInitialData"] =')));
///
bool get isOk => initialData != null;
@ -91,23 +91,63 @@ class ChannelAboutPage {
}
}
final _urlExp = RegExp(r'q=([^=]*)$');
class _InitialData {
// Json parsed map
final Map<String, dynamic> root;
// Json parsed class
final ChannelAboutPageId root;
_InitialData(this.root);
/* Cache results */
ChannelAboutFullMetadataRenderer _content;
String _description;
ChannelAboutFullMetadataRenderer get content =>
_content ??= getContentContext();
Map<String, dynamic> getDescriptionContext(Map<String, dynamic> root) {
if (root['metadata'] != null) {
return root['metadata']['channelMetadataRenderer'];
}
return null;
ChannelAboutFullMetadataRenderer getContentContext() {
return root
.contents
.twoColumnBrowseResultsRenderer
.tabs[5]
.tabRenderer
.content
.sectionListRenderer
.contents
.first
.itemSectionRenderer
.contents
.first
.channelAboutFullMetadataRenderer;
}
String get description => _description ??=
getDescriptionContext(root)?.getValue('description') ?? '';
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) ?? ''));
}

View File

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

View File

@ -9,8 +9,7 @@ import '../youtube_http_client.dart';
///
class EmbedPage {
static final _playerConfigExp =
RegExp(r"yt\.setConfig\({'PLAYER_CONFIG':(.*)}\);");
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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ class PlayerSource {
var val = RegExp(r'(?<=invalid namespace.*?;[\w\s]+=)\d+')
.stringMatch(_root)
?.nullIfWhitespace ??
RegExp(r'(?<=this\.signatureTimestamp=)\d+"')
RegExp(r'(?<=this\.signatureTimestamp=)\d+')
.stringMatch(_root)
?.nullIfWhitespace;
if (val == null) {

View File

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

View File

@ -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,121 +138,114 @@ 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'];
final thumbnails = List<Map<String, dynamic>>.from(
renderer.get('thumbnail')?.getValue('thumbnails') ?? []
)..sort((a, b) => a['width'].compareTo(b['width']));
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 ?? '0'),
thumbnails.map<String>((thumb) => thumb['url']).toList(growable: false)
);
VideoId(renderer.videoId),
_parseRuns(renderer.title.runs),
_parseRuns(renderer.ownerText.runs),
_parseRuns(renderer.descriptionSnippet?.runs),
(renderer.thumbnail.thumbnails ?? [])..sort((a ,b) => a.width.compareTo(b.width));
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));
@ -239,38 +254,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

View File

@ -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
@ -86,11 +86,13 @@ class WatchPage {
?.nullIfWhitespace ??
'0');
static final _playerConfigExp = RegExp(r'ytplayer\.config\s*=\s*(\{.*\}\});');
///
_PlayerConfig get playerConfig =>
_playerConfig ??= _PlayerConfig(json.decode(_matchJson(_extractJson(
_root.getElementsByTagName('html').first.text,
'ytplayer.config = '))));
_PlayerConfig get playerConfig => _playerConfig ??= _PlayerConfig(
PlayerConfigJson.fromRawJson(_playerConfigExp
.firstMatch(_root.getElementsByTagName('html').first.text)
?.group(1)));
String _extractJson(String html, String separator) {
return _matchJson(
@ -145,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);
@ -246,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 ?? '';
}

View File

@ -0,0 +1,5 @@
///
abstract class BaseSearchContent {
///
const BaseSearchContent();
}

View File

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

View File

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

View File

@ -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];

View File

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

View File

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

View File

@ -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) {
@ -218,6 +215,7 @@ class StreamsClient {
// We can try to extract the manifest from two sources:
// get_video_info and the video watch page.
// In some cases one works, in some cases another does.
try {
var context = await _getStreamContextFromVideoInfo(videoId);
return _getManifest(context);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,47 +2,48 @@ 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 {
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('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 have thumbnails', () 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('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('SearchVideosHaveThumbnails', () async {
var searchQuery = await yt.search.queryFromPage('hello');
expect(searchQuery.content.first is SearchVideo, isTrue);
expect(searchQuery.content.first, isA<SearchVideo>());
var video = searchQuery.content.first as SearchVideo;
expect(video.videoThumbnails, isNotEmpty);
});
test('Search youtube videos from search page (stream)', () async {
var query = await yt.search.getVideosFromPage('hello').take(30).toList();
expect(query, hasLength(30));
});
}

View File

@ -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', age restricted videos are not supported anymore.
'hySoCSoH-g8',
'_kmeFXjjGfk',
'MeJVWBSsPAY',
'5VGm0dczmHc',
'ZGdLIwrGHG8',
// 'rsAAeyAr-9Y', video missing
'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');
}

View File

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

View File

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

View File

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