Rework of `SearchClient`, introduces breaking changes.

Fix #197.
This commit is contained in:
Mattia 2022-04-13 23:19:09 +02:00
parent 0ce24bf017
commit e231b595a8
19 changed files with 182 additions and 169 deletions

View File

@ -1,3 +1,13 @@
## 1.11.0
- BREAKING CHANGE: Removed `SearchClient.getVideosFromPage`, use `SearchClient.search` or `SearchClient.search.search`.
- BREAKING CHANGE: `SearchClient.search` now returns `VideoSearchList` (List<Video>).
- BREAKING CHANGE: Remove the `filter` variable, now use `SearchFilter`.
- To get the filters use static access on `FeatureFilters`, `UploadDateFilter`, `TypesFilter`, `DurationFilters`, `SortFilters`.
- Introduced `SearchClient.searchContent` to search for videos, channels and playlists.
- Introduced `SearchClient.searchRaw` to manually parse the content and also get related videos and estimated results.
- Fix #197: Fixed `withHighestBitrate()`.
- Introduced: `List<VideoStreamInfo>.bestQuality`.
## 1.10.10+2 ## 1.10.10+2
- Fix #194: Now closed-captions allow malformed utf8 as well. - Fix #194: Now closed-captions allow malformed utf8 as well.

View File

@ -5,7 +5,6 @@ import '../../../youtube_explode_dart.dart';
import '../../extensions/helpers_extension.dart'; import '../../extensions/helpers_extension.dart';
import '../../retry.dart'; import '../../retry.dart';
import '../../search/base_search_content.dart'; import '../../search/base_search_content.dart';
import '../../search/search_channel.dart';
import '../models/initial_data.dart'; import '../models/initial_data.dart';
import '../models/youtube_page.dart'; import '../models/youtube_page.dart';

View File

@ -1,2 +1,6 @@
/// This can either be a [SearchVideo] or [SearchPlaylist] import 'search_channel.dart';
import 'search_playlist.dart';
import 'search_video.dart';
/// This can either be a [SearchVideo], [SearchPlaylist], [SearchChannel]
mixin BaseSearchContent {} mixin BaseSearchContent {}

View File

@ -3,6 +3,7 @@
/// {@category Search} /// {@category Search}
library youtube_explode.search; library youtube_explode.search;
export 'search_channel.dart';
export 'search_client.dart'; export 'search_client.dart';
export 'search_filter.dart'; export 'search_filter.dart';
export 'search_list.dart'; export 'search_list.dart';

View File

@ -2,9 +2,7 @@ import 'dart:convert';
import '../../youtube_explode_dart.dart'; import '../../youtube_explode_dart.dart';
import '../extensions/helpers_extension.dart'; import '../extensions/helpers_extension.dart';
import '../retry.dart';
import '../reverse_engineering/pages/search_page.dart'; import '../reverse_engineering/pages/search_page.dart';
import 'base_search_content.dart';
/// YouTube search queries. /// YouTube search queries.
class SearchClient { class SearchClient {
@ -16,12 +14,12 @@ class SearchClient {
/// 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).
/// The videos are sent in batch of 20 videos. /// The videos are sent in batch of 20 videos.
/// You [SearchList.nextPage] to get the next batch of videos. /// You [VideoSearchList.nextPage] to get the next batch of videos.
Future<SearchList> getVideos(String searchQuery, Future<VideoSearchList> search(String searchQuery,
{SearchFilter filter = const SearchFilter('')}) async { {SearchFilter filter = TypeFilters.video}) async {
final page = await SearchPage.get(_httpClient, searchQuery, filter: filter); final page = await SearchPage.get(_httpClient, searchQuery, filter: filter);
return SearchList( return VideoSearchList(
page.searchContent page.searchContent
.whereType<SearchVideo>() .whereType<SearchVideo>()
.map((e) => Video( .map((e) => Video(
@ -42,38 +40,31 @@ class SearchClient {
_httpClient); _httpClient);
} }
/// Enumerates videos returned by the specified search query @Deprecated('Use SearchClient.search')
/// (from the video search page). Future<VideoSearchList> getVideos(String searchQuery,
/// Contains only instances of [SearchVideo] or [SearchPlaylist] {SearchFilter filter = TypeFilters.video}) =>
@Deprecated( search(searchQuery, filter: filter);
'Since version 1.9.0 this is the same as [SearchClient.getVideos].')
Stream<BaseSearchContent> getVideosFromPage(String searchQuery,
{bool onlyVideos = true,
SearchFilter filter = const SearchFilter('')}) async* {
SearchPage? page;
// ignore: literal_only_boolean_expressions
for (;;) {
if (page == null) {
page = await retry(
_httpClient,
() async =>
SearchPage.get(_httpClient, searchQuery, filter: filter));
} else {
page = await page.nextPage(_httpClient);
if (page == null) {
return;
}
}
if (onlyVideos) { /// Enumerates results returned by the specified search query.
yield* Stream.fromIterable( /// The contents are sent in batch of 20 elements.
page!.searchContent.whereType<SearchVideo>()); /// The list can either contain a [SearchVideo], [SearchPlaylist] or a [SearchChannel].
} else { /// You [SearchList.nextPage] to get the next batch of content.
yield* Stream.fromIterable(page!.searchContent); Future<SearchList> searchContent(String searchQuery,
} {SearchFilter filter = const SearchFilter('')}) async {
} final page = await SearchPage.get(_httpClient, searchQuery, filter: filter);
return SearchList(page.searchContent, page, _httpClient);
} }
/// Enumerates results returned by the specified search query.
/// The contents are sent in batch of 20 elements.
/// The list can either contain a [SearchVideo], [SearchPlaylist] or a [SearchChannel].
/// You [SearchList.nextPage] to get the next batch of content.
/// Same as [SearchClient.search]
Future<VideoSearchList> call(String searchQuery,
{SearchFilter filter = const SearchFilter('')}) async =>
search(searchQuery, filter: filter);
/// Returns the suggestions youtube provide while search on the page. /// Returns the suggestions youtube provide while search on the page.
Future<List<String>> getQuerySuggestions(String query) async { Future<List<String>> getQuerySuggestions(String query) async {
final request = await _httpClient.get( final request = await _httpClient.get(
@ -87,24 +78,14 @@ class SearchClient {
} }
/// Queries to YouTube to get the results. /// Queries to YouTube to get the results.
@Deprecated('Use getVideosFromPage instead - ' /// You need to manually read [SearchQuery.content] and/or [SearchQuery.relatedVideos].
'Should be used only to get related videos') /// For most cases [SearchClient.search] is enough.
Future<SearchQuery> queryFromPage(String searchQuery) => Future<SearchQuery> searchRaw(String searchQuery,
SearchQuery.search(_httpClient, searchQuery); {SearchFilter filter = const SearchFilter('')}) =>
SearchQuery.search(_httpClient, searchQuery, filter: filter);
@Deprecated('Use searchRaw')
Future<SearchQuery> queryFromPage(String searchQuery,
{SearchFilter filter = const SearchFilter('')}) =>
searchRaw(searchQuery, filter: filter);
} }
/*
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,5 +1,3 @@
import 'search_client.dart';
class SearchFilter { class SearchFilter {
/// The value fo the 'sp' argument. /// The value fo the 'sp' argument.
final String value; final String value;
@ -7,126 +5,103 @@ class SearchFilter {
const SearchFilter(this.value); const SearchFilter(this.value);
} }
/// Video filters to be used with [SearchClient.getVideos]
class Filters {
const Filters._();
/// Features filters.
FeatureFilters get features => const FeatureFilters._();
/// Upload date filters.
UploadDateFilter get uploadDate => const UploadDateFilter._();
/// Types filters.
TypeFilters get types => const TypeFilters._();
/// Duration filters.
DurationFilters get duration => const DurationFilters._();
/// Videos sorting.
SortFilters get sort => const SortFilters._();
}
/// Video filters to be used with [SearchClient.getVideos]
const filters = Filters._();
class FeatureFilters { class FeatureFilters {
const FeatureFilters._(); const FeatureFilters._();
/// Live video. /// Live video.
SearchFilter get live => const SearchFilter('EgJAAQ%253D%253D'); static const SearchFilter live = SearchFilter('EgJAAQ%253D%253D');
/// 4K video. /// 4K video.
SearchFilter get v4k => const SearchFilter('EgJwAQ%253D%253D'); static const SearchFilter v4k = SearchFilter('EgJwAQ%253D%253D');
/// HD video. /// HD video.
SearchFilter get hd => const SearchFilter('EgIgAQ%253D%253D'); static const SearchFilter hd = SearchFilter('EgIgAQ%253D%253D');
/// Subtitled video. /// Subtitled video.
SearchFilter get subTitles => const SearchFilter('EgIoAQ%253D%253D'); static const SearchFilter subTitles = SearchFilter('EgIoAQ%253D%253D');
/// Creative comments video. /// Creative comments video.
SearchFilter get creativeCommons => const SearchFilter('EgIwAQ%253D%253D'); static const SearchFilter creativeCommons = SearchFilter('EgIwAQ%253D%253D');
/// 360° video. /// 360° video.
SearchFilter get v360 => const SearchFilter('EgJ4AQ%253D%253D'); static const SearchFilter v360 = SearchFilter('EgJ4AQ%253D%253D');
/// VR 180° video. /// VR 180° video.
SearchFilter get vr180 => const SearchFilter('EgPQAQE%253D'); static const SearchFilter vr180 = SearchFilter('EgPQAQE%253D');
/// 3D video. /// 3D video.
SearchFilter get v3D => const SearchFilter('EgI4AQ%253D%253D'); static const SearchFilter v3D = SearchFilter('EgI4AQ%253D%253D');
/// HDR video. /// HDR video.
SearchFilter get hdr => const SearchFilter('EgPIAQE%253D'); static const SearchFilter hdr = SearchFilter('EgPIAQE%253D');
/// Video with location. /// Video with location.
SearchFilter get location => const SearchFilter('EgO4AQE%253D'); static const SearchFilter location = SearchFilter('EgO4AQE%253D');
/// Purchased video. /// Purchased video.
SearchFilter get purchased => const SearchFilter('EgJIAQ%253D%253D'); static const SearchFilter purchased = SearchFilter('EgJIAQ%253D%253D');
} }
class UploadDateFilter { class UploadDateFilter {
const UploadDateFilter._(); const UploadDateFilter._();
/// Videos uploaded in the last hour. /// Videos uploaded in the last hour.
SearchFilter get lastHour => const SearchFilter('EgIIAQ%253D%253D'); static const SearchFilter lastHour = SearchFilter('EgIIAQ%253D%253D');
/// Videos uploaded today. /// Videos uploaded today.
SearchFilter get today => const SearchFilter('EgIIAg%253D%253D'); static const SearchFilter today = SearchFilter('EgIIAg%253D%253D');
/// Videos uploaded in the last week. /// Videos uploaded in the last week.
SearchFilter get lastWeek => const SearchFilter('EgIIAw%253D%253D'); static const SearchFilter lastWeek = SearchFilter('EgIIAw%253D%253D');
/// Videos uploaded in the last month. /// Videos uploaded in the last month.
SearchFilter get lastMonth => const SearchFilter('EgIIBA%253D%253D'); static const SearchFilter lastMonth = SearchFilter('EgIIBA%253D%253D');
/// Videos uploaded in the last year. /// Videos uploaded in the last year.
SearchFilter get lastYear => const SearchFilter('EgIIBQ%253D%253D'); static const SearchFilter lastYear = SearchFilter('EgIIBQ%253D%253D');
} }
class TypeFilters { class TypeFilters {
const TypeFilters._(); const TypeFilters._();
/// Videos. /// Videos.
SearchFilter get video => const SearchFilter('EgIQAQ%253D%253D'); static const SearchFilter video = SearchFilter('EgIQAQ%253D%253D');
/// Channels. /// Channels.
SearchFilter get channel => const SearchFilter('EgIQAg%253D%253D'); static const SearchFilter channel = SearchFilter('EgIQAg%253D%253D');
/// Playlists. /// Playlists.
SearchFilter get playlist => const SearchFilter('EgIQAw%253D%253D'); static const SearchFilter playlist = SearchFilter('EgIQAw%253D%253D');
/// Movies. /// Movies.
SearchFilter get movie => const SearchFilter('EgIQBA%253D%253D'); static const SearchFilter movie = SearchFilter('EgIQBA%253D%253D');
/// Shows. /// Shows.
SearchFilter get show => const SearchFilter('EgIQBQ%253D%253D'); static const SearchFilter show = SearchFilter('EgIQBQ%253D%253D');
} }
class DurationFilters { class DurationFilters {
const DurationFilters._(); const DurationFilters._();
/// Short videos, < 4 minutes. /// Short videos, < 4 minutes.
SearchFilter get short => const SearchFilter('EgIYAQ%253D%253D'); static const SearchFilter short = SearchFilter('EgIYAQ%253D%253D');
/// Long videos, > 20 minutes. /// Long videos, > 20 minutes.
SearchFilter get long => const SearchFilter('EgIYAg%253D%253D'); static const SearchFilter long = SearchFilter('EgIYAg%253D%253D');
} }
class SortFilters { class SortFilters {
const SortFilters._(); const SortFilters._();
/// Sort by relevance (default). /// Sort by relevance (default).
SearchFilter get relevance => const SearchFilter('CAASAhAB'); static const SearchFilter relevance = SearchFilter('CAASAhAB');
/// Sort by upload date (default). /// Sort by upload date (default).
SearchFilter get uploadDate => const SearchFilter('CAI%253D'); static const SearchFilter uploadDate = SearchFilter('CAI%253D');
/// Sort by view count (default). /// Sort by view count (default).
SearchFilter get viewCount => const SearchFilter('CAM%253D'); static const SearchFilter viewCount = SearchFilter('CAM%253D');
/// Sort by rating (default). /// Sort by rating (default).
SearchFilter get rating => const SearchFilter('CAE%253D'); static const SearchFilter rating = SearchFilter('CAE%253D');
} }

View File

@ -5,16 +5,17 @@ import 'package:collection/collection.dart';
import '../../youtube_explode_dart.dart'; import '../../youtube_explode_dart.dart';
import '../extensions/helpers_extension.dart'; import '../extensions/helpers_extension.dart';
import '../reverse_engineering/pages/search_page.dart'; import '../reverse_engineering/pages/search_page.dart';
import 'base_search_content.dart';
/// This list contains search videos. /// This list contains search videos.
///This behaves like a [List] but has the [SearchList.nextPage] to get the next batch of videos. ///This behaves like a [List] but has the [SearchList.nextPage] to get the next batch of videos.
class SearchList extends DelegatingList<Video> { class SearchList<T extends BaseSearchContent> extends DelegatingList<T> {
final SearchPage _page; final SearchPage _page;
final YoutubeHttpClient _httpClient; final YoutubeHttpClient _httpClient;
/// Construct an instance of [SearchList] /// Construct an instance of [SearchList]
/// See [SearchList] /// See [SearchList]
SearchList(List<Video> base, this._page, this._httpClient) : super(base); SearchList(List<T> base, this._page, this._httpClient) : super(base);
/// Fetches the next batch of videos or returns null if there are no more /// Fetches the next batch of videos or returns null if there are no more
/// results. /// results.
@ -23,7 +24,28 @@ class SearchList extends DelegatingList<Video> {
if (page == null) { if (page == null) {
return null; return null;
} }
return SearchList(
return SearchList<T>(page.searchContent as List<T>, page, _httpClient);
}
}
class VideoSearchList extends DelegatingList<Video> {
final SearchPage _page;
final YoutubeHttpClient _httpClient;
/// Construct an instance of [SearchList]
/// See [SearchList]
VideoSearchList(List<Video> base, this._page, this._httpClient) : super(base);
/// Fetches the next batch of videos or returns null if there are no more
/// results.
Future<VideoSearchList?> nextPage() async {
final page = await _page.nextPage(_httpClient);
if (page == null) {
return null;
}
return VideoSearchList(
page.searchContent page.searchContent
.whereType<SearchVideo>() .whereType<SearchVideo>()
.map((e) => Video( .map((e) => Video(

View File

@ -1,5 +1,5 @@
import '../../youtube_explode_dart.dart';
import '../reverse_engineering/pages/search_page.dart'; import '../reverse_engineering/pages/search_page.dart';
import '../reverse_engineering/youtube_http_client.dart';
/// ///
class SearchQuery { class SearchQuery {
@ -15,8 +15,9 @@ class SearchQuery {
/// Search a video. /// Search a video.
static Future<SearchQuery> search( static Future<SearchQuery> search(
YoutubeHttpClient httpClient, String searchQuery) async { YoutubeHttpClient httpClient, String searchQuery,
var page = await SearchPage.get(httpClient, searchQuery); {SearchFilter filter = const SearchFilter('')}) async {
var page = await SearchPage.get(httpClient, searchQuery, filter: filter);
return SearchQuery(httpClient, searchQuery, page); return SearchQuery(httpClient, searchQuery, page);
} }
@ -31,7 +32,7 @@ class SearchQuery {
} }
/// Content of this search. /// Content of this search.
/// Contains either [SearchVideo] or [SearchPlaylist] /// Contains either [SearchVideo], [SearchPlaylist] or [SearchChannel]
List<dynamic> get content => _page.searchContent; List<dynamic> get content => _page.searchContent;
/// Videos related to this search. /// Videos related to this search.

View File

@ -29,7 +29,7 @@ class AudioOnlyStreamInfo with StreamInfo, AudioStreamInfo {
final String audioCodec; final String audioCodec;
@override @override
@JsonKey(toJson: mediaTypeTojson, fromJson: mediaTypeFromJson) @JsonKey(toJson: mediaTypeToJson, fromJson: mediaTypeFromJson)
final MediaType codec; final MediaType codec;
@override @override

View File

@ -30,7 +30,7 @@ Map<String, dynamic> _$AudioOnlyStreamInfoToJson(
'size': instance.size, 'size': instance.size,
'bitrate': instance.bitrate, 'bitrate': instance.bitrate,
'audioCodec': instance.audioCodec, 'audioCodec': instance.audioCodec,
'codec': mediaTypeTojson(instance.codec), 'codec': mediaTypeToJson(instance.codec),
'fragments': instance.fragments, 'fragments': instance.fragments,
'qualityLabel': instance.qualityLabel, 'qualityLabel': instance.qualityLabel,
}; };

View File

@ -61,7 +61,7 @@ class MuxedStreamInfo with StreamInfo, AudioStreamInfo, VideoStreamInfo {
/// Stream codec. /// Stream codec.
@override @override
@JsonKey(toJson: mediaTypeTojson, fromJson: mediaTypeFromJson) @JsonKey(toJson: mediaTypeToJson, fromJson: mediaTypeFromJson)
final MediaType codec; final MediaType codec;
/// Stream codec. /// Stream codec.
@ -85,7 +85,8 @@ class MuxedStreamInfo with StreamInfo, AudioStreamInfo, VideoStreamInfo {
); );
@override @override
String toString() => 'Muxed ($tag | $qualityLabel | $container)'; String toString() =>
'Muxed ($tag | ${videoResolution}p${framerate.framesPerSecond} | $container)';
factory MuxedStreamInfo.fromJson(Map<String, dynamic> json) => factory MuxedStreamInfo.fromJson(Map<String, dynamic> json) =>
_$MuxedStreamInfoFromJson(json); _$MuxedStreamInfoFromJson(json);

View File

@ -34,7 +34,7 @@ Map<String, dynamic> _$MuxedStreamInfoToJson(MuxedStreamInfo instance) =>
'videoQuality': _$VideoQualityEnumMap[instance.videoQuality], 'videoQuality': _$VideoQualityEnumMap[instance.videoQuality],
'videoResolution': instance.videoResolution, 'videoResolution': instance.videoResolution,
'framerate': instance.framerate, 'framerate': instance.framerate,
'codec': mediaTypeTojson(instance.codec), 'codec': mediaTypeToJson(instance.codec),
'qualityLabel': instance.qualityLabel, 'qualityLabel': instance.qualityLabel,
}; };

View File

@ -37,12 +37,12 @@ mixin StreamInfo {
/// Extension for Iterables of StreamInfo. /// Extension for Iterables of StreamInfo.
extension StreamInfoIterableExt<T extends StreamInfo> on Iterable<T> { extension StreamInfoIterableExt<T extends StreamInfo> on Iterable<T> {
/// Gets the stream with highest bitrate. /// Gets the stream with highest bitrate.
T withHighestBitrate() => sortByBitrate().last; T withHighestBitrate() => sortByBitrate().first;
/// Gets the video streams sorted by bitrate in ascending order. /// Gets the video streams sorted by bitrate in ascending order.
/// This returns new list without editing the original list. /// This returns new list without editing the original list.
List<T> sortByBitrate() => List<T> sortByBitrate() =>
toList()..sort((a, b) => a.bitrate.compareTo(b.bitrate)); toList()..sort((a, b) => b.bitrate.compareTo(a.bitrate));
/// Print a formatted text of all the streams. Like youtube-dl -F option. /// Print a formatted text of all the streams. Like youtube-dl -F option.
String describe() { String describe() {
@ -100,5 +100,5 @@ class _Column {
} }
} }
String mediaTypeTojson(MediaType value) => value.toString(); String mediaTypeToJson(MediaType value) => value.toString();
MediaType mediaTypeFromJson(String value) => MediaType.parse(value); MediaType mediaTypeFromJson(String value) => MediaType.parse(value);

View File

@ -6,7 +6,7 @@ export 'framerate.dart';
export 'muxed_stream_info.dart'; export 'muxed_stream_info.dart';
export 'stream_container.dart'; export 'stream_container.dart';
export 'stream_context.dart'; export 'stream_context.dart';
export 'stream_info.dart' hide mediaTypeFromJson, mediaTypeTojson; export 'stream_info.dart' hide mediaTypeFromJson, mediaTypeToJson;
export 'stream_manifest.dart'; export 'stream_manifest.dart';
export 'streams_client.dart'; export 'streams_client.dart';
export 'video_only_stream_info.dart'; export 'video_only_stream_info.dart';

View File

@ -47,7 +47,7 @@ class VideoOnlyStreamInfo with StreamInfo, VideoStreamInfo {
final List<Fragment> fragments; final List<Fragment> fragments;
@override @override
@JsonKey(toJson: mediaTypeTojson, fromJson: mediaTypeFromJson) @JsonKey(toJson: mediaTypeToJson, fromJson: mediaTypeFromJson)
final MediaType codec; final MediaType codec;
VideoOnlyStreamInfo( VideoOnlyStreamInfo(
@ -65,7 +65,8 @@ class VideoOnlyStreamInfo with StreamInfo, VideoStreamInfo {
this.codec); this.codec);
@override @override
String toString() => 'Video-only ($tag | $videoResolution | $container)'; String toString() =>
'Video-only ($tag | ${videoResolution}p${framerate.framesPerSecond} | $container)';
factory VideoOnlyStreamInfo.fromJson(Map<String, dynamic> json) => factory VideoOnlyStreamInfo.fromJson(Map<String, dynamic> json) =>
_$VideoOnlyStreamInfoFromJson(json); _$VideoOnlyStreamInfoFromJson(json);

View File

@ -38,7 +38,7 @@ Map<String, dynamic> _$VideoOnlyStreamInfoToJson(
'videoResolution': instance.videoResolution, 'videoResolution': instance.videoResolution,
'framerate': instance.framerate, 'framerate': instance.framerate,
'fragments': instance.fragments, 'fragments': instance.fragments,
'codec': mediaTypeTojson(instance.codec), 'codec': mediaTypeToJson(instance.codec),
}; };
const _$VideoQualityEnumMap = { const _$VideoQualityEnumMap = {

View File

@ -32,11 +32,7 @@ extension VideoStreamInfoExtension<T extends VideoStreamInfo> on Iterable<T> {
Set<String> getAllVideoQualitiesLabel() => map((e) => e.qualityLabel).toSet(); Set<String> getAllVideoQualitiesLabel() => map((e) => e.qualityLabel).toSet();
/// Gets the stream with best video quality. /// Gets the stream with best video quality.
@Deprecated( T get bestQuality => sortByVideoQuality().first;
'This is actually a typo, and actually returns the videos sorted by the best *video quality*. Now use the `bestQuality` getter ')
T withHighestBitrate() => sortByVideoQuality().last;
T get bestQuality => sortByVideoQuality().last;
/// Gets the video streams sorted by highest video quality /// Gets the video streams sorted by highest video quality
/// (then by framerate) in ascending order. /// (then by framerate) in ascending order.

View File

@ -1,6 +1,6 @@
name: youtube_explode_dart 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. description: A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
version: 1.10.10+2 version: 1.11.0
homepage: https://github.com/Hexer10/youtube_explode_dart homepage: https://github.com/Hexer10/youtube_explode_dart

View File

@ -1,6 +1,11 @@
import 'dart:math';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
final rnd = Random();
const letters = r'abcdefghilmnopqrstuvzjkwy1234567890!@#$%^&*()_+{}|"?><|~`|';
void main() { void main() {
YoutubeExplode? yt; YoutubeExplode? yt;
setUp(() { setUp(() {
@ -12,47 +17,64 @@ void main() {
}); });
test('Search a youtube video from the search page', () async { test('Search a youtube video from the search page', () async {
var videos = await yt!.search.getVideos('undead corporation megalomania'); var videos = await yt!.search.search('undead corporation megalomania');
expect(videos, isNotEmpty); expect(videos, isNotEmpty);
}); });
test('Search a youtube video from the search page-2', () async { test('Search with no results', () async {
var videos = await yt!.search var videos = await yt!.search.search(
// ignore: deprecated_member_use_from_same_package List.generate(1300, (_) => letters[rnd.nextInt(letters.length)])
.getVideosFromPage('hello') .join());
.where((e) => e is SearchVideo) // Take only the videos. expect(videos, isEmpty);
.cast<SearchVideo>() var nextPage = await videos.nextPage();
.take(10) // Take on 10 results. expect(nextPage, isNull);
.toList();
expect(videos, hasLength(10));
var video = videos.first;
expect(video.id, isNotNull);
expect(video.title, isNotEmpty);
expect(video.author, isNotEmpty);
expect(video.description, isNotEmpty);
expect(video.duration, isNotEmpty);
expect(video.viewCount, greaterThan(0));
expect(video.thumbnails, isNotEmpty);
}); });
// Seems all search key also have result now, so hide this test case test('Search only channels', () async {
// test('Search with no results - old', () async { var channels = await yt!.search
// var query = .searchContent('PewDiePie', filter: TypeFilters.channel);
// // ignore: deprecated_member_use_from_same_package expect(channels.every((e) => e is SearchChannel), isTrue);
// await yt!.search.queryFromPage('g;jghEOGHJeguEPOUIhjegoUEHGOGHPSASG'); });
// expect(query.content, isEmpty);
// expect(query.relatedVideos, isEmpty);
// var nextPage = await query.nextPage();
// expect(nextPage, isNull);
// });
test('Search youtube videos have thumbnails - old', () async { test('Search only playlists', () async {
// ignore: deprecated_member_use_from_same_package var channels =
var searchQuery = await yt!.search.queryFromPage('hello'); await yt!.search.searchContent('Banana', filter: TypeFilters.playlist);
expect(searchQuery.content.first, isA<SearchVideo>()); expect(channels.every((e) => e is SearchPlaylist), isTrue);
});
var video = searchQuery.content.first as SearchVideo; test('Search only movies', () async {
expect(video.thumbnails, isNotEmpty); var channels =
await yt!.search.searchContent('Banana', filter: TypeFilters.playlist);
expect(channels.every((e) => e is SearchPlaylist), isTrue);
});
test('Search test search filters', () async {
var featureSearch =
await yt!.search.searchContent('hello', filter: FeatureFilters.hd);
expect(featureSearch, isNotEmpty);
var uploadSearch = await yt!.search
.searchContent('hello', filter: UploadDateFilter.lastHour);
expect(uploadSearch, isNotEmpty);
var durationSearch =
await yt!.search.searchContent('hello', filter: DurationFilters.long);
expect(durationSearch, isNotEmpty);
var sortSearch =
await yt!.search.searchContent('hello', filter: SortFilters.viewCount);
expect(sortSearch, isNotEmpty);
});
test('Search raw', () async {
var search = await yt!.search.searchRaw('hello');
expect(search.content, isNotEmpty);
expect(search.relatedVideos, isNotEmpty);
expect(search.estimatedResults, greaterThan(1));
});
test('Get youtube search suggestions', () async {
var suggestions = await yt!.search.getQuerySuggestions('hello');
expect(suggestions, isNotEmpty);
}); });
} }