parent
0ce24bf017
commit
e231b595a8
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue