Fix error when the http-client is closed and the request is still running.

This commit is contained in:
vi-k 2021-09-10 19:44:47 +10:00
parent 1df6a2e184
commit 09b28a7beb
18 changed files with 67 additions and 26 deletions

View File

@ -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`.
@ -20,11 +23,11 @@
## 1.10.1
- Fix issue #146: Closed Captions couldn't be extracted anymore.
- Code cleanup.
## 1.10.0
- Fix issue #144: get_video_info was removed from yt.
- Min sdk version now is 2.13.0
- Min sdk version now is 2.13.0
- BREAKING CHANGE: New comments API implementation.
## 1.9.10
@ -45,7 +48,7 @@
## 1.9.6
- Fix comment client.
- Fix issue #130 (ClosedCaptions)
## 1.9.5
- Temporary for issue #130

View File

@ -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';

View File

@ -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.");
}

View File

@ -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;

View File

@ -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);
});

View File

@ -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;

View File

@ -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: {

View File

@ -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);
});

View File

@ -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);

View File

@ -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);

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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']!;

View File

@ -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);
});

View File

@ -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);
}
}

View File

@ -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);