youtube_explode/lib/src/reverse_engineering/youtube_http_client.dart

203 lines
5.9 KiB
Dart
Raw Normal View History

import 'dart:async';
2021-07-21 02:06:02 +02:00
import 'dart:convert';
import 'package:http/http.dart' as http;
2021-04-02 22:56:32 +02:00
import 'package:youtube_explode_dart/src/retry.dart';
2020-04-18 23:22:13 +02:00
2020-06-03 23:02:21 +02:00
import '../exceptions/exceptions.dart';
2021-07-21 02:06:02 +02:00
import '../extensions/helpers_extension.dart';
2020-06-05 20:08:04 +02:00
import '../videos/streams/streams.dart';
2020-06-03 23:02:21 +02:00
2020-09-03 20:49:22 +02:00
/// HttpClient wrapper for YouTube
class YoutubeHttpClient extends http.BaseClient {
2020-09-03 20:49:22 +02:00
final http.Client _httpClient;
2020-04-18 23:22:13 +02:00
final Map<String, String> _defaultHeaders = const {
2020-06-03 23:02:21 +02:00
'user-agent':
2020-10-27 14:44:11 +01:00
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
2021-04-02 22:56:32 +02:00
'cookie': 'CONSENT=YES+cb',
'accept':
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-language': 'accept-language: en-US,en;q=0.9',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'sec-gpc': '1',
'upgrade-insecure-requests': '1'
2020-06-03 23:02:21 +02:00
};
2020-09-03 20:49:22 +02:00
/// Initialize an instance of [YoutubeHttpClient]
2021-03-04 12:20:00 +01:00
YoutubeHttpClient([http.Client? httpClient])
2020-09-03 20:49:22 +02:00
: _httpClient = httpClient ?? http.Client();
2020-04-18 23:22:13 +02:00
/// Throws if something is wrong with the response.
void _validateResponse(http.BaseResponse response, int statusCode) {
2021-03-04 12:20:00 +01:00
var request = response.request!;
2020-04-18 23:22:13 +02:00
if (request.url.host.endsWith('.google.com') &&
request.url.path.startsWith('/sorry/')) {
2020-06-03 23:02:21 +02:00
throw RequestLimitExceededException.httpRequest(response);
2020-04-18 23:22:13 +02:00
}
if (statusCode >= 500) {
2020-06-03 23:02:21 +02:00
throw TransientFailureException.httpRequest(response);
2020-04-18 23:22:13 +02:00
}
if (statusCode == 429) {
2020-06-03 23:02:21 +02:00
throw RequestLimitExceededException.httpRequest(response);
2020-04-18 23:22:13 +02:00
}
if (statusCode >= 400) {
2020-06-03 23:02:21 +02:00
throw FatalFailureException.httpRequest(response);
2020-04-18 23:22:13 +02:00
}
}
2020-07-16 20:02:54 +02:00
///
2020-04-18 23:22:13 +02:00
Future<String> getString(dynamic url,
2021-03-04 12:20:00 +01:00
{Map<String, String> headers = const {}, bool validate = true}) async {
var response = await get(url, headers: headers);
2020-06-22 17:41:39 +02:00
if (validate) {
_validateResponse(response, response.statusCode);
}
return response.body;
}
@override
Future<http.Response> get(dynamic url,
2021-03-04 12:20:00 +01:00
{Map<String, String>? headers = const {}, bool validate = false}) async {
2021-03-11 14:20:10 +01:00
assert(url is String || url is Uri);
if (url is String) {
url = Uri.parse(url);
}
var response = await super.get(url, headers: headers);
if (validate) {
_validateResponse(response, response.statusCode);
}
return response;
}
2021-07-25 14:47:26 +02:00
@override
Future<http.Response> post(Uri url,
{Map<String, String>? headers,
Object? body,
Encoding? encoding,
bool validate = false}) async {
final response =
await super.post(url, headers: headers, body: body, encoding: encoding);
if (validate) {
_validateResponse(response, response.statusCode);
}
return response;
}
2020-07-16 20:02:54 +02:00
///
Future<String> postString(dynamic url,
2021-03-04 12:20:00 +01:00
{Map<String, String>? body,
Map<String, String> headers = const {},
2020-06-13 22:54:53 +02:00
bool validate = true}) async {
2021-06-24 15:23:35 +02:00
assert(url is String || url is Uri);
if (url is String) {
url = Uri.parse(url);
}
var response = await post(url, headers: headers, body: body);
2020-04-18 23:22:13 +02:00
if (validate) {
2020-06-03 23:02:21 +02:00
_validateResponse(response, response.statusCode);
2020-04-18 23:22:13 +02:00
}
return response.body;
}
2020-06-05 20:20:53 +02:00
Stream<List<int>> getStream(StreamInfo streamInfo,
2021-03-04 12:20:00 +01:00
{Map<String, String> headers = const {},
bool validate = true,
int start = 0,
int errorCount = 0}) async* {
2020-06-05 20:20:53 +02:00
var url = streamInfo.url;
2020-12-25 23:29:01 +01:00
var bytesCount = start;
while (bytesCount != streamInfo.size.totalBytes) {
2020-12-25 23:29:01 +01:00
try {
final response = await retry(() {
final request = http.Request('get', url);
request.headers['range'] = 'bytes=$bytesCount-${bytesCount + 9898989 - 1}';
return send(request);
});
2020-12-25 23:29:01 +01:00
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);
2020-12-25 23:29:01 +01:00
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;
}
2020-04-18 23:22:13 +02:00
}
}
2020-07-16 20:02:54 +02:00
///
2021-03-04 12:20:00 +01:00
Future<int?> getContentLength(dynamic url,
{Map<String, String> headers = const {}, bool validate = true}) async {
2020-04-18 23:22:13 +02:00
var response = await head(url, headers: headers);
if (validate) {
2020-06-03 23:02:21 +02:00
_validateResponse(response, response.statusCode);
2020-04-18 23:22:13 +02:00
}
2020-06-05 16:17:08 +02:00
return int.tryParse(response.headers['content-length'] ?? '');
2020-04-18 23:22:13 +02:00
}
2020-06-03 23:02:21 +02:00
2021-07-21 02:06:02 +02:00
/// Sends a call to the youtube api endpoint.
Future<JsonMap> sendPost(String action, String token) async {
assert(action == 'next' || action == 'browse' || action == 'search');
final body = {
'context': const {
'client': {
'hl': 'en',
'clientName': 'WEB',
'clientVersion': '2.20200911.04.00'
}
},
'continuation': token
};
final url = Uri.parse(
'https://www.youtube.com/youtubei/v1/$action?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8');
return retry<JsonMap>(() async {
final raw = await post(url, body: json.encode(body));
return json.decode(raw.body);
});
}
@override
2020-06-03 23:02:21 +02:00
void close() => _httpClient.close();
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
_defaultHeaders.forEach((key, value) {
if (request.headers[key] == null) {
2021-03-04 12:20:00 +01:00
request.headers[key] = _defaultHeaders[key]!;
}
});
// print('Request: $request');
2020-06-27 21:32:03 +02:00
// print('Stack:\n${StackTrace.current}');
return _httpClient.send(request);
}
2020-04-18 23:22:13 +02:00
}