Fix search issues.

Fix `NoSuchMethodError` exception.

Closes #102
This commit is contained in:
Mattia 2021-02-26 16:08:48 +01:00
parent 5d76402694
commit 13249aed18
16 changed files with 100 additions and 69 deletions

View File

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

View File

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

View File

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

View File

@ -5,4 +5,4 @@ abstract class YoutubeExplodeException implements Exception {
///
YoutubeExplodeException(this.message);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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