parent
5d76402694
commit
13249aed18
|
@ -2,6 +2,7 @@ library youtube_explode.exceptions;
|
||||||
|
|
||||||
export 'fatal_failure_exception.dart';
|
export 'fatal_failure_exception.dart';
|
||||||
export 'request_limit_exceeded_exception.dart';
|
export 'request_limit_exceeded_exception.dart';
|
||||||
|
export 'search_item_section_exception.dart';
|
||||||
export 'transient_failure_exception.dart';
|
export 'transient_failure_exception.dart';
|
||||||
export 'video_requires_purchase_exception.dart';
|
export 'video_requires_purchase_exception.dart';
|
||||||
export 'video_unavailable_exception.dart';
|
export 'video_unavailable_exception.dart';
|
||||||
|
|
|
@ -3,8 +3,7 @@ import 'package:http/http.dart';
|
||||||
import 'youtube_explode_exception.dart';
|
import 'youtube_explode_exception.dart';
|
||||||
|
|
||||||
/// Exception thrown when a fatal failure occurs.
|
/// Exception thrown when a fatal failure occurs.
|
||||||
class FatalFailureException
|
class FatalFailureException implements YoutubeExplodeException {
|
||||||
implements YoutubeExplodeException {
|
|
||||||
/// Description message
|
/// Description message
|
||||||
@override
|
@override
|
||||||
final String message;
|
final String message;
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
//
|
||||||
|
|
||||||
|
import '../../youtube_explode_dart.dart';
|
||||||
|
|
||||||
|
/// Exception thrown when the Item Section is missing from a search request.
|
||||||
|
class SearchItemSectionException implements YoutubeExplodeException {
|
||||||
|
@override
|
||||||
|
// TODO: implement message
|
||||||
|
String get message => 'Failed to find the item section.';
|
||||||
|
}
|
|
@ -5,4 +5,4 @@ abstract class YoutubeExplodeException implements Exception {
|
||||||
|
|
||||||
///
|
///
|
||||||
YoutubeExplodeException(this.message);
|
YoutubeExplodeException(this.message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,9 @@ Future<T> retry<T>(FutureOr<T> Function() function) async {
|
||||||
|
|
||||||
/// Get "retry" cost of each YoutubeExplode exception.
|
/// Get "retry" cost of each YoutubeExplode exception.
|
||||||
int getExceptionCost(Exception e) {
|
int getExceptionCost(Exception e) {
|
||||||
if (e is TransientFailureException || e is FormatException) {
|
if (e is TransientFailureException ||
|
||||||
|
e is FormatException ||
|
||||||
|
e is SearchItemSectionException) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (e is RequestLimitExceededException) {
|
if (e is RequestLimitExceededException) {
|
||||||
|
|
|
@ -2,11 +2,11 @@ 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/reverse_engineering/responses/player_config_base.dart';
|
|
||||||
|
|
||||||
import '../../extensions/helpers_extension.dart';
|
import '../../extensions/helpers_extension.dart';
|
||||||
import '../../retry.dart';
|
import '../../retry.dart';
|
||||||
import '../youtube_http_client.dart';
|
import '../youtube_http_client.dart';
|
||||||
|
import 'player_config_base.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
class EmbedPage {
|
class EmbedPage {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/// Base class for PlayerConfig.
|
/// Base class for PlayerConfig.
|
||||||
abstract class PlayerConfigBase<T> {
|
abstract class PlayerConfigBase<T> {
|
||||||
|
|
||||||
/// Root node.
|
/// Root node.
|
||||||
final T root;
|
final T root;
|
||||||
|
|
||||||
|
@ -9,4 +8,4 @@ abstract class PlayerConfigBase<T> {
|
||||||
|
|
||||||
/// Player source url.
|
/// Player source url.
|
||||||
String get sourceUrl;
|
String get sourceUrl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,20 +155,22 @@ class _InitialData {
|
||||||
|
|
||||||
_InitialData(this.root);
|
_InitialData(this.root);
|
||||||
|
|
||||||
/* Cache results */
|
|
||||||
|
|
||||||
List<dynamic> _searchContent;
|
|
||||||
List<dynamic> _relatedVideos;
|
|
||||||
List<RelatedQuery> _relatedQueries;
|
|
||||||
|
|
||||||
List<PurpleContent> getContentContext() {
|
List<PurpleContent> getContentContext() {
|
||||||
if (root.contents != null) {
|
if (root.contents != null) {
|
||||||
return root.contents.twoColumnSearchResultsRenderer.primaryContents
|
return root.contents.twoColumnSearchResultsRenderer.primaryContents
|
||||||
.sectionListRenderer.contents.first.itemSectionRenderer.contents;
|
.sectionListRenderer.contents.first.itemSectionRenderer.contents;
|
||||||
}
|
}
|
||||||
if (root.onResponseReceivedCommands != null) {
|
if (root.onResponseReceivedCommands != null) {
|
||||||
return root.onResponseReceivedCommands.first.appendContinuationItemsAction
|
final itemSection = root
|
||||||
.continuationItems[0].itemSectionRenderer.contents;
|
.onResponseReceivedCommands
|
||||||
|
.first
|
||||||
|
.appendContinuationItemsAction
|
||||||
|
.continuationItems[0]
|
||||||
|
.itemSectionRenderer;
|
||||||
|
if (itemSection == null) {
|
||||||
|
throw SearchItemSectionException();
|
||||||
|
}
|
||||||
|
return itemSection.contents;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -203,11 +205,11 @@ class _InitialData {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains only [SearchVideo] or [SearchPlaylist]
|
// Contains only [SearchVideo] or [SearchPlaylist]
|
||||||
List<BaseSearchContent> get searchContent => _searchContent ??=
|
List<BaseSearchContent> get searchContent =>
|
||||||
getContentContext().map(_parseContent).where((e) => e != null).toList();
|
getContentContext().map(_parseContent).where((e) => e != null).toList();
|
||||||
|
|
||||||
List<RelatedQuery> get relatedQueries =>
|
List<RelatedQuery> get relatedQueries =>
|
||||||
(_relatedQueries ??= getContentContext()
|
getContentContext()
|
||||||
?.where((e) => e.horizontalCardListRenderer != null)
|
?.where((e) => e.horizontalCardListRenderer != null)
|
||||||
?.map((e) => e.horizontalCardListRenderer.cards)
|
?.map((e) => e.horizontalCardListRenderer.cards)
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
|
@ -217,16 +219,16 @@ class _InitialData {
|
||||||
VideoId(
|
VideoId(
|
||||||
Uri.parse(e.thumbnail.thumbnails.first.url).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()
|
getContentContext()
|
||||||
?.where((e) => e.shelfRenderer != null)
|
?.where((e) => e.shelfRenderer != null)
|
||||||
?.map((e) => e.shelfRenderer.content.verticalListRenderer.items)
|
?.map((e) => e.shelfRenderer.content.verticalListRenderer.items)
|
||||||
?.firstOrNull
|
?.firstOrNull
|
||||||
?.map(_parseContent)
|
?.map(_parseContent)
|
||||||
?.toList()) ??
|
?.toList() ??
|
||||||
const [];
|
const [];
|
||||||
|
|
||||||
String get continuationToken => _getContinuationToken();
|
String get continuationToken => _getContinuationToken();
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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/reverse_engineering/responses/player_config_base.dart';
|
|
||||||
|
|
||||||
import '../../../youtube_explode_dart.dart';
|
import '../../../youtube_explode_dart.dart';
|
||||||
import '../../extensions/helpers_extension.dart';
|
import '../../extensions/helpers_extension.dart';
|
||||||
|
@ -9,6 +8,7 @@ import '../../videos/video_id.dart';
|
||||||
import '../youtube_http_client.dart';
|
import '../youtube_http_client.dart';
|
||||||
import 'generated/player_response_json.g.dart';
|
import 'generated/player_response_json.g.dart';
|
||||||
import 'generated/watch_page_id.g.dart';
|
import 'generated/watch_page_id.g.dart';
|
||||||
|
import 'player_config_base.dart';
|
||||||
import 'player_response.dart';
|
import 'player_response.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
|
@ -143,7 +143,7 @@ class YoutubeHttpClient extends http.BaseClient {
|
||||||
request.headers[key] = _defaultHeaders[key];
|
request.headers[key] = _defaultHeaders[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// print('Request: $request');
|
print('Request: $request');
|
||||||
// print('Stack:\n${StackTrace.current}');
|
// print('Stack:\n${StackTrace.current}');
|
||||||
return _httpClient.send(request);
|
return _httpClient.send(request);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import '../common/common.dart';
|
import '../../youtube_explode_dart.dart';
|
||||||
import '../reverse_engineering/responses/playlist_response.dart';
|
import '../retry.dart';
|
||||||
import '../reverse_engineering/responses/search_page.dart';
|
import '../reverse_engineering/responses/search_page.dart';
|
||||||
import '../reverse_engineering/youtube_http_client.dart';
|
import '../reverse_engineering/youtube_http_client.dart';
|
||||||
import '../videos/video.dart';
|
|
||||||
import '../videos/video_id.dart';
|
|
||||||
import 'base_search_content.dart';
|
import 'base_search_content.dart';
|
||||||
import 'search_query.dart';
|
import 'search_query.dart';
|
||||||
|
import 'search_list.dart';
|
||||||
|
|
||||||
/// YouTube search queries.
|
/// YouTube search queries.
|
||||||
class SearchClient {
|
class SearchClient {
|
||||||
|
@ -14,50 +13,29 @@ class SearchClient {
|
||||||
/// Initializes an instance of [SearchClient]
|
/// Initializes an instance of [SearchClient]
|
||||||
SearchClient(this._httpClient);
|
SearchClient(this._httpClient);
|
||||||
|
|
||||||
/// Enumerates videos returned by the specified search query.
|
/// Enumerates videos returned by the specified search query
|
||||||
/// (from the YouTube Embedded API)
|
/// (from the video search page).
|
||||||
Stream<Video> getVideos(String searchQuery) async* {
|
/// The videos are sent in batch of 20 videos.
|
||||||
var encounteredVideoIds = <String>{};
|
/// You [SearchList.nextPage] to get the next batch of videos.
|
||||||
|
Future<SearchList> getVideos(String searchQuery) {
|
||||||
for (var page = 0; page < double.maxFinite; page++) {
|
var stream =
|
||||||
var response =
|
getVideosFromPage(searchQuery, onlyVideos: true).cast<SearchVideo>();
|
||||||
await PlaylistResponse.searchResults(_httpClient, searchQuery);
|
return SearchList.create(stream);
|
||||||
|
|
||||||
var countDelta = 0;
|
|
||||||
for (var video in response.videos) {
|
|
||||||
var videoId = video.id;
|
|
||||||
|
|
||||||
if (!encounteredVideoIds.add(videoId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield Video(
|
|
||||||
VideoId(videoId),
|
|
||||||
video.title,
|
|
||||||
video.author,
|
|
||||||
video.channelId,
|
|
||||||
video.uploadDate,
|
|
||||||
video.description,
|
|
||||||
video.duration,
|
|
||||||
ThumbnailSet(videoId),
|
|
||||||
video.keywords,
|
|
||||||
Engagement(video.viewCount, video.likes, video.dislikes));
|
|
||||||
countDelta++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Videos loop around, so break when we stop seeing new videos
|
|
||||||
if (countDelta <= 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerates videos returned by the specified search query
|
/// Enumerates videos returned by the specified search query
|
||||||
/// (from the video search page).
|
/// (from the video search page).
|
||||||
/// Contains only instances of [SearchVideo] or [SearchPlaylist]
|
/// Contains only instances of [SearchVideo] or [SearchPlaylist]
|
||||||
Stream<BaseSearchContent> getVideosFromPage(String searchQuery) async* {
|
Stream<BaseSearchContent> getVideosFromPage(String searchQuery,
|
||||||
var page = await SearchPage.get(_httpClient, searchQuery);
|
{bool onlyVideos = true}) async* {
|
||||||
yield* Stream.fromIterable(page.initialData.searchContent);
|
var page =
|
||||||
|
await retry(() async => SearchPage.get(_httpClient, searchQuery));
|
||||||
|
if (onlyVideos) {
|
||||||
|
yield* Stream.fromIterable(
|
||||||
|
page.initialData.searchContent.whereType<SearchVideo>());
|
||||||
|
} else {
|
||||||
|
yield* Stream.fromIterable(page.initialData.searchContent);
|
||||||
|
}
|
||||||
|
|
||||||
// ignore: literal_only_boolean_expressions
|
// ignore: literal_only_boolean_expressions
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -65,7 +43,13 @@ class SearchClient {
|
||||||
if (page == null) {
|
if (page == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
yield* Stream.fromIterable(page.initialData.searchContent);
|
|
||||||
|
if (onlyVideos) {
|
||||||
|
yield* Stream.fromIterable(
|
||||||
|
page.initialData.searchContent.whereType<SearchVideo>());
|
||||||
|
} else {
|
||||||
|
yield* Stream.fromIterable(page.initialData.searchContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import '../../youtube_explode_dart.dart';
|
||||||
|
|
||||||
|
///
|
||||||
|
class SearchList extends DelegatingList<SearchVideo> {
|
||||||
|
final Stream<SearchVideo> _stream;
|
||||||
|
|
||||||
|
///
|
||||||
|
SearchList._(List<SearchVideo> base, this._stream) : super(base);
|
||||||
|
|
||||||
|
///
|
||||||
|
static Future<SearchList> create(Stream<SearchVideo> stream) async {
|
||||||
|
Stream<SearchVideo> broadcast;
|
||||||
|
broadcast = stream.asBroadcastStream(onCancel: (subscription) {
|
||||||
|
print('Pause');
|
||||||
|
subscription.pause();
|
||||||
|
}, onListen: (subscription) {
|
||||||
|
print('Resume');
|
||||||
|
subscription.resume();
|
||||||
|
});
|
||||||
|
final base = await broadcast.take(20).toList();
|
||||||
|
return SearchList._(base, broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
Future<SearchList> nextPage() async {
|
||||||
|
final base = await _stream.take(20).toList();
|
||||||
|
return SearchList._(base, _stream);
|
||||||
|
}
|
||||||
|
}
|
|
@ -97,8 +97,7 @@ class StreamsClient {
|
||||||
videoId, VideoId(previewVideoId));
|
videoId, VideoId(previewVideoId));
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerSourceUrl =
|
var playerSourceUrl = watchPage.sourceUrl ?? playerConfig?.sourceUrl;
|
||||||
watchPage.sourceUrl ?? playerConfig?.sourceUrl;
|
|
||||||
var playerSource = !playerSourceUrl.isNullOrWhiteSpace
|
var playerSource = !playerSourceUrl.isNullOrWhiteSpace
|
||||||
? await PlayerSource.get(_httpClient, playerSourceUrl)
|
? await PlayerSource.get(_httpClient, playerSourceUrl)
|
||||||
: null;
|
: null;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import '../common/common.dart';
|
||||||
import '../reverse_engineering/responses/responses.dart';
|
import '../reverse_engineering/responses/responses.dart';
|
||||||
import 'video_id.dart';
|
import 'video_id.dart';
|
||||||
|
|
||||||
|
@Deprecated('This class is not used anymore - Since youtube changes in 02/2021')
|
||||||
/// YouTube video metadata.
|
/// YouTube video metadata.
|
||||||
class Video with EquatableMixin {
|
class Video with EquatableMixin {
|
||||||
/// Video ID.
|
/// Video ID.
|
||||||
|
|
|
@ -14,6 +14,7 @@ dependencies:
|
||||||
equatable: ^1.1.0
|
equatable: ^1.1.0
|
||||||
meta: ^1.1.8
|
meta: ^1.1.8
|
||||||
json_annotation: ^3.1.0
|
json_annotation: ^3.1.0
|
||||||
|
collection: ^1.14.13
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
effective_dart: ^1.2.4
|
effective_dart: ^1.2.4
|
||||||
|
|
|
@ -13,7 +13,7 @@ void main() {
|
||||||
|
|
||||||
test('Search a youtube video from the api', () async {
|
test('Search a youtube video from the api', () async {
|
||||||
var videos =
|
var videos =
|
||||||
await yt.search.getVideos('undead corporation megalomania').toList();
|
await yt.search.getVideos('undead corporation megalomania');
|
||||||
expect(videos, isNotEmpty);
|
expect(videos, isNotEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue