Null safety migration

This commit is contained in:
Mattia 2021-03-04 12:20:00 +01:00
parent 5385ba1628
commit cdca20011f
22 changed files with 200 additions and 133 deletions

View File

@ -8,9 +8,9 @@ class ChannelId with EquatableMixin {
final String value;
/// Initializes an instance of [ChannelId]
ChannelId(String value) : value = parseChannelId(value) {
if (this.value == null) {
throw ArgumentError.value(value, 'value', 'Invalid channel id');
ChannelId(String value) : value = parseChannelId(value) ?? '' {
if (value.isEmpty) {
throw ArgumentError.value(value);
}
}
@ -33,8 +33,8 @@ class ChannelId with EquatableMixin {
/// Parses a channel id from an url.
/// Returns null if the username is not found.
static String parseChannelId(String url) {
if (url.isNullOrWhiteSpace) {
static String? parseChannelId(String url) {
if (url.isEmpty) {
return null;
}
@ -45,7 +45,7 @@ class ChannelId with EquatableMixin {
var regMatch = RegExp(r'youtube\..+?/channel/(.*?)(?:\?|&|/|$)')
.firstMatch(url)
?.group(1);
if (!regMatch.isNullOrWhiteSpace && validateChannelId(regMatch)) {
if (!regMatch.isNullOrWhiteSpace && validateChannelId(regMatch!)) {
return regMatch;
}
return null;

View File

@ -6,8 +6,8 @@ class Username {
final String value;
/// Initializes an instance of [Username].
Username(String urlOrUsername) : value = parseUsername(urlOrUsername) {
if (value == null) {
Username(String urlOrUsername) : value = parseUsername(urlOrUsername) ?? '' {
if (value.isEmpty) {
throw ArgumentError.value(
urlOrUsername, 'urlOrUsername', 'Invalid username');
}
@ -27,8 +27,8 @@ class Username {
}
/// Parses a username from a url.
static String parseUsername(String nameOrUrl) {
if (nameOrUrl.isNullOrWhiteSpace) {
static String? parseUsername(String nameOrUrl) {
if (nameOrUrl.isEmpty) {
return null;
}
@ -39,7 +39,7 @@ class Username {
var regMatch = RegExp(r'youtube\..+?/user/(.*?)(?:\?|&|/|$)')
.firstMatch(nameOrUrl)
?.group(1);
if (!regMatch.isNullOrWhiteSpace && validateUsername(regMatch)) {
if (!regMatch.isNullOrWhiteSpace && validateUsername(regMatch!)) {
return regMatch;
}
return null;

View File

@ -6,26 +6,50 @@ class Engagement extends Equatable {
final int viewCount;
/// Like count.
final int likeCount;
final int? likeCount;
/// Dislike count.
final int dislikeCount;
final int? dislikeCount;
/// Initializes an instance of [Statistics]
/// Initializes an instance of [Engagement]
const Engagement(this.viewCount, this.likeCount, this.dislikeCount);
/// Average user rating in stars (1 star to 5 stars).
/// Returns -1 if likeCount or dislikeCount is null.
num get avgRating {
if (likeCount + dislikeCount == 0) {
if (likeCount == null || dislikeCount == null) {
return -1;
}
if (likeCount! + dislikeCount! == 0) {
return 0;
}
return 1 + 4.0 * likeCount / (likeCount + dislikeCount);
return 1 + 4.0 * likeCount! / (likeCount! + dislikeCount!);
}
@override
String toString() =>
'$viewCount views, $likeCount likes, $dislikeCount dislikes';
@override
List<Object?> get props => [viewCount, likeCount, dislikeCount];
}
/// User activity statistics.
/// No null types
class SafeEngagement extends Engagement {
@override
final int viewCount;
@override
final int likeCount;
@override
final int dislikeCount;
/// Initializes an instance of [Engagement]
const SafeEngagement(this.viewCount, this.likeCount, this.dislikeCount)
: super(viewCount, likeCount, dislikeCount);
@override
List<Object> get props => [viewCount, likeCount, dislikeCount];
}

View File

@ -4,23 +4,8 @@ import '../reverse_engineering/cipher/cipher_operations.dart';
/// Utility for Strings.
extension StringUtility on String {
/// Parses this value as int stripping the non digit characters,
/// returns null if this fails.
int parseInt() => int.tryParse(this?.stripNonDigits());
/// Returns null if this string is whitespace.
String get nullIfWhitespace => trim().isEmpty ? null : this;
/// Returns true if the string is null or empty.
bool get isNullOrWhiteSpace {
if (this == null) {
return true;
}
if (trim().isEmpty) {
return true;
}
return false;
}
String? get nullIfWhitespace => trim().isEmpty ? null : this;
/// Returns null if this string is a whitespace.
String substringUntil(String separator) => substring(0, indexOf(separator));
@ -59,6 +44,24 @@ extension StringUtility on String {
}
}
/// Utility for Strings.
extension StringUtility2 on String? {
/// Parses this value as int stripping the non digit characters,
/// returns null if this fails.
int? parseInt() => int.tryParse(this?.stripNonDigits() ?? '');
/// Returns true if the string is null or empty.
bool get isNullOrWhiteSpace {
if (this == null) {
return true;
}
if (this!.trim().isEmpty) {
return true;
}
return false;
}
}
/// List decipher utility.
extension ListDecipher on Iterable<CipherOperation> {
/// Apply every CipherOperation on the [signature]
@ -74,7 +77,7 @@ extension ListDecipher on Iterable<CipherOperation> {
/// List Utility.
extension ListUtil<E> on Iterable<E> {
/// Returns the first element of a list or null if empty.
E get firstOrNull {
E? get firstOrNull {
if (length == 0) {
return null;
}
@ -83,12 +86,22 @@ extension ListUtil<E> on Iterable<E> {
/// Same as [elementAt] but if the index is higher than the length returns
/// null
E elementAtSafe(int index) {
E? elementAtSafe(int index) {
if (index >= length) {
return null;
}
return elementAt(index);
}
/// Same as [firstWhere] but returns null if no found
E? firstWhereNull(bool Function(E element) test) {
for (final element in this) {
if (test(element)) {
return element;
}
}
return null;
}
}
/// Uri utility
@ -106,7 +119,7 @@ extension UriUtility on Uri {
///
extension GetOrNull<K, V> on Map<K, V> {
/// Get a value from a map
V getValue(K key) {
V? getValue(K key) {
var v = this[key];
if (v == null) {
return null;
@ -118,7 +131,7 @@ extension GetOrNull<K, V> on Map<K, V> {
///
extension GetOrNullMap on Map {
/// Get a map inside a map
Map<String, dynamic> get(String key) {
Map<String, dynamic>? get(String key) {
var v = this[key];
if (v == null) {
return null;
@ -128,7 +141,7 @@ extension GetOrNullMap on Map {
/// Get a value inside a map.
/// If it is null this returns null, if of another type this throws.
T getT<T>(String key) {
T? getT<T>(String key) {
var v = this[key];
if (v == null) {
return null;
@ -140,7 +153,7 @@ extension GetOrNullMap on Map {
}
/// Get a List<Map<String, dynamic>>> from a map.
List<Map<String, dynamic>> getList(String key) {
List<Map<String, dynamic>>? getList(String key) {
var v = this[key];
if (v == null) {
return null;
@ -149,7 +162,7 @@ extension GetOrNullMap on Map {
throw Exception('Invalid type: ${v.runtimeType} should be of type List');
}
return (v.toList() as List<dynamic>).cast<Map<String, dynamic>>();
return (v.toList()).cast<Map<String, dynamic>>();
}
}
@ -164,7 +177,8 @@ extension UriUtils on Uri {
}
}
/// Parse properties with `runs` method.
/// Parse properties with `text` method.
extension RunsParser on List<dynamic> {
String parseRuns() => this?.map((e) => e['text'])?.join() ?? '';
///
String parseRuns() => map((e) => e['text']).join() ?? '';
}

View File

@ -17,8 +17,8 @@ class PlaylistId with EquatableMixin {
final String value;
/// Initializes an instance of [PlaylistId]
PlaylistId(String idOrUrl) : value = parsePlaylistId(idOrUrl) {
if (value == null) {
PlaylistId(String idOrUrl) : value = parsePlaylistId(idOrUrl) ?? '' {
if (value.isEmpty) {
throw ArgumentError.value(idOrUrl, 'idOrUrl', 'Invalid url');
}
}
@ -61,7 +61,7 @@ class PlaylistId with EquatableMixin {
/// Parses a playlist [url] returning its id.
/// If the [url] is a valid it is returned itself.
static String parsePlaylistId(String url) {
static String? parsePlaylistId(String url) {
if (url.isNullOrWhiteSpace) {
return null;
}
@ -71,25 +71,25 @@ class PlaylistId with EquatableMixin {
}
var regMatch = _regMatchExp.firstMatch(url)?.group(1);
if (!regMatch.isNullOrWhiteSpace && validatePlaylistId(regMatch)) {
if (!regMatch.isNullOrWhiteSpace && validatePlaylistId(regMatch!)) {
return regMatch;
}
var compositeMatch = _compositeMatchExp.firstMatch(url)?.group(1);
if (!compositeMatch.isNullOrWhiteSpace &&
validatePlaylistId(compositeMatch)) {
validatePlaylistId(compositeMatch!)) {
return compositeMatch;
}
var shortCompositeMatch = _shortCompositeMatchExp.firstMatch(url)?.group(1);
if (!shortCompositeMatch.isNullOrWhiteSpace &&
validatePlaylistId(shortCompositeMatch)) {
validatePlaylistId(shortCompositeMatch!)) {
return shortCompositeMatch;
}
var embedCompositeMatch = _embedCompositeMatchExp.firstMatch(url)?.group(1);
if (!embedCompositeMatch.isNullOrWhiteSpace &&
validatePlaylistId(embedCompositeMatch)) {
validatePlaylistId(embedCompositeMatch!)) {
return embedCompositeMatch;
}
return null;

View File

@ -12,14 +12,10 @@ import 'generated/channel_about_page_id.g.dart';
class ChannelAboutPage {
final Document _root;
_InitialData _initialData;
///
_InitialData get initialData {
if (_initialData != null) {
return _initialData;
}
late final _InitialData initialData = _getInitialData();
_InitialData _getInitialData() {
final scriptText = _root
.querySelectorAll('script')
.map((e) => e.text)
@ -27,17 +23,17 @@ class ChannelAboutPage {
var initialDataText = scriptText.firstWhere(
(e) => e.contains('window["ytInitialData"] ='),
orElse: () => null);
if (initialDataText != null) {
return _initialData = _InitialData(ChannelAboutPageId.fromRawJson(
orElse: () => '');
if (initialDataText.isNotEmpty) {
return _InitialData(ChannelAboutPageId.fromRawJson(
_extractJson(initialDataText, 'window["ytInitialData"] =')));
}
initialDataText = scriptText.firstWhere(
(e) => e.contains('var ytInitialData = '),
orElse: () => null);
if (initialDataText != null) {
return _initialData = _InitialData(ChannelAboutPageId.fromRawJson(
orElse: () => '');
if (initialDataText.isNotEmpty) {
return _InitialData(ChannelAboutPageId.fromRawJson(
_extractJson(initialDataText, 'var ytInitialData = ')));
}
@ -45,9 +41,6 @@ class ChannelAboutPage {
'Failed to retrieve initial data from the channel about page, please report this to the project GitHub page.'); // ignore: lines_longer_than_80_chars
}
///
bool get isOk => initialData != null;
///
String get description => initialData.description;
@ -58,7 +51,7 @@ class ChannelAboutPage {
String _matchJson(String str) {
var bracketCount = 0;
int lastI;
late int lastI;
for (var i = 0; i < str.length; i++) {
lastI = i;
if (str[i] == '{') {
@ -88,9 +81,6 @@ class ChannelAboutPage {
var raw = await httpClient.getString(url);
var result = ChannelAboutPage.parse(raw);
if (!result.isOk) {
throw TransientFailureException('Channel about page is broken');
}
return result;
});
}
@ -104,9 +94,6 @@ class ChannelAboutPage {
var raw = await httpClient.getString(url);
var result = ChannelAboutPage.parse(raw);
if (!result.isOk) {
throw TransientFailureException('Channel about page is broken');
}
return result;
});
}
@ -120,13 +107,10 @@ class _InitialData {
_InitialData(this.root);
/* Cache results */
ChannelAboutFullMetadataRenderer _content;
ChannelAboutFullMetadataRenderer get content =>
_content ??= getContentContext();
late final ChannelAboutFullMetadataRenderer content = _getContentContext();
ChannelAboutFullMetadataRenderer getContentContext() {
ChannelAboutFullMetadataRenderer _getContentContext() {
return root
.contents
.twoColumnBrowseResultsRenderer
@ -166,8 +150,8 @@ class _InitialData {
String get country => content.country.simpleText;
String parseRuns(List<dynamic> runs) =>
runs?.map((e) => e.text)?.join() ?? '';
String parseRuns(List<dynamic>? runs) =>
runs?.map((e) => e.text).join() ?? '';
Uri extractUrl(String text) =>
Uri.parse(Uri.decodeFull(_urlExp.firstMatch(text)?.group(1) ?? ''));

View File

@ -15,18 +15,21 @@ class ChannelPage {
///
String get channelUrl =>
_root.querySelector('meta[property="og:url"]')?.attributes['content'];
_root.querySelector('meta[property="og:url"]')?.attributes['content'] ??
'';
///
String get channelId => channelUrl.substringAfter('channel/');
///
String get channelTitle =>
_root.querySelector('meta[property="og:title"]')?.attributes['content'];
_root.querySelector('meta[property="og:title"]')?.attributes['content'] ??
'';
///
String get channelLogoUrl =>
_root.querySelector('meta[property="og:image"]')?.attributes['content'];
_root.querySelector('meta[property="og:image"]')?.attributes['content'] ??
'';
///
ChannelPage(this._root);

View File

@ -16,14 +16,10 @@ class ChannelUploadPage {
final String channelId;
final Document _root;
_InitialData _initialData;
late final _InitialData _initialData = _getInitialData();
///
_InitialData get initialData {
if (_initialData != null) {
return _initialData;
}
_InitialData _getInitialData() {
final scriptText = _root
.querySelectorAll('script')
.map((e) => e.text)
@ -31,17 +27,17 @@ class ChannelUploadPage {
var initialDataText = scriptText.firstWhere(
(e) => e.contains('window["ytInitialData"] ='),
orElse: () => null);
if (initialDataText != null) {
return _initialData = _InitialData(ChannelUploadPageId.fromRawJson(
orElse: () => '');
if (initialDataText.isNotEmpty) {
return _InitialData(ChannelUploadPageId.fromRawJson(
_extractJson(initialDataText, 'window["ytInitialData"] =')));
}
initialDataText = scriptText.firstWhere(
(e) => e.contains('var ytInitialData = '),
orElse: () => null);
if (initialDataText != null) {
return _initialData = _InitialData(ChannelUploadPageId.fromRawJson(
orElse: () => '');
if (initialDataText.isNotEmpty) {
return _InitialData(ChannelUploadPageId.fromRawJson(
_extractJson(initialDataText, 'var ytInitialData = ')));
}
@ -56,7 +52,7 @@ class ChannelUploadPage {
String _matchJson(String str) {
var bracketCount = 0;
int lastI;
late int lastI;
for (var i = 0; i < str.length; i++) {
lastI = i;
if (str[i] == '{') {

View File

@ -1,3 +1,5 @@
// @dart=2.9
// To parse this JSON data, do
//
// final channelAboutId = channelAboutIdFromJson(jsonString);

View File

@ -1,3 +1,5 @@
// @dart=2.9
// To parse this JSON data, do
//
// final channelUploadPageId = channelUploadPageIdFromJson(jsonString);

View File

@ -1,3 +1,5 @@
// @dart=2.9
// To parse this JSON data, do
//
// final playerResponseJson = playerResponseJsonFromJson(jsonString);

View File

@ -1,3 +1,5 @@
// @dart=2.9
// To parse this JSON data, do
//
// final playerConfigJson = playerConfigJsonFromJson(jsonString);

View File

@ -1,3 +1,5 @@
// @dart=2.9
// To parse this JSON data, do
//
// final playlistPageId = playlistPageIdFromJson(jsonString);

View File

@ -1,3 +1,5 @@
// @dart=2.9
// To parse this JSON data, do
//
// final playlistResponseJson = playlistResponseJsonFromJson(jsonString);

View File

@ -1,3 +1,5 @@
// @dart=2.9
// To parse this JSON data, do
//
// final searchPageId = searchPageIdFromJson(jsonString);

View File

@ -1,3 +1,5 @@
// @dart=2.9
// To parse this JSON data, do
//
// final watchPageId = watchPageIdFromJson(jsonString);

View File

@ -24,12 +24,12 @@ class YoutubeHttpClient extends http.BaseClient {
};
/// Initialize an instance of [YoutubeHttpClient]
YoutubeHttpClient([http.Client httpClient])
YoutubeHttpClient([http.Client? httpClient])
: _httpClient = httpClient ?? http.Client();
/// Throws if something is wrong with the response.
void _validateResponse(http.BaseResponse response, int statusCode) {
var request = response.request;
var request = response.request!;
if (request.url.host.endsWith('.google.com') &&
request.url.path.startsWith('/sorry/')) {
throw RequestLimitExceededException.httpRequest(response);
@ -50,7 +50,7 @@ class YoutubeHttpClient extends http.BaseClient {
///
Future<String> getString(dynamic url,
{Map<String, String> headers, bool validate = true}) async {
{Map<String, String> headers = const {}, bool validate = true}) async {
var response = await get(url, headers: headers);
if (validate) {
@ -62,7 +62,7 @@ class YoutubeHttpClient extends http.BaseClient {
@override
Future<http.Response> get(dynamic url,
{Map<String, String> headers, bool validate = false}) async {
{Map<String, String>? headers = const {}, bool validate = false}) async {
var response = await super.get(url, headers: headers);
if (validate) {
_validateResponse(response, response.statusCode);
@ -72,8 +72,8 @@ class YoutubeHttpClient extends http.BaseClient {
///
Future<String> postString(dynamic url,
{Map<String, String> body,
Map<String, String> headers,
{Map<String, String>? body,
Map<String, String> headers = const {},
bool validate = true}) async {
var response = await post(url, headers: headers, body: body);
@ -85,7 +85,7 @@ class YoutubeHttpClient extends http.BaseClient {
}
Stream<List<int>> getStream(StreamInfo streamInfo,
{Map<String, String> headers,
{Map<String, String> headers = const {},
bool validate = true,
int start = 0,
int errorCount = 0}) async* {
@ -122,8 +122,8 @@ class YoutubeHttpClient extends http.BaseClient {
}
///
Future<int> getContentLength(dynamic url,
{Map<String, String> headers, bool validate = true}) async {
Future<int?> getContentLength(dynamic url,
{Map<String, String> headers = const {}, bool validate = true}) async {
var response = await head(url, headers: headers);
if (validate) {
@ -140,7 +140,7 @@ class YoutubeHttpClient extends http.BaseClient {
Future<http.StreamedResponse> send(http.BaseRequest request) {
_defaultHeaders.forEach((key, value) {
if (request.headers[key] == null) {
request.headers[key] = _defaultHeaders[key];
request.headers[key] = _defaultHeaders[key]!;
}
});
// print('Request: $request');

View File

@ -36,7 +36,7 @@ class ClosedCaption {
/// Returns null if not found.
/// Note that some captions may not have any parts at all.
ClosedCaptionPart getPartByTime(Duration offset) =>
parts.firstWhere((e) => e.offset >= offset, orElse: () => null);
parts.firstWhere((e) => e.offset >= offset, orElse: () => null));
@override
String toString() => 'Text: $text';

View File

@ -28,7 +28,7 @@ class Video with EquatableMixin {
/// Video upload date.
/// Note: For search queries it is calculated with:
/// DateTime.now() - how much time is was published.
final DateTime uploadDate;
final DateTime? uploadDate;
/// Video description.
final String description;
@ -46,11 +46,11 @@ class Video with EquatableMixin {
final Engagement engagement;
/// Returns true if this is a live stream.
final bool isLive;
final bool? isLive;
/// Used internally.
/// Shouldn't be used in the code.
final WatchPage watchPage;
final WatchPage? watchPage;
/// Returns true if the watch page is available for this video.
bool get hasWatchPage => watchPage != null;
@ -65,11 +65,11 @@ class Video with EquatableMixin {
this.description,
this.duration,
this.thumbnails,
Iterable<String> keywords,
Iterable<String>? keywords,
this.engagement,
this.isLive, // ignore: avoid_positional_boolean_parameters
[this.watchPage])
: keywords = UnmodifiableListView(keywords);
: keywords = UnmodifiableListView(keywords ?? []);
@override
String toString() => 'Video ($title)';
@ -77,3 +77,33 @@ class Video with EquatableMixin {
@override
List<Object> get props => [id];
}
/// See [Video].
/// This class has no nullable values.
class SafeVideo extends Video {
@override
final DateTime uploadDate;
@override
final SafeEngagement engagement;
@override
final bool isLive;
///
SafeVideo(
VideoId id,
String title,
String author,
ChannelId channelId,
this.uploadDate,
String description,
Duration duration,
ThumbnailSet thumbnails,
Iterable<String>? keywords,
this.engagement,
this.isLive, // ignore: avoid_positional_boolean_parameters
[WatchPage? watchPage])
: super(id, title, author, channelId, uploadDate, description, duration,
thumbnails, keywords, engagement, isLive, watchPage);
}

View File

@ -12,8 +12,8 @@ class VideoId with EquatableMixin {
final String value;
/// Initializes an instance of [VideoId] with a url or video id.
VideoId(String idOrUrl) : value = parseVideoId(idOrUrl) {
if (value == null) {
VideoId(String idOrUrl) : value = parseVideoId(idOrUrl) ?? '' {
if (value.isEmpty) {
throw ArgumentError.value(
idOrUrl, 'urlOrUrl', 'Invalid YouTube video ID or URL');
}
@ -40,7 +40,7 @@ class VideoId with EquatableMixin {
/// Parses a video id from url or if given a valid id as url returns itself.
/// Returns null if the id couldn't be extracted.
static String parseVideoId(String url) {
static String? parseVideoId(String url) {
if (url.isNullOrWhiteSpace) {
return null;
}
@ -51,19 +51,19 @@ class VideoId with EquatableMixin {
// https://www.youtube.com/watch?v=yIVRs6YSbOM
var regMatch = _regMatchExp.firstMatch(url)?.group(1);
if (!regMatch.isNullOrWhiteSpace && validateVideoId(regMatch)) {
if (!regMatch.isNullOrWhiteSpace && validateVideoId(regMatch!)) {
return regMatch;
}
// https://youtu.be/yIVRs6YSbOM
var shortMatch = _shortMatchExp.firstMatch(url)?.group(1);
if (!shortMatch.isNullOrWhiteSpace && validateVideoId(shortMatch)) {
if (!shortMatch.isNullOrWhiteSpace && validateVideoId(shortMatch!)) {
return shortMatch;
}
// https://www.youtube.com/embed/yIVRs6YSbOM
var embedMatch = _embedMatchExp.firstMatch(url)?.group(1);
if (!embedMatch.isNullOrWhiteSpace && validateVideoId(embedMatch)) {
if (!embedMatch.isNullOrWhiteSpace && validateVideoId(embedMatch!)) {
return embedMatch;
}
return null;

View File

@ -1,26 +1,26 @@
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.8.0-beta.5
version: 1.8.0-nullsafety.1
homepage: https://github.com/Hexer10/youtube_explode_dart
environment:
sdk: '>=2.6.0 <3.0.0'
sdk: '>=2.12.0 <3.0.0'
dependencies:
html: ^0.14.0+3
http: ^0.12.0+4
http_parser: ^3.1.3
xml: '>=3.0.0 <5.0.0'
equatable: ^1.1.0
meta: ^1.1.8
json_annotation: ^3.1.0
collection: ^1.14.13
html: ^0.15.0
http: ^0.13.0
http_parser: ^4.0.0
xml: ^5.0.2
equatable: ^2.0.0
meta: ^1.3.0
json_annotation: ^4.0.0
collection: ^1.15.0
dev_dependencies:
effective_dart: ^1.2.4
effective_dart: ^1.3.0
console: ^3.1.0
test: ^1.12.0
grinder: ^0.8.5
pedantic: ^1.9.2
json_serializable: ^3.5.0
build_runner: ^1.10.4
test: ^1.16.5
grinder: ^0.9.0-nullsafety.0
pedantic: ^1.11.0
json_serializable: ^4.0.2
build_runner: ^1.11.5

View File

@ -11,12 +11,12 @@ void main() {
yt.close();
});
test('Search a youtube video from the api', () async {
test('Search a youtube video from the search page', () async {
var videos = await yt.search.getVideos('undead corporation megalomania');
expect(videos, isNotEmpty);
});
test('Search a youtube video from the search page', () async {
test('Search a youtube video from the search page-2', () async {
var videos = await yt.search
.getVideosFromPage('hello')
.where((e) => e is SearchVideo) // Take only the videos.