Null safety migration
This commit is contained in:
parent
5385ba1628
commit
cdca20011f
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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() ?? '';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) ?? ''));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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] == '{') {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @dart=2.9
|
||||
|
||||
// To parse this JSON data, do
|
||||
//
|
||||
// final channelAboutId = channelAboutIdFromJson(jsonString);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @dart=2.9
|
||||
|
||||
// To parse this JSON data, do
|
||||
//
|
||||
// final channelUploadPageId = channelUploadPageIdFromJson(jsonString);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @dart=2.9
|
||||
|
||||
// To parse this JSON data, do
|
||||
//
|
||||
// final playerResponseJson = playerResponseJsonFromJson(jsonString);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @dart=2.9
|
||||
|
||||
// To parse this JSON data, do
|
||||
//
|
||||
// final playerConfigJson = playerConfigJsonFromJson(jsonString);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @dart=2.9
|
||||
|
||||
// To parse this JSON data, do
|
||||
//
|
||||
// final playlistPageId = playlistPageIdFromJson(jsonString);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @dart=2.9
|
||||
|
||||
// To parse this JSON data, do
|
||||
//
|
||||
// final playlistResponseJson = playlistResponseJsonFromJson(jsonString);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @dart=2.9
|
||||
|
||||
// To parse this JSON data, do
|
||||
//
|
||||
// final searchPageId = searchPageIdFromJson(jsonString);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @dart=2.9
|
||||
|
||||
// To parse this JSON data, do
|
||||
//
|
||||
// final watchPageId = watchPageIdFromJson(jsonString);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
32
pubspec.yaml
32
pubspec.yaml
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue