More Types
This commit is contained in:
parent
a582d88a63
commit
41a4947db2
|
@ -3,10 +3,16 @@
|
||||||
- Only throw custom exceptions from the library.
|
- Only throw custom exceptions from the library.
|
||||||
- `getUploadsFromPage` no longer throws.
|
- `getUploadsFromPage` no longer throws.
|
||||||
|
|
||||||
|
## 1.5.1
|
||||||
|
- 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.
|
||||||
|
|
||||||
## 1.5.0
|
## 1.5.0
|
||||||
- BREAKING CHANGE: Renamed `Container` class to `StreamContainer` to avoid conflicting with Flutter `Container`. See #66
|
- BREAKING CHANGE: Renamed `Container` class to `StreamContainer` to avoid conflicting with Flutter `Container`. See #66
|
||||||
|
|
||||||
|
|
||||||
## 1.4.4
|
## 1.4.4
|
||||||
- Expose HttpClient in APIs
|
- Expose HttpClient in APIs
|
||||||
- Fix #55: Typo in README.md
|
- Fix #55: Typo in README.md
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -85,7 +85,7 @@ class Video {
|
||||||
final int timeCreated;
|
final int timeCreated;
|
||||||
final bool ccLicense;
|
final bool ccLicense;
|
||||||
final String title;
|
final String title;
|
||||||
final int rating;
|
final num rating;
|
||||||
final bool isHd;
|
final bool isHd;
|
||||||
final Privacy privacy;
|
final Privacy privacy;
|
||||||
final int lengthSeconds;
|
final int lengthSeconds;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -37,12 +37,13 @@ class PlaylistResponse {
|
||||||
int get dislikeCount => _root.dislikes;
|
int get dislikeCount => _root.dislikes;
|
||||||
|
|
||||||
///
|
///
|
||||||
List<_Video> get videos => _videos ??= _root.video.map((e) => _Video(e));
|
List<_Video> get videos =>
|
||||||
|
_videos ??= _root.video.map((e) => _Video(e)).toList();
|
||||||
|
|
||||||
///
|
///
|
||||||
PlaylistResponse.parse(String raw) {
|
PlaylistResponse.parse(String raw) {
|
||||||
final t = json.tryDecode(raw);
|
final t = json.tryDecode(raw);
|
||||||
if (_root == null) {
|
if (t == null) {
|
||||||
throw TransientFailureException('Playerlist response is broken.');
|
throw TransientFailureException('Playerlist response is broken.');
|
||||||
}
|
}
|
||||||
_root = PlaylistResponseJson.fromJson(t);
|
_root = PlaylistResponseJson.fromJson(t);
|
||||||
|
|
|
@ -2,31 +2,40 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:html/parser.dart' as parser;
|
import 'package:html/parser.dart' as parser;
|
||||||
|
import 'package:youtube_explode_dart/src/search/base_search_content.dart';
|
||||||
|
|
||||||
import '../../../youtube_explode_dart.dart';
|
import '../../../youtube_explode_dart.dart';
|
||||||
import '../../extensions/helpers_extension.dart';
|
import '../../extensions/helpers_extension.dart';
|
||||||
import '../../playlists/playlist_id.dart';
|
|
||||||
import '../../retry.dart';
|
import '../../retry.dart';
|
||||||
import '../../search/related_query.dart';
|
import '../../search/related_query.dart';
|
||||||
import '../../search/search_playlist.dart';
|
|
||||||
import '../../search/search_video.dart';
|
import '../../search/search_video.dart';
|
||||||
import '../../videos/videos.dart';
|
import '../../videos/videos.dart';
|
||||||
import '../youtube_http_client.dart';
|
import '../youtube_http_client.dart';
|
||||||
|
import 'generated/search_page_id.g.dart' hide PlaylistId;
|
||||||
|
|
||||||
///
|
///
|
||||||
class SearchPage {
|
class SearchPage {
|
||||||
static final _xsfrTokenExp = RegExp('"XSRF_TOKEN":"(.+?)"');
|
final _apiKeyExp = RegExp(r'"INNERTUBE_API_KEY":"(\w+?)"');
|
||||||
|
|
||||||
///
|
///
|
||||||
final String queryString;
|
final String queryString;
|
||||||
final Document _root;
|
final Document _root;
|
||||||
|
|
||||||
|
String _apiKey;
|
||||||
|
|
||||||
|
///
|
||||||
|
String get apiKey => _apiKey ??= _apiKeyExp
|
||||||
|
.firstMatch(_root
|
||||||
|
.querySelectorAll('script')
|
||||||
|
.firstWhere((e) => e.text.contains('INNERTUBE_API_KEY'))
|
||||||
|
.text)
|
||||||
|
.group(1);
|
||||||
|
|
||||||
_InitialData _initialData;
|
_InitialData _initialData;
|
||||||
String _xsrfToken;
|
|
||||||
|
|
||||||
///
|
///
|
||||||
_InitialData get initialData =>
|
_InitialData get initialData =>
|
||||||
_initialData ??= _InitialData(json.decode(_extractJson(
|
_initialData ??= _InitialData(SearchPageId.fromRawJson(_extractJson(
|
||||||
_root
|
_root
|
||||||
.querySelectorAll('script')
|
.querySelectorAll('script')
|
||||||
.map((e) => e.text)
|
.map((e) => e.text)
|
||||||
|
@ -34,14 +43,6 @@ class SearchPage {
|
||||||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
||||||
'window["ytInitialData"] =')));
|
'window["ytInitialData"] =')));
|
||||||
|
|
||||||
///
|
|
||||||
String get xsfrToken => _xsrfToken ??= _xsfrTokenExp
|
|
||||||
.firstMatch(_root
|
|
||||||
.querySelectorAll('script')
|
|
||||||
.firstWhere((e) => _xsfrTokenExp.hasMatch(e.text))
|
|
||||||
.text)
|
|
||||||
.group(1);
|
|
||||||
|
|
||||||
String _extractJson(String html, String separator) {
|
String _extractJson(String html, String separator) {
|
||||||
return _matchJson(
|
return _matchJson(
|
||||||
html.substring(html.indexOf(separator) + separator.length));
|
html.substring(html.indexOf(separator) + separator.length));
|
||||||
|
@ -67,47 +68,54 @@ class SearchPage {
|
||||||
|
|
||||||
///
|
///
|
||||||
SearchPage(this._root, this.queryString,
|
SearchPage(this._root, this.queryString,
|
||||||
[_InitialData initalData, String xsfrToken])
|
[_InitialData initialData, this._apiKey])
|
||||||
: _initialData = initalData,
|
: _initialData = initialData;
|
||||||
_xsrfToken = xsfrToken;
|
|
||||||
|
|
||||||
///
|
///
|
||||||
// TODO: Replace this in favour of async* when quering;
|
// TODO: Replace this in favour of async* when quering;
|
||||||
Future<SearchPage> nextPage(YoutubeHttpClient httpClient) async {
|
Future<SearchPage> nextPage(YoutubeHttpClient httpClient) async {
|
||||||
if (initialData.continuation == '') {
|
if (initialData.continuationToken == '' ||
|
||||||
|
initialData.estimatedResults == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return get(httpClient, queryString,
|
return get(httpClient, queryString,
|
||||||
ctoken: initialData.continuation,
|
token: initialData.continuationToken, key: apiKey);
|
||||||
itct: initialData.clickTrackingParams,
|
|
||||||
xsrfToken: xsfrToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
static Future<SearchPage> get(
|
static Future<SearchPage> get(
|
||||||
YoutubeHttpClient httpClient, String queryString,
|
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 =
|
var url =
|
||||||
'https://www.youtube.com/results?search_query=${Uri.encodeQueryComponent(queryString)}';
|
'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 {
|
return retry(() async {
|
||||||
Map<String, String> body;
|
var raw = await httpClient.getString(url);
|
||||||
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);
|
|
||||||
}
|
|
||||||
return SearchPage.parse(raw, queryString);
|
return SearchPage.parse(raw, queryString);
|
||||||
});
|
});
|
||||||
|
// ask for next page
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@ -116,115 +124,113 @@ class SearchPage {
|
||||||
|
|
||||||
class _InitialData {
|
class _InitialData {
|
||||||
// Json parsed map
|
// Json parsed map
|
||||||
final Map<String, dynamic> _root;
|
final SearchPageId root;
|
||||||
|
|
||||||
_InitialData(this._root);
|
_InitialData(this.root);
|
||||||
|
|
||||||
/* Cache results */
|
/* Cache results */
|
||||||
|
|
||||||
List<dynamic> _searchContent;
|
List<dynamic> _searchContent;
|
||||||
List<dynamic> _relatedVideos;
|
List<dynamic> _relatedVideos;
|
||||||
List<RelatedQuery> _relatedQueries;
|
List<RelatedQuery> _relatedQueries;
|
||||||
String _continuation;
|
|
||||||
String _clickTrackingParams;
|
|
||||||
|
|
||||||
List<Map<String, dynamic>> getContentContext(Map<String, dynamic> root) {
|
List<PurpleContent> getContentContext() {
|
||||||
if (root['contents'] != null) {
|
if (root.contents != null) {
|
||||||
return _root['contents']['twoColumnSearchResultsRenderer']
|
return root.contents.twoColumnSearchResultsRenderer.primaryContents
|
||||||
['primaryContents']['sectionListRenderer']['contents']
|
.sectionListRenderer.contents.first.itemSectionRenderer.contents;
|
||||||
.first['itemSectionRenderer']['contents']
|
|
||||||
.cast<Map<String, dynamic>>();
|
|
||||||
}
|
}
|
||||||
if (root['response'] != null) {
|
if (root.onResponseReceivedCommands != null) {
|
||||||
return _root['response']['continuationContents']
|
return root.onResponseReceivedCommands.first.appendContinuationItemsAction
|
||||||
['itemSectionContinuation']['contents']
|
.continuationItems[0].itemSectionRenderer.contents;
|
||||||
.cast<Map<String, dynamic>>();
|
|
||||||
}
|
}
|
||||||
throw FatalFailureException('Failed to get initial data context.');
|
throw FatalFailureException('Failed to get initial data context.');
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> getContinuationContext(Map<String, dynamic> root) {
|
String _getContinuationToken() {
|
||||||
if (_root['contents'] != null) {
|
if (root.contents != null) {
|
||||||
return (_root['contents']['twoColumnSearchResultsRenderer']
|
var contents = root.contents.twoColumnSearchResultsRenderer
|
||||||
['primaryContents']['sectionListRenderer']['contents']
|
.primaryContents.sectionListRenderer.contents;
|
||||||
?.first['itemSectionRenderer']['continuations']
|
|
||||||
?.first as Map)
|
if (contents.length <= 1) {
|
||||||
?.getValue('nextContinuationData')
|
return null;
|
||||||
?.cast<String, dynamic>();
|
}
|
||||||
|
return contents[1]
|
||||||
|
.continuationItemRenderer
|
||||||
|
.continuationEndpoint
|
||||||
|
.continuationCommand
|
||||||
|
.token;
|
||||||
}
|
}
|
||||||
if (_root['response'] != null) {
|
if (root.onResponseReceivedCommands != null) {
|
||||||
return _root['response']['continuationContents']
|
return root
|
||||||
['itemSectionContinuation']['continuations']
|
.onResponseReceivedCommands
|
||||||
?.first['nextContinuationData']
|
.first
|
||||||
?.cast<String, dynamic>();
|
.appendContinuationItemsAction
|
||||||
|
.continuationItems[1]
|
||||||
|
?.continuationItemRenderer
|
||||||
|
?.continuationEndpoint
|
||||||
|
?.continuationCommand
|
||||||
|
?.token ??
|
||||||
|
' ';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains only [SearchVideo] or [SearchPlaylist]
|
// Contains only [SearchVideo] or [SearchPlaylist]
|
||||||
List<dynamic> get searchContent => _searchContent ??= getContentContext(_root)
|
List<BaseSearchContent> get searchContent => _searchContent ??=
|
||||||
.map(_parseContent)
|
getContentContext().map(_parseContent).where((e) => e != null).toList();
|
||||||
.where((e) => e != null)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
List<RelatedQuery> get relatedQueries =>
|
List<RelatedQuery> get relatedQueries =>
|
||||||
(_relatedQueries ??= getContentContext(_root)
|
(_relatedQueries ??= getContentContext()
|
||||||
?.where((e) => e.containsKey('horizontalCardListRenderer'))
|
?.where((e) => e.horizontalCardListRenderer != null)
|
||||||
?.map((e) => e['horizontalCardListRenderer']['cards'])
|
?.map((e) => e.horizontalCardListRenderer.cards)
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.map((e) => e['searchRefinementCardRenderer'])
|
?.map((e) => e.searchRefinementCardRenderer)
|
||||||
?.map((e) => RelatedQuery(
|
?.map((e) => RelatedQuery(
|
||||||
e['searchEndpoint']['searchEndpoint']['query'],
|
e.searchEndpoint.searchEndpoint.query,
|
||||||
VideoId(Uri.parse(e['thumbnail']['thumbnails'].first['url'])
|
VideoId(
|
||||||
.pathSegments[1])))
|
Uri.parse(e.thumbnail.thumbnails.first.url).pathSegments[1])))
|
||||||
?.toList()
|
?.toList()
|
||||||
?.cast<RelatedQuery>()) ??
|
?.cast<RelatedQuery>()) ??
|
||||||
const [];
|
const [];
|
||||||
|
|
||||||
List<dynamic> get relatedVideos =>
|
List<dynamic> get relatedVideos =>
|
||||||
(_relatedVideos ??= getContentContext(_root)
|
(_relatedVideos ??= getContentContext()
|
||||||
?.where((e) => e.containsKey('shelfRenderer'))
|
?.where((e) => e.shelfRenderer != null)
|
||||||
?.map((e) =>
|
?.map((e) => e.shelfRenderer.content.verticalListRenderer.items)
|
||||||
e['shelfRenderer']['content']['verticalListRenderer']['items'])
|
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.map(_parseContent)
|
?.map(_parseContent)
|
||||||
?.toList()) ??
|
?.toList()) ??
|
||||||
const [];
|
const [];
|
||||||
|
|
||||||
String get continuation => _continuation ??=
|
String get continuationToken => _getContinuationToken();
|
||||||
getContinuationContext(_root)?.getValue('continuation') ?? '';
|
|
||||||
|
|
||||||
String get clickTrackingParams => _clickTrackingParams ??=
|
int get estimatedResults => int.parse(root.estimatedResults ?? 0);
|
||||||
getContinuationContext(_root)?.getValue('clickTrackingParams') ?? '';
|
|
||||||
|
|
||||||
int get estimatedResults => int.parse(_root['estimatedResults'] ?? 0);
|
BaseSearchContent _parseContent(PurpleContent content) {
|
||||||
|
|
||||||
dynamic _parseContent(dynamic content) {
|
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (content.containsKey('videoRenderer')) {
|
if (content.videoRenderer != null) {
|
||||||
Map<String, dynamic> renderer = content['videoRenderer'];
|
var renderer = content.videoRenderer;
|
||||||
//TODO: Add if it's a live
|
//TODO: Add if it's a live
|
||||||
return SearchVideo(
|
return SearchVideo(
|
||||||
VideoId(renderer['videoId']),
|
VideoId(renderer.videoId),
|
||||||
_parseRuns(renderer['title']),
|
_parseRuns(renderer.title.runs),
|
||||||
_parseRuns(renderer['ownerText']),
|
_parseRuns(renderer.ownerText.runs),
|
||||||
_parseRuns(renderer['descriptionSnippet']),
|
_parseRuns(renderer.descriptionSnippet?.runs),
|
||||||
renderer.get('lengthText')?.getValue('simpleText') ?? '',
|
renderer.lengthText?.simpleText ?? '',
|
||||||
int.parse(renderer['viewCountText']['simpleText']
|
int.parse(renderer.viewCountText?.simpleText
|
||||||
.toString()
|
?.stripNonDigits()
|
||||||
.stripNonDigits()
|
?.nullIfWhitespace ??
|
||||||
.nullIfWhitespace ??
|
|
||||||
'0'));
|
'0'));
|
||||||
}
|
}
|
||||||
if (content.containsKey('radioRenderer')) {
|
if (content.radioRenderer != null) {
|
||||||
var renderer = content['radioRenderer'];
|
var renderer = content.radioRenderer;
|
||||||
|
|
||||||
return SearchPlaylist(
|
return SearchPlaylist(
|
||||||
PlaylistId(renderer['playlistId']),
|
PlaylistId(renderer.playlistId),
|
||||||
renderer['title']['simpleText'],
|
renderer.title.simpleText,
|
||||||
int.parse(_parseRuns(renderer['videoCountText'])
|
int.parse(_parseRuns(renderer.videoCountText.runs)
|
||||||
.stripNonDigits()
|
.stripNonDigits()
|
||||||
.nullIfWhitespace ??
|
.nullIfWhitespace ??
|
||||||
0));
|
0));
|
||||||
|
@ -233,38 +239,6 @@ class _InitialData {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _parseRuns(Map<dynamic, dynamic> runs) =>
|
String _parseRuns(List<dynamic> runs) =>
|
||||||
runs?.getValue('runs')?.map((e) => e['text'])?.join() ?? '';
|
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
|
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:html/parser.dart' as parser;
|
import 'package:html/parser.dart' as parser;
|
||||||
import 'package:http_parser/http_parser.dart';
|
|
||||||
|
|
||||||
import '../../../youtube_explode_dart.dart';
|
import '../../../youtube_explode_dart.dart';
|
||||||
import '../../extensions/helpers_extension.dart';
|
import '../../extensions/helpers_extension.dart';
|
||||||
import '../../retry.dart';
|
import '../../retry.dart';
|
||||||
import '../../videos/video_id.dart';
|
import '../../videos/video_id.dart';
|
||||||
import '../youtube_http_client.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 'player_response.dart';
|
||||||
import 'stream_info_provider.dart';
|
|
||||||
|
|
||||||
///
|
///
|
||||||
class WatchPage {
|
class WatchPage {
|
||||||
|
@ -37,13 +35,13 @@ class WatchPage {
|
||||||
|
|
||||||
///
|
///
|
||||||
_InitialData get initialData =>
|
_InitialData get initialData =>
|
||||||
_initialData ??= _InitialData(json.decode(_matchJson(_extractJson(
|
_initialData ??= _InitialData(WatchPageId.fromRawJson(_extractJson(
|
||||||
_root
|
_root
|
||||||
.querySelectorAll('script')
|
.querySelectorAll('script')
|
||||||
.map((e) => e.text)
|
.map((e) => e.text)
|
||||||
.toList()
|
.toList()
|
||||||
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
.firstWhere((e) => e.contains('window["ytInitialData"] =')),
|
||||||
'window["ytInitialData"] ='))));
|
'window["ytInitialData"] =')));
|
||||||
|
|
||||||
///
|
///
|
||||||
String get xsfrToken => _xsfrToken ??= _xsfrTokenExp
|
String get xsfrToken => _xsfrToken ??= _xsfrTokenExp
|
||||||
|
@ -88,9 +86,9 @@ class WatchPage {
|
||||||
|
|
||||||
///
|
///
|
||||||
_PlayerConfig get playerConfig =>
|
_PlayerConfig get playerConfig =>
|
||||||
_playerConfig ??= _PlayerConfig(json.decode(_matchJson(_extractJson(
|
_playerConfig ??= _PlayerConfig(PlayerConfigJson.fromRawJson(_extractJson(
|
||||||
_root.getElementsByTagName('html').first.text,
|
_root.getElementsByTagName('html').first.text,
|
||||||
'ytplayer.config = '))));
|
'ytplayer.config = ')));
|
||||||
|
|
||||||
String _extractJson(String html, String separator) {
|
String _extractJson(String html, String separator) {
|
||||||
return _matchJson(
|
return _matchJson(
|
||||||
|
@ -145,99 +143,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 {
|
class _PlayerConfig {
|
||||||
// Json parsed map
|
// 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 get playerResponse =>
|
||||||
PlayerResponse.parse(_root['args']['player_response']);
|
PlayerResponse.parse(root.args.playerResponse);
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InitialData {
|
class _InitialData {
|
||||||
// Json parsed map
|
// Json parsed map
|
||||||
final Map<String, dynamic> root;
|
final WatchPageId root;
|
||||||
|
|
||||||
_InitialData(this.root);
|
_InitialData(this.root);
|
||||||
|
|
||||||
|
@ -246,26 +166,21 @@ class _InitialData {
|
||||||
String _continuation;
|
String _continuation;
|
||||||
String _clickTrackingParams;
|
String _clickTrackingParams;
|
||||||
|
|
||||||
Map<String, dynamic> getContinuationContext(Map<String, dynamic> root) {
|
NextContinuationData getContinuationContext() {
|
||||||
if (root['contents'] != null) {
|
if (root.contents != null) {
|
||||||
return (root['contents']['twoColumnWatchNextResults']['results']
|
return root.contents.twoColumnWatchNextResults.results.results.contents
|
||||||
['results']['contents'] as List<dynamic>)
|
.firstWhere((e) => e.itemSectionRenderer != null)
|
||||||
?.firstWhere((e) => e.containsKey('itemSectionRenderer'))[
|
.itemSectionRenderer
|
||||||
'itemSectionRenderer']['continuations']
|
.continuations
|
||||||
?.first['nextContinuationData']
|
.first
|
||||||
?.cast<String, dynamic>();
|
.nextContinuationData;
|
||||||
}
|
|
||||||
if (root['response'] != null) {
|
|
||||||
return root['response']['itemSectionContinuation']['continuations']
|
|
||||||
?.first['nextContinuationData']
|
|
||||||
?.cast<String, dynamic>();
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get continuation => _continuation ??=
|
String get continuation =>
|
||||||
getContinuationContext(root)?.getValue('continuation') ?? '';
|
_continuation ??= getContinuationContext()?.continuation ?? '';
|
||||||
|
|
||||||
String get clickTrackingParams => _clickTrackingParams ??=
|
String get clickTrackingParams => _clickTrackingParams ??=
|
||||||
getContinuationContext(root)?.getValue('clickTrackingParams') ?? '';
|
getContinuationContext()?.clickTrackingParams ?? '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
///
|
||||||
|
abstract class BaseSearchContent {
|
||||||
|
///
|
||||||
|
const BaseSearchContent();
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
import '../videos/video_id.dart';
|
import '../videos/video_id.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
class RelatedQuery {
|
class RelatedQuery with EquatableMixin {
|
||||||
/// Query related to a search query.
|
/// Query related to a search query.
|
||||||
final String query;
|
final String query;
|
||||||
|
|
||||||
|
@ -10,4 +12,10 @@ class RelatedQuery {
|
||||||
|
|
||||||
/// Initialize a [RelatedQuery] instance.
|
/// Initialize a [RelatedQuery] instance.
|
||||||
RelatedQuery(this.query, this.videoId);
|
RelatedQuery(this.query, this.videoId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'RelatedQuery($videoId): $query';
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [query, videoId];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import 'package:youtube_explode_dart/src/reverse_engineering/responses/search_page.dart';
|
||||||
|
|
||||||
import '../common/common.dart';
|
import '../common/common.dart';
|
||||||
import '../reverse_engineering/responses/playlist_response.dart';
|
import '../reverse_engineering/responses/playlist_response.dart';
|
||||||
import '../reverse_engineering/youtube_http_client.dart';
|
import '../reverse_engineering/youtube_http_client.dart';
|
||||||
import '../videos/video.dart';
|
import '../videos/video.dart';
|
||||||
import '../videos/video_id.dart';
|
import '../videos/video_id.dart';
|
||||||
|
import 'base_search_content.dart';
|
||||||
import 'search_query.dart';
|
import 'search_query.dart';
|
||||||
|
|
||||||
/// YouTube search queries.
|
/// YouTube search queries.
|
||||||
|
@ -13,7 +16,8 @@ class SearchClient {
|
||||||
SearchClient(this._httpClient);
|
SearchClient(this._httpClient);
|
||||||
|
|
||||||
/// Enumerates videos returned by the specified search query.
|
/// 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>{};
|
var encounteredVideoIds = <String>{};
|
||||||
|
|
||||||
for (var page = 0; page < double.maxFinite; page++) {
|
for (var page = 0; page < double.maxFinite; page++) {
|
||||||
|
@ -49,7 +53,41 @@ 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.
|
/// Queries to YouTube to get the results.
|
||||||
|
@Deprecated('Use getVideosFromPage instead')
|
||||||
Future<SearchQuery> queryFromPage(String searchQuery) =>
|
Future<SearchQuery> queryFromPage(String searchQuery) =>
|
||||||
SearchQuery.search(_httpClient, searchQuery);
|
SearchQuery.search(_httpClient, searchQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
channelId = ChannelId.fromString(channelId);
|
||||||
|
var page = await ChannelUploadPage.get(
|
||||||
|
_httpClient, channelId.value, videoSorting.code);
|
||||||
|
yield* Stream.fromIterable(page.initialData.uploads);
|
||||||
|
|
||||||
|
// ignore: literal_only_boolean_expressions
|
||||||
|
while (true) {
|
||||||
|
page = await page.nextPage(_httpClient);
|
||||||
|
if (page == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
yield* Stream.fromIterable(page.initialData.uploads);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
import '../playlists/playlist_id.dart';
|
import '../playlists/playlist_id.dart';
|
||||||
|
import 'base_search_content.dart';
|
||||||
|
|
||||||
/// Metadata related to a search query result (playlist)
|
/// Metadata related to a search query result (playlist)
|
||||||
class SearchPlaylist with EquatableMixin {
|
class SearchPlaylist extends BaseSearchContent with EquatableMixin {
|
||||||
/// PlaylistId.
|
/// PlaylistId.
|
||||||
final PlaylistId playlistId;
|
final PlaylistId playlistId;
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ class SearchPlaylist with EquatableMixin {
|
||||||
SearchPlaylist(this.playlistId, this.playlistTitle, this.playlistVideoCount);
|
SearchPlaylist(this.playlistId, this.playlistTitle, this.playlistVideoCount);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '(Playlist) $playlistTitle ($playlistId)';
|
String toString() => '[Playlist] $playlistTitle ($playlistId)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [playlistId];
|
List<Object> get props => [playlistId];
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import '../videos/video_id.dart';
|
import '../videos/video_id.dart';
|
||||||
|
import 'base_search_content.dart';
|
||||||
|
|
||||||
/// Metadata related to a search query result (video).
|
/// Metadata related to a search query result (video).
|
||||||
class SearchVideo {
|
class SearchVideo extends BaseSearchContent {
|
||||||
/// VideoId.
|
/// VideoId.
|
||||||
final VideoId videoId;
|
final VideoId videoId;
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,8 @@ class CommentsClient {
|
||||||
/// the results.
|
/// the results.
|
||||||
///
|
///
|
||||||
/// The streams doesn't emit any data if [Video.hasWatchPage] is false.
|
/// The streams doesn't emit any data if [Video.hasWatchPage] is false.
|
||||||
|
/// Use `videos.get(videoId, forceWatchPage: true)` to assure that the
|
||||||
|
/// WatchPage is fetched.
|
||||||
Stream<Comment> getComments(Video video) async* {
|
Stream<Comment> getComments(Video video) async* {
|
||||||
if (video.watchPage == null) {
|
if (video.watchPage == null) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -104,10 +104,7 @@ class StreamsClient {
|
||||||
throw VideoUnplayableException.liveStream(videoId);
|
throw VideoUnplayableException.liveStream(videoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var streamInfoProviders = <StreamInfoProvider>[
|
var streamInfoProviders = <StreamInfoProvider>[...playerResponse.streams];
|
||||||
...playerConfig.streams,
|
|
||||||
...playerResponse.streams
|
|
||||||
];
|
|
||||||
|
|
||||||
var dashManifestUrl = playerResponse.dashManifestUrl;
|
var dashManifestUrl = playerResponse.dashManifestUrl;
|
||||||
if (!dashManifestUrl.isNullOrWhiteSpace) {
|
if (!dashManifestUrl.isNullOrWhiteSpace) {
|
||||||
|
|
|
@ -71,9 +71,13 @@ class VideoClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a [Video] instance from a [videoId]
|
/// 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);
|
videoId = VideoId.fromString(videoId);
|
||||||
|
|
||||||
|
if (forceWatchPage) {
|
||||||
|
return _getVideoFromWatchPage(videoId);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await _getVideoFromFixPlaylist(videoId);
|
return await _getVideoFromFixPlaylist(videoId);
|
||||||
} on YoutubeExplodeException {
|
} on YoutubeExplodeException {
|
||||||
|
|
|
@ -13,11 +13,11 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GetClosedCaptionTracksOfAnyVideo', () async {
|
test('GetClosedCaptionTracksOfAnyVideo', () async {
|
||||||
var manifest = await yt.videos.closedCaptions.getManifest('_QdPW8JrYzQ');
|
var manifest = await yt.videos.closedCaptions.getManifest('WOxr2dmLHLo');
|
||||||
expect(manifest.tracks, isNotEmpty);
|
expect(manifest.tracks, isNotEmpty);
|
||||||
});
|
});
|
||||||
test('GetClosedCaptionTrackOfAnyVideoSpecific', () async {
|
test('GetClosedCaptionTrackOfAnyVideoSpecific', () async {
|
||||||
var manifest = await yt.videos.closedCaptions.getManifest('_QdPW8JrYzQ');
|
var manifest = await yt.videos.closedCaptions.getManifest('WOxr2dmLHLo');
|
||||||
var trackInfo = manifest.tracks.first;
|
var trackInfo = manifest.tracks.first;
|
||||||
var track = await yt.videos.closedCaptions.get(trackInfo);
|
var track = await yt.videos.closedCaptions.get(trackInfo);
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ void main() {
|
||||||
});
|
});
|
||||||
test('GetClosedCaptionTrackAtSpecificTime', () async {
|
test('GetClosedCaptionTrackAtSpecificTime', () async {
|
||||||
var manifest = await yt.videos.closedCaptions
|
var manifest = await yt.videos.closedCaptions
|
||||||
.getManifest('https://www.youtube.com/watch?v=YltHGKX80Y8');
|
.getManifest('https://www.youtube.com/watch?v=ppJy5uGZLi4');
|
||||||
var trackInfo = manifest.getByLanguage('en');
|
var trackInfo = manifest.getByLanguage('en');
|
||||||
var track = await yt.videos.closedCaptions.get(trackInfo);
|
var track = await yt.videos.closedCaptions.get(trackInfo);
|
||||||
var caption =
|
var caption =
|
||||||
|
@ -35,8 +35,8 @@ void main() {
|
||||||
|
|
||||||
expect(caption, isNotNull);
|
expect(caption, isNotNull);
|
||||||
expect(captionPart, isNotNull);
|
expect(captionPart, isNotNull);
|
||||||
expect(caption.text, 'know I worked really hard on not doing');
|
expect(caption.text, 'how about this black there are some');
|
||||||
expect(captionPart.text, ' hard');
|
expect(captionPart.text, ' about');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ void main() {
|
||||||
|
|
||||||
test('GetCommentOfVideo', () async {
|
test('GetCommentOfVideo', () async {
|
||||||
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
|
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
|
||||||
var video = await yt.videos.get(VideoId(videoUrl));
|
var video = await yt.videos.get(VideoId(videoUrl), forceWatchPage: true);
|
||||||
var comments = await yt.videos.commentsClient.getComments(video).toList();
|
var comments = await yt.videos.commentsClient.getComments(video).toList();
|
||||||
expect(comments.length, greaterThanOrEqualTo(1));
|
expect(comments.length, greaterThanOrEqualTo(1));
|
||||||
}, skip: 'This may fail on some environments');
|
}, skip: 'This may fail on some environments');
|
||||||
|
|
|
@ -13,9 +13,8 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('SearchYouTubeVideosFromApi', () async {
|
test('SearchYouTubeVideosFromApi', () async {
|
||||||
var videos = await yt.search
|
var videos =
|
||||||
.getVideosAsync('undead corporation megalomania')
|
await yt.search.getVideos('undead corporation megalomania').toList();
|
||||||
.toList();
|
|
||||||
expect(videos, isNotEmpty);
|
expect(videos, isNotEmpty);
|
||||||
}, skip: 'Endpoint removed from YouTube');
|
}, skip: 'Endpoint removed from YouTube');
|
||||||
|
|
||||||
|
@ -36,5 +35,10 @@ void main() {
|
||||||
var nextPage = await query.nextPage();
|
var nextPage = await query.nextPage();
|
||||||
expect(nextPage, isNull);
|
expect(nextPage, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('SearchYouTubeVideosFromPageStream', () async {
|
||||||
|
var query = await yt.search.getVideosFromPage('hello').take(30).toList();
|
||||||
|
expect(query, hasLength(30));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue