Fix error when the http-client is closed and the request is still running.
This commit is contained in:
parent
1df6a2e184
commit
09b28a7beb
|
@ -1,3 +1,6 @@
|
|||
## 1.10.7
|
||||
- Fix the error of incomplete data loading on the Android emulator.
|
||||
- Fix error when the http-client is closed and the request is still running.
|
||||
## 1.10.6
|
||||
- Implement `Playlist.videoCount`.
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
library youtube_explode.exceptions;
|
||||
|
||||
export 'fatal_failure_exception.dart';
|
||||
export 'http_client_closed.dart';
|
||||
export 'request_limit_exceeded_exception.dart';
|
||||
export 'search_item_section_exception.dart';
|
||||
export 'transient_failure_exception.dart';
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import 'youtube_explode_exception.dart';
|
||||
|
||||
/// An exception is thrown when the http-client is closed
|
||||
/// and the request is still running.
|
||||
class HttpClientClosedException extends YoutubeExplodeException {
|
||||
HttpClientClosedException()
|
||||
: super('The request could not be completed because '
|
||||
"the YoutubeExplode's http-client was closed.");
|
||||
}
|
|
@ -4,11 +4,12 @@ import 'dart:async';
|
|||
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../youtube_explode_dart.dart';
|
||||
import 'exceptions/exceptions.dart';
|
||||
|
||||
/// Run the [function] each time an exception is thrown until the retryCount
|
||||
/// is 0.
|
||||
Future<T> retry<T>(FutureOr<T> Function() function) async {
|
||||
Future<T> retry<T>(YoutubeHttpClient? client, FutureOr<T> Function() function) async {
|
||||
var retryCount = 5;
|
||||
|
||||
// ignore: literal_only_boolean_expressions
|
||||
|
@ -17,6 +18,10 @@ Future<T> retry<T>(FutureOr<T> Function() function) async {
|
|||
return await function();
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} on Exception catch (e) {
|
||||
if (client != null && client.closed) {
|
||||
throw HttpClientClosedException();
|
||||
}
|
||||
|
||||
retryCount -= getExceptionCost(e);
|
||||
if (retryCount <= 0) {
|
||||
rethrow;
|
||||
|
|
|
@ -23,7 +23,7 @@ class ClosedCaptionClient {
|
|||
static Future<ClosedCaptionClient> get(
|
||||
YoutubeHttpClient httpClient, Uri url) {
|
||||
final formatUrl = url.replaceQueryParameters({'fmt': 'srv3'});
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(formatUrl);
|
||||
return ClosedCaptionClient.parse(raw);
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ class CommentsClient {
|
|||
static Future<CommentsClient?> get(
|
||||
YoutubeHttpClient httpClient, Video video) async {
|
||||
final watchPage = video.watchPage ??
|
||||
await retry<WatchPage>(
|
||||
await retry<WatchPage>(httpClient,
|
||||
() async => WatchPage.get(httpClient, video.id.value));
|
||||
|
||||
final continuation = watchPage.commentsContinuation;
|
||||
|
|
|
@ -63,7 +63,7 @@ class EmbeddedPlayerClient {
|
|||
|
||||
final url = Uri.parse('https://www.youtube.com/youtubei/v1/player');
|
||||
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
final raw = await httpClient.post(url,
|
||||
body: json.encode(body),
|
||||
headers: {
|
||||
|
|
|
@ -29,7 +29,7 @@ class DashManifest {
|
|||
|
||||
///
|
||||
static Future<DashManifest> get(YoutubeHttpClient httpClient, dynamic url) {
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
return DashManifest.parse(raw);
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ class ChannelAboutPage extends YoutubePage<_InitialData> {
|
|||
static Future<ChannelAboutPage> get(YoutubeHttpClient httpClient, String id) {
|
||||
var url = 'https://www.youtube.com/channel/$id/about?hl=en';
|
||||
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
var result = ChannelAboutPage.parse(raw);
|
||||
|
||||
|
@ -52,7 +52,7 @@ class ChannelAboutPage extends YoutubePage<_InitialData> {
|
|||
YoutubeHttpClient httpClient, String username) {
|
||||
var url = 'https://www.youtube.com/user/$username/about?hl=en';
|
||||
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
var result = ChannelAboutPage.parse(raw);
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ class ChannelPage extends YoutubePage<_InitialData> {
|
|||
static Future<ChannelPage> get(YoutubeHttpClient httpClient, String id) {
|
||||
var url = 'https://www.youtube.com/channel/$id?hl=en';
|
||||
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
var result = ChannelPage.parse(raw);
|
||||
|
||||
|
@ -56,7 +56,7 @@ class ChannelPage extends YoutubePage<_InitialData> {
|
|||
YoutubeHttpClient httpClient, String username) {
|
||||
var url = 'https://www.youtube.com/user/$username?hl=en';
|
||||
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
var result = ChannelPage.parse(raw);
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ class ChannelUploadPage extends YoutubePage<_InitialData> {
|
|||
YoutubeHttpClient httpClient, String channelId, String sorting) {
|
||||
var url =
|
||||
'https://www.youtube.com/channel/$channelId/videos?view=0&sort=$sorting&flow=grid';
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
return ChannelUploadPage.parse(raw, channelId);
|
||||
});
|
||||
|
|
|
@ -71,7 +71,7 @@ class EmbedPage {
|
|||
static Future<EmbedPage> get(YoutubeHttpClient httpClient, String videoId) {
|
||||
var url = 'https://youtube.com/embed/$videoId?hl=en';
|
||||
// final url = 'http://localhost:8080/embed/$videoId?hl=en';
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
return EmbedPage.parse(raw);
|
||||
});
|
||||
|
|
|
@ -47,7 +47,7 @@ class PlaylistPage extends YoutubePage<_InitialData> {
|
|||
String id,
|
||||
) async {
|
||||
var url = 'https://www.youtube.com/playlist?list=$id&hl=en&persist_hl=1';
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
return PlaylistPage.parse(raw, id);
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ class SearchPage extends YoutubePage<_InitialData> {
|
|||
{SearchFilter filter = const SearchFilter('')}) {
|
||||
var url =
|
||||
'https://www.youtube.com/results?search_query=${Uri.encodeQueryComponent(queryString)}&sp=${filter.value}';
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
return SearchPage.parse(raw, queryString);
|
||||
});
|
||||
|
|
|
@ -119,7 +119,7 @@ class WatchPage extends YoutubePage<_InitialData> {
|
|||
///
|
||||
static Future<WatchPage> get(YoutubeHttpClient httpClient, String videoId) {
|
||||
final url = 'https://youtube.com/watch?v=$videoId&bpctr=9999999999&hl=en';
|
||||
return retry(() async {
|
||||
return retry(httpClient, () async {
|
||||
var req = await httpClient.get(url, validate: true);
|
||||
|
||||
var cookies = req.headers['set-cookie']!;
|
||||
|
|
|
@ -108,7 +108,7 @@ class PlayerSource {
|
|||
static Future<PlayerSource> get(
|
||||
YoutubeHttpClient httpClient, String url) async {
|
||||
if (_cache[url]?.expired ?? true) {
|
||||
var val = await retry(() async {
|
||||
var val = await retry(httpClient, () async {
|
||||
var raw = await httpClient.getString(url);
|
||||
return PlayerSource.parse(raw);
|
||||
});
|
||||
|
|
|
@ -2,17 +2,21 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:youtube_explode_dart/src/retry.dart';
|
||||
|
||||
import '../exceptions/exceptions.dart';
|
||||
import '../extensions/helpers_extension.dart';
|
||||
import '../retry.dart';
|
||||
import '../videos/streams/streams.dart';
|
||||
|
||||
/// HttpClient wrapper for YouTube
|
||||
class YoutubeHttpClient extends http.BaseClient {
|
||||
final http.Client _httpClient;
|
||||
|
||||
final Map<String, String> _defaultHeaders = const {
|
||||
// Flag to interrupt receiving stream.
|
||||
bool _closed = false;
|
||||
bool get closed => _closed;
|
||||
|
||||
static const Map<String, String> _defaultHeaders = {
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
|
||||
'cookie': 'CONSENT=YES+cb',
|
||||
|
@ -33,7 +37,10 @@ class YoutubeHttpClient extends http.BaseClient {
|
|||
|
||||
/// Throws if something is wrong with the response.
|
||||
void _validateResponse(http.BaseResponse response, int statusCode) {
|
||||
if (_closed) return;
|
||||
|
||||
var request = response.request!;
|
||||
|
||||
if (request.url.host.endsWith('.google.com') &&
|
||||
request.url.path.startsWith('/sorry/')) {
|
||||
throw RequestLimitExceededException.httpRequest(response);
|
||||
|
@ -56,6 +63,7 @@ class YoutubeHttpClient extends http.BaseClient {
|
|||
Future<String> getString(dynamic url,
|
||||
{Map<String, String> headers = const {}, bool validate = true}) async {
|
||||
var response = await get(url, headers: headers);
|
||||
if (_closed) throw HttpClientClosedException();
|
||||
|
||||
if (validate) {
|
||||
_validateResponse(response, response.statusCode);
|
||||
|
@ -72,6 +80,8 @@ class YoutubeHttpClient extends http.BaseClient {
|
|||
url = Uri.parse(url);
|
||||
}
|
||||
var response = await super.get(url, headers: headers);
|
||||
if (_closed) throw HttpClientClosedException();
|
||||
|
||||
if (validate) {
|
||||
_validateResponse(response, response.statusCode);
|
||||
}
|
||||
|
@ -86,6 +96,8 @@ class YoutubeHttpClient extends http.BaseClient {
|
|||
bool validate = false}) async {
|
||||
final response =
|
||||
await super.post(url, headers: headers, body: body, encoding: encoding);
|
||||
if (_closed) throw HttpClientClosedException();
|
||||
|
||||
if (validate) {
|
||||
_validateResponse(response, response.statusCode);
|
||||
}
|
||||
|
@ -102,6 +114,7 @@ class YoutubeHttpClient extends http.BaseClient {
|
|||
url = Uri.parse(url);
|
||||
}
|
||||
var response = await post(url, headers: headers, body: body);
|
||||
if (_closed) throw HttpClientClosedException();
|
||||
|
||||
if (validate) {
|
||||
_validateResponse(response, response.statusCode);
|
||||
|
@ -117,9 +130,9 @@ class YoutubeHttpClient extends http.BaseClient {
|
|||
int errorCount = 0}) async* {
|
||||
var url = streamInfo.url;
|
||||
var bytesCount = start;
|
||||
while (bytesCount != streamInfo.size.totalBytes) {
|
||||
while (!_closed && bytesCount != streamInfo.size.totalBytes) {
|
||||
try {
|
||||
final response = await retry(() {
|
||||
final response = await retry(this, () {
|
||||
final request = http.Request('get', url);
|
||||
request.headers['range'] = 'bytes=$bytesCount-${bytesCount + 9898989 - 1}';
|
||||
return send(request);
|
||||
|
@ -134,6 +147,8 @@ class YoutubeHttpClient extends http.BaseClient {
|
|||
}, onError: (_) => null, onDone: stream.close, cancelOnError: false);
|
||||
errorCount = 0;
|
||||
yield* stream.stream;
|
||||
} on HttpClientClosedException {
|
||||
break;
|
||||
} on Exception {
|
||||
if (errorCount == 5) {
|
||||
rethrow;
|
||||
|
@ -153,6 +168,7 @@ class YoutubeHttpClient extends http.BaseClient {
|
|||
Future<int?> getContentLength(dynamic url,
|
||||
{Map<String, String> headers = const {}, bool validate = true}) async {
|
||||
var response = await head(url, headers: headers);
|
||||
if (_closed) throw HttpClientClosedException();
|
||||
|
||||
if (validate) {
|
||||
_validateResponse(response, response.statusCode);
|
||||
|
@ -179,24 +195,31 @@ class YoutubeHttpClient extends http.BaseClient {
|
|||
final url = Uri.parse(
|
||||
'https://www.youtube.com/youtubei/v1/$action?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8');
|
||||
|
||||
return retry<JsonMap>(() async {
|
||||
return retry<JsonMap>(this, () async {
|
||||
final raw = await post(url, body: json.encode(body));
|
||||
if (_closed) throw HttpClientClosedException();
|
||||
|
||||
return json.decode(raw.body);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void close() => _httpClient.close();
|
||||
void close() {
|
||||
_closed = true;
|
||||
_httpClient.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
||||
if (_closed) throw HttpClientClosedException();
|
||||
|
||||
_defaultHeaders.forEach((key, value) {
|
||||
if (request.headers[key] == null) {
|
||||
request.headers[key] = _defaultHeaders[key]!;
|
||||
}
|
||||
});
|
||||
// print('Request: $request');
|
||||
// print('Stack:\n${StackTrace.current}');
|
||||
// print('Stack:\n${StackTrace.current}');
|
||||
return _httpClient.send(request);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ class SearchClient {
|
|||
// ignore: literal_only_boolean_expressions
|
||||
for (;;) {
|
||||
if (page == null) {
|
||||
page = await retry(() async =>
|
||||
page = await retry(_httpClient, () async =>
|
||||
SearchPage.get(_httpClient, searchQuery, filter: filter));
|
||||
} else {
|
||||
page = await page.nextPage(_httpClient);
|
||||
|
|
Loading…
Reference in New Issue