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