parent
c34ae57ee7
commit
a6f2dcf272
|
@ -1,5 +1,15 @@
|
||||||
include: package:effective_dart/analysis_options.yaml
|
include: package:effective_dart/analysis_options.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude: #most likely not all of these are needed, but as it is now it works.
|
||||||
|
- "**/*.g.dart"
|
||||||
|
- /**/*.g.dart
|
||||||
|
- \**\*.g.dart
|
||||||
|
- "*.g.dart"
|
||||||
|
- "**.g.dart"
|
||||||
|
- example\**
|
||||||
|
- lib\src\reverse_engineering\responses\generated\**
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
- valid_regexps
|
- valid_regexps
|
||||||
|
@ -57,13 +67,3 @@ linter:
|
||||||
- prefer_single_quotes
|
- prefer_single_quotes
|
||||||
- use_function_type_syntax_for_parameters
|
- use_function_type_syntax_for_parameters
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude: #most likely not all of these are needed, but as it is now it works.
|
|
||||||
- "**/*.g.dart"
|
|
||||||
- "**\*.g.dart"
|
|
||||||
- /**/*.g.dart
|
|
||||||
- \**\*.g.dart
|
|
||||||
- "*.g.dart"
|
|
||||||
- "**.g.dart"
|
|
||||||
- example\**
|
|
||||||
- lib\src\reverse_engineering\responses\generated\**
|
|
|
@ -2,11 +2,14 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
var yt = YoutubeExplode();
|
var yt = YoutubeExplode();
|
||||||
var video =
|
|
||||||
await yt.videos.get('https://www.youtube.com/watch?v=AI7ULzgf8RU');
|
|
||||||
|
|
||||||
print('Title: ${video.title}');
|
var manifest = await yt.videos.closedCaptions
|
||||||
|
.getManifest('Pxgvgh9IFqA', autoGenerated: true);
|
||||||
|
print(manifest.tracks);
|
||||||
|
print('\n\n---------------------\n\n');
|
||||||
|
|
||||||
// Close the YoutubeExplode's http client.
|
manifest = await yt.videos.closedCaptions
|
||||||
|
.getManifest('Pxgvgh9IFqA', autoGenerated: false);
|
||||||
|
print(manifest.tracks);
|
||||||
yt.close();
|
yt.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
import 'channel_link.dart';
|
|
||||||
import '../common/thumbnail.dart';
|
import '../common/thumbnail.dart';
|
||||||
|
import 'channel_link.dart';
|
||||||
|
|
||||||
/// YouTube channel's about page metadata.
|
/// YouTube channel's about page metadata.
|
||||||
class ChannelAbout with EquatableMixin {
|
class ChannelAbout with EquatableMixin {
|
||||||
|
|
|
@ -113,3 +113,14 @@ extension GetOrNullMap on Map {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
extension UriUtils on Uri {
|
||||||
|
///
|
||||||
|
Uri replaceQueryParameters(Map<String, String> parameters) {
|
||||||
|
var query = Map<String, String>.from(queryParameters);
|
||||||
|
query.addAll(parameters);
|
||||||
|
|
||||||
|
return replace(queryParameters: query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:html/parser.dart' as parser;
|
import 'package:html/parser.dart' as parser;
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
|
||||||
|
|
||||||
|
import '../../../youtube_explode_dart.dart';
|
||||||
import '../../exceptions/exceptions.dart';
|
import '../../exceptions/exceptions.dart';
|
||||||
|
import '../../extensions/helpers_extension.dart';
|
||||||
import '../../retry.dart';
|
import '../../retry.dart';
|
||||||
import '../youtube_http_client.dart';
|
import '../youtube_http_client.dart';
|
||||||
import 'generated/channel_about_page_id.g.dart';
|
import 'generated/channel_about_page_id.g.dart';
|
||||||
import '../../extensions/helpers_extension.dart';
|
|
||||||
|
|
||||||
///
|
///
|
||||||
class ChannelAboutPage {
|
class ChannelAboutPage {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:xml/xml.dart' as xml;
|
import 'package:xml/xml.dart' as xml;
|
||||||
|
|
||||||
|
import '../../extensions/helpers_extension.dart';
|
||||||
import '../../retry.dart';
|
import '../../retry.dart';
|
||||||
import '../youtube_http_client.dart';
|
import '../youtube_http_client.dart';
|
||||||
|
|
||||||
|
@ -23,21 +24,12 @@ class ClosedCaptionTrackResponse {
|
||||||
///
|
///
|
||||||
static Future<ClosedCaptionTrackResponse> get(
|
static Future<ClosedCaptionTrackResponse> get(
|
||||||
YoutubeHttpClient httpClient, String url) {
|
YoutubeHttpClient httpClient, String url) {
|
||||||
var formatUrl = _setQueryParameters(url, {'format': '3'});
|
var formatUrl = Uri.parse(url).replaceQueryParameters({'fmt': 'srv3'});
|
||||||
return retry(() async {
|
return retry(() async {
|
||||||
var raw = await httpClient.getString(formatUrl);
|
var raw = await httpClient.getString(formatUrl);
|
||||||
return ClosedCaptionTrackResponse.parse(raw);
|
return ClosedCaptionTrackResponse.parse(raw);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Uri _setQueryParameters(String url, Map<String, String> parameters) {
|
|
||||||
var uri = Uri.parse(url);
|
|
||||||
|
|
||||||
var query = Map<String, String>.from(uri.queryParameters);
|
|
||||||
query.addAll(parameters);
|
|
||||||
|
|
||||||
return uri.replace(queryParameters: query);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@ -47,7 +39,7 @@ class ClosedCaption {
|
||||||
Duration _offset;
|
Duration _offset;
|
||||||
Duration _duration;
|
Duration _duration;
|
||||||
Duration _end;
|
Duration _end;
|
||||||
Iterable<ClosedCaptionPart> _parts;
|
List<ClosedCaptionPart> _parts;
|
||||||
|
|
||||||
///
|
///
|
||||||
String get text => _root.text;
|
String get text => _root.text;
|
||||||
|
@ -64,8 +56,8 @@ class ClosedCaption {
|
||||||
Duration get end => _end ??= offset + duration;
|
Duration get end => _end ??= offset + duration;
|
||||||
|
|
||||||
///
|
///
|
||||||
Iterable<ClosedCaptionPart> getParts() =>
|
List<ClosedCaptionPart> getParts() => _parts ??=
|
||||||
_parts ??= _root.findAllElements('s').map((e) => ClosedCaptionPart._(e));
|
_root.findAllElements('s').map((e) => ClosedCaptionPart._(e)).toList();
|
||||||
|
|
||||||
ClosedCaption._(this._root);
|
ClosedCaption._(this._root);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:youtube_explode_dart/src/reverse_engineering/responses/generated/player_response.g.dart';
|
|
||||||
|
|
||||||
import '../../extensions/helpers_extension.dart';
|
import '../../extensions/helpers_extension.dart';
|
||||||
|
import 'generated/player_response.g.dart';
|
||||||
import 'stream_info_provider.dart';
|
import 'stream_info_provider.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
|
@ -2,11 +2,11 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:html/parser.dart' as parser;
|
import 'package:html/parser.dart' as parser;
|
||||||
import 'package:youtube_explode_dart/src/search/base_search_content.dart';
|
|
||||||
|
|
||||||
import '../../../youtube_explode_dart.dart';
|
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/related_query.dart';
|
import '../../search/related_query.dart';
|
||||||
import '../../search/search_video.dart';
|
import '../../search/search_video.dart';
|
||||||
import '../../videos/videos.dart';
|
import '../../videos/videos.dart';
|
||||||
|
|
|
@ -85,53 +85,24 @@ class YoutubeHttpClient extends http.BaseClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
// TODO: Check why isRateLimited is not working.
|
|
||||||
Stream<List<int>> getStream(StreamInfo streamInfo,
|
Stream<List<int>> getStream(StreamInfo streamInfo,
|
||||||
{Map<String, String> headers,
|
{Map<String, String> headers,
|
||||||
bool validate = true,
|
bool validate = true,
|
||||||
int start = 0,
|
int start = 0,
|
||||||
int errorCount = 0}) async* {
|
int errorCount = 0}) async* {
|
||||||
var url = streamInfo.url;
|
var url = streamInfo.url;
|
||||||
// if (!streamInfo.isRateLimited()) {
|
|
||||||
// var request = http.Request('get', url);
|
|
||||||
// request.headers.addAll(_defaultHeaders);
|
|
||||||
// var response = await request.send();
|
|
||||||
// if (validate) {
|
|
||||||
// _validateResponse(response, response.statusCode);
|
|
||||||
// }
|
|
||||||
// yield* response.stream;
|
|
||||||
// } else {
|
|
||||||
|
|
||||||
var bytesCount = start;
|
var query = Map.from(url.queryParameters);
|
||||||
for (var i = start; i < streamInfo.size.totalBytes; i += 9898989) {
|
query['ratebypass'] = 'yes';
|
||||||
try {
|
url = url.replace(queryParameters: query);
|
||||||
final request = http.Request('get', url);
|
|
||||||
request.headers['range'] = 'bytes=$i-${i + 9898989 - 1}';
|
var request = http.Request('get', url);
|
||||||
final response = await send(request);
|
request.headers.addAll(_defaultHeaders);
|
||||||
if (validate) {
|
var response = await request.send();
|
||||||
_validateResponse(response, response.statusCode);
|
if (validate) {
|
||||||
}
|
_validateResponse(response, response.statusCode);
|
||||||
final stream = StreamController<List<int>>();
|
|
||||||
response.stream.listen((data) {
|
|
||||||
bytesCount += data.length;
|
|
||||||
stream.add(data);
|
|
||||||
}, onError: (_) => null, onDone: stream.close, cancelOnError: false);
|
|
||||||
errorCount = 0;
|
|
||||||
yield* stream.stream;
|
|
||||||
} on Exception {
|
|
||||||
if (errorCount == 5) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
yield* getStream(streamInfo,
|
|
||||||
headers: headers,
|
|
||||||
validate: validate,
|
|
||||||
start: bytesCount,
|
|
||||||
errorCount: errorCount + 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// }
|
yield* response.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:youtube_explode_dart/src/reverse_engineering/responses/search_page.dart';
|
|
||||||
|
|
||||||
import '../common/common.dart';
|
import '../common/common.dart';
|
||||||
import '../reverse_engineering/responses/playlist_response.dart';
|
import '../reverse_engineering/responses/playlist_response.dart';
|
||||||
|
import '../reverse_engineering/responses/search_page.dart';
|
||||||
import '../reverse_engineering/youtube_http_client.dart';
|
import '../reverse_engineering/youtube_http_client.dart';
|
||||||
import '../videos/video.dart';
|
import '../videos/video.dart';
|
||||||
import '../videos/video_id.dart';
|
import '../videos/video_id.dart';
|
||||||
|
|
|
@ -32,4 +32,7 @@ class ClosedCaption {
|
||||||
/// Note that some captions may not have any parts at all.
|
/// Note that some captions may not have any parts at all.
|
||||||
ClosedCaptionPart getPartByTime(Duration offset) =>
|
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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
import 'package:xml/xml.dart' as xml;
|
||||||
|
|
||||||
import '../../extensions/helpers_extension.dart';
|
import '../../extensions/helpers_extension.dart';
|
||||||
import '../../reverse_engineering/responses/closed_caption_track_response.dart'
|
import '../../reverse_engineering/responses/responses.dart'
|
||||||
hide ClosedCaption, ClosedCaptionPart;
|
hide ClosedCaption, ClosedCaptionPart, ClosedCaptionTrack;
|
||||||
import '../../reverse_engineering/responses/video_info_response.dart';
|
|
||||||
import '../../reverse_engineering/youtube_http_client.dart';
|
import '../../reverse_engineering/youtube_http_client.dart';
|
||||||
import '../videos.dart';
|
import '../videos.dart';
|
||||||
import 'closed_caption.dart';
|
import 'closed_caption.dart';
|
||||||
|
@ -20,16 +21,57 @@ class ClosedCaptionClient {
|
||||||
|
|
||||||
/// Gets the manifest that contains information
|
/// Gets the manifest that contains information
|
||||||
/// about available closed caption tracks in the specified video.
|
/// about available closed caption tracks in the specified video.
|
||||||
Future<ClosedCaptionManifest> getManifest(dynamic videoId) async {
|
Future<ClosedCaptionManifest> getManifest(dynamic videoId,
|
||||||
|
{bool autoGenerated = false}) async {
|
||||||
videoId = VideoId.fromString(videoId);
|
videoId = VideoId.fromString(videoId);
|
||||||
var videoInfoResponse =
|
var tracks = <ClosedCaptionTrackInfo>[];
|
||||||
await VideoInfoResponse.get(_httpClient, videoId.value);
|
if (!autoGenerated) {
|
||||||
var playerResponse = videoInfoResponse.playerResponse;
|
var subList = await _httpClient.get(
|
||||||
|
'https://video.google.com/timedtext?hl=en&type=list&v=${videoId.value}',
|
||||||
|
validate: true);
|
||||||
|
// ignore: deprecated_member_use
|
||||||
|
var content = xml.parse(subList.body);
|
||||||
|
|
||||||
var tracks = playerResponse.closedCaptionTrack.map((track) =>
|
var langList = <String>[];
|
||||||
ClosedCaptionTrackInfo(Uri.parse(track.url),
|
for (var track in content.findAllElements('track')) {
|
||||||
Language(track.languageCode, track.languageName),
|
var lang = track.getAttribute('lang_code');
|
||||||
isAutoGenerated: track.autoGenerated));
|
if (langList.contains(lang)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
langList.add(lang);
|
||||||
|
for (var ext in ClosedCaptionFormat.values) {
|
||||||
|
tracks.add(ClosedCaptionTrackInfo(
|
||||||
|
Uri.parse('https://www.youtube.com/api/timedtext')
|
||||||
|
.replaceQueryParameters({
|
||||||
|
'lang': lang,
|
||||||
|
'v': videoId.value,
|
||||||
|
'fmt': ext.formatCode,
|
||||||
|
'name': track.getAttribute('name'),
|
||||||
|
}),
|
||||||
|
Language(lang, track.getAttribute('lang_translated')),
|
||||||
|
format: ext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (langList.isEmpty) {
|
||||||
|
return ClosedCaptionManifest([]);
|
||||||
|
}
|
||||||
|
return ClosedCaptionManifest(tracks);
|
||||||
|
} else {
|
||||||
|
var videoInfoResponse =
|
||||||
|
await VideoInfoResponse.get(_httpClient, videoId.value);
|
||||||
|
var playerResponse = videoInfoResponse.playerResponse;
|
||||||
|
|
||||||
|
for (var track in playerResponse.closedCaptionTrack) {
|
||||||
|
for (var ext in ClosedCaptionFormat.values) {
|
||||||
|
tracks.add(ClosedCaptionTrackInfo(
|
||||||
|
Uri.parse(track.url)
|
||||||
|
.replaceQueryParameters({'fmt': ext.formatCode}),
|
||||||
|
Language(track.languageCode, track.languageName),
|
||||||
|
isAutoGenerated: track.autoGenerated,
|
||||||
|
format: ext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return ClosedCaptionManifest(tracks);
|
return ClosedCaptionManifest(tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,62 +88,17 @@ class ClosedCaptionClient {
|
||||||
return ClosedCaptionTrack(captions);
|
return ClosedCaptionTrack(captions);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
/// Auto translated a closed caption track.
|
||||||
Future<String> getSrt(ClosedCaptionTrackInfo trackInfo) async {
|
ClosedCaptionTrackInfo autoTranslate(
|
||||||
var track = await get(trackInfo);
|
ClosedCaptionTrackInfo trackInfo, String lang) {
|
||||||
|
return ClosedCaptionTrackInfo(
|
||||||
var buffer = StringBuffer();
|
trackInfo.url.replaceQueryParameters({'tlang': lang}),
|
||||||
for (var i = 0; i < track.captions.length; i++) {
|
Language(lang, ''),
|
||||||
var caption = track.captions[i];
|
isAutoGenerated: trackInfo.isAutoGenerated,
|
||||||
|
format: trackInfo.format);
|
||||||
// Line number
|
|
||||||
buffer.writeln('${i + 1}');
|
|
||||||
|
|
||||||
// Time start --> time end
|
|
||||||
buffer.write(caption.offset.toSrtFormat());
|
|
||||||
buffer.write(' --> ');
|
|
||||||
buffer.write(caption.end.toSrtFormat());
|
|
||||||
buffer.writeln();
|
|
||||||
|
|
||||||
// Actual text
|
|
||||||
buffer.writeln(caption.text);
|
|
||||||
buffer.writeln();
|
|
||||||
}
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on Duration {
|
|
||||||
String toSrtFormat() {
|
|
||||||
String threeDigits(int n) {
|
|
||||||
if (n >= 1000) {
|
|
||||||
return n.toString().substring(0, 3);
|
|
||||||
}
|
|
||||||
if (n >= 100) {
|
|
||||||
return '$n';
|
|
||||||
}
|
|
||||||
if (n >= 10) {
|
|
||||||
return '0$n';
|
|
||||||
}
|
|
||||||
return '00$n';
|
|
||||||
}
|
|
||||||
|
|
||||||
String twoDigits(int n) {
|
|
||||||
if (n >= 10) {
|
|
||||||
return '$n';
|
|
||||||
}
|
|
||||||
return '0$n';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inMicroseconds < 0) {
|
|
||||||
return '-${-this}';
|
|
||||||
}
|
|
||||||
var twoDigitHours = twoDigits(inHours);
|
|
||||||
var twoDigitMinutes =
|
|
||||||
twoDigits(inMinutes.remainder(Duration.minutesPerHour));
|
|
||||||
var twoDigitSeconds =
|
|
||||||
twoDigits(inSeconds.remainder(Duration.secondsPerMinute));
|
|
||||||
var fourDigitsUs = threeDigits(inMilliseconds.remainder(1000));
|
|
||||||
return '$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds,$fourDigitsUs';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the subtitles as a string.
|
||||||
|
Future<String> getSubTitles(ClosedCaptionTrackInfo trackInfo) =>
|
||||||
|
_httpClient.getString(trackInfo.url);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,13 @@ class ClosedCaptionTrackInfo extends Equatable {
|
||||||
/// Whether the associated track was automatically generated.
|
/// Whether the associated track was automatically generated.
|
||||||
final bool isAutoGenerated;
|
final bool isAutoGenerated;
|
||||||
|
|
||||||
|
/// Track format
|
||||||
|
final ClosedCaptionFormat format;
|
||||||
|
|
||||||
/// Initializes an instance of [ClosedCaptionTrackInfo]
|
/// Initializes an instance of [ClosedCaptionTrackInfo]
|
||||||
const ClosedCaptionTrackInfo(this.url, this.language, {this.isAutoGenerated});
|
const ClosedCaptionTrackInfo(this.url, this.language,
|
||||||
|
{this.isAutoGenerated = false, this.format})
|
||||||
|
: assert(format != null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'CC Track ($language)';
|
String toString() => 'CC Track ($language)';
|
||||||
|
@ -22,3 +27,29 @@ class ClosedCaptionTrackInfo extends Equatable {
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [url, language, isAutoGenerated];
|
List<Object> get props => [url, language, isAutoGenerated];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// SubTiles format.
|
||||||
|
class ClosedCaptionFormat {
|
||||||
|
/// .srv format(1).
|
||||||
|
static const ClosedCaptionFormat srv1 = ClosedCaptionFormat._('srv1');
|
||||||
|
|
||||||
|
/// .srv format(2).
|
||||||
|
static const ClosedCaptionFormat srv2 = ClosedCaptionFormat._('srv2');
|
||||||
|
|
||||||
|
/// .srv format(3).
|
||||||
|
static const ClosedCaptionFormat srv3 = ClosedCaptionFormat._('srv3');
|
||||||
|
|
||||||
|
/// .ttml format.
|
||||||
|
static const ClosedCaptionFormat ttml = ClosedCaptionFormat._('ttml');
|
||||||
|
|
||||||
|
/// .vtt format.
|
||||||
|
static const ClosedCaptionFormat vtt = ClosedCaptionFormat._('vtt');
|
||||||
|
|
||||||
|
/// List of all sub titles format.
|
||||||
|
static const List<ClosedCaptionFormat> values = [srv1, srv2, srv3, ttml, vtt];
|
||||||
|
|
||||||
|
/// Format code as string.
|
||||||
|
final String formatCode;
|
||||||
|
|
||||||
|
const ClosedCaptionFormat._(this.formatCode);
|
||||||
|
}
|
||||||
|
|
|
@ -24,14 +24,6 @@ abstract class StreamInfo {
|
||||||
StreamInfo(this.tag, this.url, this.container, this.size, this.bitrate);
|
StreamInfo(this.tag, this.url, this.container, this.size, this.bitrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extensions for [StreamInfo].
|
|
||||||
extension StreamInfoExt on StreamInfo {
|
|
||||||
static final _exp = RegExp('ratebypass[=/]yes');
|
|
||||||
|
|
||||||
/// Returns true if this video is rate limited.
|
|
||||||
bool isRateLimited() => _exp.hasMatch(url.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
||||||
|
|
|
@ -71,7 +71,7 @@ class VideoClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a [Video] instance from a [videoId]
|
/// Get a [Video] instance from a [videoId]
|
||||||
Future<Video> get(dynamic videoId, {forceWatchPage = false}) async {
|
Future<Video> get(dynamic videoId, {bool forceWatchPage = false}) async {
|
||||||
videoId = VideoId.fromString(videoId);
|
videoId = VideoId.fromString(videoId);
|
||||||
|
|
||||||
if (forceWatchPage) {
|
if (forceWatchPage) {
|
||||||
|
|
|
@ -19,17 +19,16 @@ void main() {
|
||||||
|
|
||||||
test('Search a youtube videos from the search page', () async {
|
test('Search a youtube videos from the search page', () async {
|
||||||
// ignore: deprecated_member_use_from_same_package
|
// ignore: deprecated_member_use_from_same_package
|
||||||
var searchQuery = await yt.search.queryFromPage(
|
var searchQuery = await yt.search.queryFromPage('hello');
|
||||||
'hello');
|
|
||||||
expect(searchQuery.content, isNotEmpty);
|
expect(searchQuery.content, isNotEmpty);
|
||||||
expect(searchQuery.relatedVideos, isNotEmpty);
|
expect(searchQuery.relatedVideos, isNotEmpty);
|
||||||
expect(searchQuery.relatedQueries, isNotEmpty);
|
expect(searchQuery.relatedQueries, isNotEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Search with no results', () async {
|
test('Search with no results', () async {
|
||||||
// ignore: deprecated_member_use_from_same_package
|
var query =
|
||||||
var query = await yt.search.queryFromPage(
|
// ignore: deprecated_member_use_from_same_package
|
||||||
'g;jghEOGHJeguEPOUIhjegoUEHGOGHPSASG');
|
await yt.search.queryFromPage('g;jghEOGHJeguEPOUIhjegoUEHGOGHPSASG');
|
||||||
expect(query.content, isEmpty);
|
expect(query.content, isEmpty);
|
||||||
expect(query.relatedQueries, isEmpty);
|
expect(query.relatedQueries, isEmpty);
|
||||||
expect(query.relatedVideos, isEmpty);
|
expect(query.relatedVideos, isEmpty);
|
||||||
|
@ -39,8 +38,7 @@ void main() {
|
||||||
|
|
||||||
test('Search youtube videos have thumbnails', () async {
|
test('Search youtube videos have thumbnails', () async {
|
||||||
// ignore: deprecated_member_use_from_same_package
|
// ignore: deprecated_member_use_from_same_package
|
||||||
var searchQuery = await yt.search.queryFromPage(
|
var searchQuery = await yt.search.queryFromPage('hello');
|
||||||
'hello');
|
|
||||||
expect(searchQuery.content.first, isA<SearchVideo>());
|
expect(searchQuery.content.first, isA<SearchVideo>());
|
||||||
|
|
||||||
var video = searchQuery.content.first as SearchVideo;
|
var video = searchQuery.content.first as SearchVideo;
|
||||||
|
|
Loading…
Reference in New Issue