Compare commits
No commits in common. "1795df619d2f84a4b0d5cd8a079f494ced7a8b76" and "51ba543deba72505afff180db1e686f3896b5595" have entirely different histories.
1795df619d
...
51ba543deb
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,16 +1,3 @@
|
|||
## 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
|
||||
- Fix #194: Now closed-captions allow malformed utf8 as well.
|
||||
|
||||
## 1.10.10+1
|
||||
- Deprecated `withHighestBitrate()` in favour of `bestQuality`.
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ class Username with _$Username {
|
|||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !RegExp('[^0-9a-zA-Z]').hasMatch(name);
|
||||
}
|
||||
|
||||
/// Parses a username from a url.
|
||||
|
|
|
@ -5,6 +5,7 @@ import '../../../youtube_explode_dart.dart';
|
|||
import '../../extensions/helpers_extension.dart';
|
||||
import '../../retry.dart';
|
||||
import '../../search/base_search_content.dart';
|
||||
import '../../search/search_channel.dart';
|
||||
import '../models/initial_data.dart';
|
||||
import '../models/youtube_page.dart';
|
||||
|
||||
|
|
|
@ -1,6 +1,2 @@
|
|||
import 'search_channel.dart';
|
||||
import 'search_playlist.dart';
|
||||
import 'search_video.dart';
|
||||
|
||||
/// This can either be a [SearchVideo], [SearchPlaylist], [SearchChannel]
|
||||
/// This can either be a [SearchVideo] or [SearchPlaylist]
|
||||
mixin BaseSearchContent {}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
/// {@category Search}
|
||||
library youtube_explode.search;
|
||||
|
||||
export 'search_channel.dart';
|
||||
export 'search_client.dart';
|
||||
export 'search_filter.dart';
|
||||
export 'search_list.dart';
|
||||
|
|
|
@ -2,7 +2,9 @@ import 'dart:convert';
|
|||
|
||||
import '../../youtube_explode_dart.dart';
|
||||
import '../extensions/helpers_extension.dart';
|
||||
import '../retry.dart';
|
||||
import '../reverse_engineering/pages/search_page.dart';
|
||||
import 'base_search_content.dart';
|
||||
|
||||
/// YouTube search queries.
|
||||
class SearchClient {
|
||||
|
@ -14,12 +16,12 @@ class SearchClient {
|
|||
/// Enumerates videos returned by the specified search query
|
||||
/// (from the video search page).
|
||||
/// The videos are sent in batch of 20 videos.
|
||||
/// You [VideoSearchList.nextPage] to get the next batch of videos.
|
||||
Future<VideoSearchList> search(String searchQuery,
|
||||
{SearchFilter filter = TypeFilters.video}) async {
|
||||
/// You [SearchList.nextPage] to get the next batch of videos.
|
||||
Future<SearchList> getVideos(String searchQuery,
|
||||
{SearchFilter filter = const SearchFilter('')}) async {
|
||||
final page = await SearchPage.get(_httpClient, searchQuery, filter: filter);
|
||||
|
||||
return VideoSearchList(
|
||||
return SearchList(
|
||||
page.searchContent
|
||||
.whereType<SearchVideo>()
|
||||
.map((e) => Video(
|
||||
|
@ -40,31 +42,38 @@ class SearchClient {
|
|||
_httpClient);
|
||||
}
|
||||
|
||||
@Deprecated('Use SearchClient.search')
|
||||
Future<VideoSearchList> getVideos(String searchQuery,
|
||||
{SearchFilter filter = TypeFilters.video}) =>
|
||||
search(searchQuery, filter: filter);
|
||||
/// Enumerates videos returned by the specified search query
|
||||
/// (from the video search page).
|
||||
/// Contains only instances of [SearchVideo] or [SearchPlaylist]
|
||||
@Deprecated(
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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);
|
||||
if (onlyVideos) {
|
||||
yield* Stream.fromIterable(
|
||||
page!.searchContent.whereType<SearchVideo>());
|
||||
} else {
|
||||
yield* Stream.fromIterable(page!.searchContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
Future<List<String>> getQuerySuggestions(String query) async {
|
||||
final request = await _httpClient.get(
|
||||
|
@ -78,14 +87,24 @@ class SearchClient {
|
|||
}
|
||||
|
||||
/// Queries to YouTube to get the results.
|
||||
/// You need to manually read [SearchQuery.content] and/or [SearchQuery.relatedVideos].
|
||||
/// For most cases [SearchClient.search] is enough.
|
||||
Future<SearchQuery> searchRaw(String 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);
|
||||
@Deprecated('Use getVideosFromPage instead - '
|
||||
'Should be used only to get related videos')
|
||||
Future<SearchQuery> queryFromPage(String searchQuery) =>
|
||||
SearchQuery.search(_httpClient, searchQuery);
|
||||
}
|
||||
|
||||
/*
|
||||
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,3 +1,5 @@
|
|||
import 'search_client.dart';
|
||||
|
||||
class SearchFilter {
|
||||
/// The value fo the 'sp' argument.
|
||||
final String value;
|
||||
|
@ -5,103 +7,126 @@ class SearchFilter {
|
|||
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 {
|
||||
const FeatureFilters._();
|
||||
|
||||
/// Live video.
|
||||
static const SearchFilter live = SearchFilter('EgJAAQ%253D%253D');
|
||||
SearchFilter get live => const SearchFilter('EgJAAQ%253D%253D');
|
||||
|
||||
/// 4K video.
|
||||
static const SearchFilter v4k = SearchFilter('EgJwAQ%253D%253D');
|
||||
SearchFilter get v4k => const SearchFilter('EgJwAQ%253D%253D');
|
||||
|
||||
/// HD video.
|
||||
static const SearchFilter hd = SearchFilter('EgIgAQ%253D%253D');
|
||||
SearchFilter get hd => const SearchFilter('EgIgAQ%253D%253D');
|
||||
|
||||
/// Subtitled video.
|
||||
static const SearchFilter subTitles = SearchFilter('EgIoAQ%253D%253D');
|
||||
SearchFilter get subTitles => const SearchFilter('EgIoAQ%253D%253D');
|
||||
|
||||
/// Creative comments video.
|
||||
static const SearchFilter creativeCommons = SearchFilter('EgIwAQ%253D%253D');
|
||||
SearchFilter get creativeCommons => const SearchFilter('EgIwAQ%253D%253D');
|
||||
|
||||
/// 360° video.
|
||||
static const SearchFilter v360 = SearchFilter('EgJ4AQ%253D%253D');
|
||||
SearchFilter get v360 => const SearchFilter('EgJ4AQ%253D%253D');
|
||||
|
||||
/// VR 180° video.
|
||||
static const SearchFilter vr180 = SearchFilter('EgPQAQE%253D');
|
||||
SearchFilter get vr180 => const SearchFilter('EgPQAQE%253D');
|
||||
|
||||
/// 3D video.
|
||||
static const SearchFilter v3D = SearchFilter('EgI4AQ%253D%253D');
|
||||
SearchFilter get v3D => const SearchFilter('EgI4AQ%253D%253D');
|
||||
|
||||
/// HDR video.
|
||||
static const SearchFilter hdr = SearchFilter('EgPIAQE%253D');
|
||||
SearchFilter get hdr => const SearchFilter('EgPIAQE%253D');
|
||||
|
||||
/// Video with location.
|
||||
static const SearchFilter location = SearchFilter('EgO4AQE%253D');
|
||||
SearchFilter get location => const SearchFilter('EgO4AQE%253D');
|
||||
|
||||
/// Purchased video.
|
||||
static const SearchFilter purchased = SearchFilter('EgJIAQ%253D%253D');
|
||||
SearchFilter get purchased => const SearchFilter('EgJIAQ%253D%253D');
|
||||
}
|
||||
|
||||
class UploadDateFilter {
|
||||
const UploadDateFilter._();
|
||||
|
||||
/// Videos uploaded in the last hour.
|
||||
static const SearchFilter lastHour = SearchFilter('EgIIAQ%253D%253D');
|
||||
SearchFilter get lastHour => const SearchFilter('EgIIAQ%253D%253D');
|
||||
|
||||
/// Videos uploaded today.
|
||||
static const SearchFilter today = SearchFilter('EgIIAg%253D%253D');
|
||||
SearchFilter get today => const SearchFilter('EgIIAg%253D%253D');
|
||||
|
||||
/// Videos uploaded in the last week.
|
||||
static const SearchFilter lastWeek = SearchFilter('EgIIAw%253D%253D');
|
||||
SearchFilter get lastWeek => const SearchFilter('EgIIAw%253D%253D');
|
||||
|
||||
/// Videos uploaded in the last month.
|
||||
static const SearchFilter lastMonth = SearchFilter('EgIIBA%253D%253D');
|
||||
SearchFilter get lastMonth => const SearchFilter('EgIIBA%253D%253D');
|
||||
|
||||
/// Videos uploaded in the last year.
|
||||
static const SearchFilter lastYear = SearchFilter('EgIIBQ%253D%253D');
|
||||
SearchFilter get lastYear => const SearchFilter('EgIIBQ%253D%253D');
|
||||
}
|
||||
|
||||
class TypeFilters {
|
||||
const TypeFilters._();
|
||||
|
||||
/// Videos.
|
||||
static const SearchFilter video = SearchFilter('EgIQAQ%253D%253D');
|
||||
SearchFilter get video => const SearchFilter('EgIQAQ%253D%253D');
|
||||
|
||||
/// Channels.
|
||||
static const SearchFilter channel = SearchFilter('EgIQAg%253D%253D');
|
||||
SearchFilter get channel => const SearchFilter('EgIQAg%253D%253D');
|
||||
|
||||
/// Playlists.
|
||||
static const SearchFilter playlist = SearchFilter('EgIQAw%253D%253D');
|
||||
SearchFilter get playlist => const SearchFilter('EgIQAw%253D%253D');
|
||||
|
||||
/// Movies.
|
||||
static const SearchFilter movie = SearchFilter('EgIQBA%253D%253D');
|
||||
SearchFilter get movie => const SearchFilter('EgIQBA%253D%253D');
|
||||
|
||||
/// Shows.
|
||||
static const SearchFilter show = SearchFilter('EgIQBQ%253D%253D');
|
||||
SearchFilter get show => const SearchFilter('EgIQBQ%253D%253D');
|
||||
}
|
||||
|
||||
class DurationFilters {
|
||||
const DurationFilters._();
|
||||
|
||||
/// Short videos, < 4 minutes.
|
||||
static const SearchFilter short = SearchFilter('EgIYAQ%253D%253D');
|
||||
SearchFilter get short => const SearchFilter('EgIYAQ%253D%253D');
|
||||
|
||||
/// Long videos, > 20 minutes.
|
||||
static const SearchFilter long = SearchFilter('EgIYAg%253D%253D');
|
||||
SearchFilter get long => const SearchFilter('EgIYAg%253D%253D');
|
||||
}
|
||||
|
||||
class SortFilters {
|
||||
const SortFilters._();
|
||||
|
||||
/// Sort by relevance (default).
|
||||
static const SearchFilter relevance = SearchFilter('CAASAhAB');
|
||||
SearchFilter get relevance => const SearchFilter('CAASAhAB');
|
||||
|
||||
/// Sort by upload date (default).
|
||||
static const SearchFilter uploadDate = SearchFilter('CAI%253D');
|
||||
SearchFilter get uploadDate => const SearchFilter('CAI%253D');
|
||||
|
||||
/// Sort by view count (default).
|
||||
static const SearchFilter viewCount = SearchFilter('CAM%253D');
|
||||
SearchFilter get viewCount => const SearchFilter('CAM%253D');
|
||||
|
||||
/// Sort by rating (default).
|
||||
static const SearchFilter rating = SearchFilter('CAE%253D');
|
||||
SearchFilter get rating => const SearchFilter('CAE%253D');
|
||||
}
|
||||
|
|
|
@ -5,17 +5,16 @@ import 'package:collection/collection.dart';
|
|||
import '../../youtube_explode_dart.dart';
|
||||
import '../extensions/helpers_extension.dart';
|
||||
import '../reverse_engineering/pages/search_page.dart';
|
||||
import 'base_search_content.dart';
|
||||
|
||||
/// This list contains search videos.
|
||||
///This behaves like a [List] but has the [SearchList.nextPage] to get the next batch of videos.
|
||||
class SearchList<T extends BaseSearchContent> extends DelegatingList<T> {
|
||||
class SearchList extends DelegatingList<Video> {
|
||||
final SearchPage _page;
|
||||
final YoutubeHttpClient _httpClient;
|
||||
|
||||
/// Construct an instance of [SearchList]
|
||||
/// See [SearchList]
|
||||
SearchList(List<T> base, this._page, this._httpClient) : super(base);
|
||||
SearchList(List<Video> base, this._page, this._httpClient) : super(base);
|
||||
|
||||
/// Fetches the next batch of videos or returns null if there are no more
|
||||
/// results.
|
||||
|
@ -24,28 +23,7 @@ class SearchList<T extends BaseSearchContent> extends DelegatingList<T> {
|
|||
if (page == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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(
|
||||
return SearchList(
|
||||
page.searchContent
|
||||
.whereType<SearchVideo>()
|
||||
.map((e) => Video(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import '../../youtube_explode_dart.dart';
|
||||
import '../reverse_engineering/pages/search_page.dart';
|
||||
import '../reverse_engineering/youtube_http_client.dart';
|
||||
|
||||
///
|
||||
class SearchQuery {
|
||||
|
@ -15,9 +15,8 @@ class SearchQuery {
|
|||
|
||||
/// Search a video.
|
||||
static Future<SearchQuery> search(
|
||||
YoutubeHttpClient httpClient, String searchQuery,
|
||||
{SearchFilter filter = const SearchFilter('')}) async {
|
||||
var page = await SearchPage.get(httpClient, searchQuery, filter: filter);
|
||||
YoutubeHttpClient httpClient, String searchQuery) async {
|
||||
var page = await SearchPage.get(httpClient, searchQuery);
|
||||
return SearchQuery(httpClient, searchQuery, page);
|
||||
}
|
||||
|
||||
|
@ -32,7 +31,7 @@ class SearchQuery {
|
|||
}
|
||||
|
||||
/// Content of this search.
|
||||
/// Contains either [SearchVideo], [SearchPlaylist] or [SearchChannel]
|
||||
/// Contains either [SearchVideo] or [SearchPlaylist]
|
||||
List<dynamic> get content => _page.searchContent;
|
||||
|
||||
/// Videos related to this search.
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import '../../extensions/helpers_extension.dart';
|
||||
import '../../reverse_engineering/clients/closed_caption_client.dart' as re
|
||||
show ClosedCaptionClient;
|
||||
|
@ -30,8 +28,7 @@ class ClosedCaptionClient {
|
|||
]}) async {
|
||||
videoId = VideoId.fromString(videoId);
|
||||
var tracks = <ClosedCaptionTrackInfo>{};
|
||||
var watchPage =
|
||||
await WatchPage.get(_httpClient, (videoId as VideoId).value);
|
||||
var watchPage = await WatchPage.get(_httpClient, (videoId as VideoId).value);
|
||||
var playerResponse = watchPage.playerResponse!;
|
||||
|
||||
for (final track in playerResponse.closedCaptionTrack) {
|
||||
|
@ -59,9 +56,7 @@ class ClosedCaptionClient {
|
|||
return ClosedCaptionTrack(captions);
|
||||
}
|
||||
|
||||
/// Returns the subtitles as a string. In XML format.
|
||||
Future<String> getSubTitles(ClosedCaptionTrackInfo trackInfo) async {
|
||||
final r = await _httpClient.get(trackInfo.url);
|
||||
return utf8.decode(r.bodyBytes, allowMalformed: true);
|
||||
}
|
||||
/// Returns the subtitles as a string.
|
||||
Future<String> getSubTitles(ClosedCaptionTrackInfo trackInfo) =>
|
||||
_httpClient.getString(trackInfo.url);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ class AudioOnlyStreamInfo with StreamInfo, AudioStreamInfo {
|
|||
final String audioCodec;
|
||||
|
||||
@override
|
||||
@JsonKey(toJson: mediaTypeToJson, fromJson: mediaTypeFromJson)
|
||||
@JsonKey(toJson: mediaTypeTojson, fromJson: mediaTypeFromJson)
|
||||
final MediaType codec;
|
||||
|
||||
@override
|
||||
|
|
|
@ -30,7 +30,7 @@ Map<String, dynamic> _$AudioOnlyStreamInfoToJson(
|
|||
'size': instance.size,
|
||||
'bitrate': instance.bitrate,
|
||||
'audioCodec': instance.audioCodec,
|
||||
'codec': mediaTypeToJson(instance.codec),
|
||||
'codec': mediaTypeTojson(instance.codec),
|
||||
'fragments': instance.fragments,
|
||||
'qualityLabel': instance.qualityLabel,
|
||||
};
|
||||
|
|
|
@ -61,7 +61,7 @@ class MuxedStreamInfo with StreamInfo, AudioStreamInfo, VideoStreamInfo {
|
|||
|
||||
/// Stream codec.
|
||||
@override
|
||||
@JsonKey(toJson: mediaTypeToJson, fromJson: mediaTypeFromJson)
|
||||
@JsonKey(toJson: mediaTypeTojson, fromJson: mediaTypeFromJson)
|
||||
final MediaType codec;
|
||||
|
||||
/// Stream codec.
|
||||
|
@ -85,8 +85,7 @@ class MuxedStreamInfo with StreamInfo, AudioStreamInfo, VideoStreamInfo {
|
|||
);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Muxed ($tag | ${videoResolution}p${framerate.framesPerSecond} | $container)';
|
||||
String toString() => 'Muxed ($tag | $qualityLabel | $container)';
|
||||
|
||||
factory MuxedStreamInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$MuxedStreamInfoFromJson(json);
|
||||
|
|
|
@ -34,7 +34,7 @@ Map<String, dynamic> _$MuxedStreamInfoToJson(MuxedStreamInfo instance) =>
|
|||
'videoQuality': _$VideoQualityEnumMap[instance.videoQuality],
|
||||
'videoResolution': instance.videoResolution,
|
||||
'framerate': instance.framerate,
|
||||
'codec': mediaTypeToJson(instance.codec),
|
||||
'codec': mediaTypeTojson(instance.codec),
|
||||
'qualityLabel': instance.qualityLabel,
|
||||
};
|
||||
|
||||
|
|
|
@ -37,12 +37,12 @@ mixin StreamInfo {
|
|||
/// Extension for Iterables of StreamInfo.
|
||||
extension StreamInfoIterableExt<T extends StreamInfo> on Iterable<T> {
|
||||
/// Gets the stream with highest bitrate.
|
||||
T withHighestBitrate() => sortByBitrate().first;
|
||||
T withHighestBitrate() => sortByBitrate().last;
|
||||
|
||||
/// Gets the video streams sorted by bitrate in ascending order.
|
||||
/// This returns new list without editing the original list.
|
||||
List<T> sortByBitrate() =>
|
||||
toList()..sort((a, b) => b.bitrate.compareTo(a.bitrate));
|
||||
toList()..sort((a, b) => a.bitrate.compareTo(b.bitrate));
|
||||
|
||||
/// Print a formatted text of all the streams. Like youtube-dl -F option.
|
||||
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);
|
||||
|
|
|
@ -6,7 +6,7 @@ export 'framerate.dart';
|
|||
export 'muxed_stream_info.dart';
|
||||
export 'stream_container.dart';
|
||||
export 'stream_context.dart';
|
||||
export 'stream_info.dart' hide mediaTypeFromJson, mediaTypeToJson;
|
||||
export 'stream_info.dart' hide mediaTypeFromJson, mediaTypeTojson;
|
||||
export 'stream_manifest.dart';
|
||||
export 'streams_client.dart';
|
||||
export 'video_only_stream_info.dart';
|
||||
|
|
|
@ -47,7 +47,7 @@ class VideoOnlyStreamInfo with StreamInfo, VideoStreamInfo {
|
|||
final List<Fragment> fragments;
|
||||
|
||||
@override
|
||||
@JsonKey(toJson: mediaTypeToJson, fromJson: mediaTypeFromJson)
|
||||
@JsonKey(toJson: mediaTypeTojson, fromJson: mediaTypeFromJson)
|
||||
final MediaType codec;
|
||||
|
||||
VideoOnlyStreamInfo(
|
||||
|
@ -65,8 +65,7 @@ class VideoOnlyStreamInfo with StreamInfo, VideoStreamInfo {
|
|||
this.codec);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Video-only ($tag | ${videoResolution}p${framerate.framesPerSecond} | $container)';
|
||||
String toString() => 'Video-only ($tag | $videoResolution | $container)';
|
||||
|
||||
factory VideoOnlyStreamInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$VideoOnlyStreamInfoFromJson(json);
|
||||
|
|
|
@ -38,7 +38,7 @@ Map<String, dynamic> _$VideoOnlyStreamInfoToJson(
|
|||
'videoResolution': instance.videoResolution,
|
||||
'framerate': instance.framerate,
|
||||
'fragments': instance.fragments,
|
||||
'codec': mediaTypeToJson(instance.codec),
|
||||
'codec': mediaTypeTojson(instance.codec),
|
||||
};
|
||||
|
||||
const _$VideoQualityEnumMap = {
|
||||
|
|
|
@ -32,7 +32,11 @@ extension VideoStreamInfoExtension<T extends VideoStreamInfo> on Iterable<T> {
|
|||
Set<String> getAllVideoQualitiesLabel() => map((e) => e.qualityLabel).toSet();
|
||||
|
||||
/// Gets the stream with best video quality.
|
||||
T get bestQuality => sortByVideoQuality().first;
|
||||
@Deprecated(
|
||||
'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
|
||||
/// (then by framerate) in ascending order.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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.
|
||||
version: 1.11.0
|
||||
version: 1.10.10+1
|
||||
|
||||
homepage: https://github.com/Hexer10/youtube_explode_dart
|
||||
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
final rnd = Random();
|
||||
const letters = r'abcdefghilmnopqrstuvzjkwy1234567890!@#$%^&*()_+{}|"?><|~`|';
|
||||
|
||||
void main() {
|
||||
YoutubeExplode? yt;
|
||||
setUp(() {
|
||||
|
@ -17,64 +12,47 @@ void main() {
|
|||
});
|
||||
|
||||
test('Search a youtube video from the search page', () async {
|
||||
var videos = await yt!.search.search('undead corporation megalomania');
|
||||
var videos = await yt!.search.getVideos('undead corporation megalomania');
|
||||
expect(videos, isNotEmpty);
|
||||
});
|
||||
|
||||
test('Search with no results', () async {
|
||||
var videos = await yt!.search.search(
|
||||
List.generate(1300, (_) => letters[rnd.nextInt(letters.length)])
|
||||
.join());
|
||||
expect(videos, isEmpty);
|
||||
var nextPage = await videos.nextPage();
|
||||
expect(nextPage, isNull);
|
||||
test('Search a youtube video from the search page-2', () async {
|
||||
var videos = await yt!.search
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
.getVideosFromPage('hello')
|
||||
.where((e) => e is SearchVideo) // Take only the videos.
|
||||
.cast<SearchVideo>()
|
||||
.take(10) // Take on 10 results.
|
||||
.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);
|
||||
});
|
||||
|
||||
test('Search only videos', () async {
|
||||
var videos =
|
||||
await yt!.search.searchContent('Banana', filter: TypeFilters.video);
|
||||
expect(videos, everyElement(isA<SearchVideo>()));
|
||||
});
|
||||
// Seems all search key also have result now, so hide this test case
|
||||
// test('Search with no results - old', () async {
|
||||
// var query =
|
||||
// // ignore: deprecated_member_use_from_same_package
|
||||
// await yt!.search.queryFromPage('g;jghEOGHJeguEPOUIhjegoUEHGOGHPSASG');
|
||||
// expect(query.content, isEmpty);
|
||||
// expect(query.relatedVideos, isEmpty);
|
||||
// var nextPage = await query.nextPage();
|
||||
// expect(nextPage, isNull);
|
||||
// });
|
||||
|
||||
test('Search only channels', () async {
|
||||
var channels = await yt!.search
|
||||
.searchContent('PewDiePie', filter: TypeFilters.channel);
|
||||
expect(channels, everyElement(isA<SearchChannel>()));
|
||||
});
|
||||
test('Search youtube videos have thumbnails - old', () async {
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
var searchQuery = await yt!.search.queryFromPage('hello');
|
||||
expect(searchQuery.content.first, isA<SearchVideo>());
|
||||
|
||||
test('Search only playlists', () async {
|
||||
var playlists =
|
||||
await yt!.search.searchContent('Banana', filter: TypeFilters.playlist);
|
||||
expect(playlists, everyElement(isA<SearchPlaylist>()));
|
||||
});
|
||||
|
||||
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);
|
||||
var video = searchQuery.content.first as SearchVideo;
|
||||
expect(video.thumbnails, isNotEmpty);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue