diff --git a/analysis_options.yaml b/analysis_options.yaml index 6e1dd93..3f512d8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -15,5 +15,5 @@ linter: - prefer_contains analyzer: -# exclude: -# - path/to/excluded/files/** + exclude: + - example\** diff --git a/lib/src/channel/channel_client.dart b/lib/src/channel/channel_client.dart new file mode 100644 index 0000000..38ccd1a --- /dev/null +++ b/lib/src/channel/channel_client.dart @@ -0,0 +1,3 @@ +class ChannelClient { + ChannelClient._(); +} \ No newline at end of file diff --git a/lib/src/exceptions/fatal_failure_exception.dart b/lib/src/exceptions/fatal_failure_exception.dart new file mode 100644 index 0000000..ce6c6b4 --- /dev/null +++ b/lib/src/exceptions/fatal_failure_exception.dart @@ -0,0 +1,26 @@ +import 'dart:io'; + +import 'package:http/http.dart'; + +import 'youtube_explode_exception.dart'; + +/// Exception thrown when a fatal failure occurs. +class FatalFailureException implements YoutubeExplodeException { + final String message; + + /// Initializes an instance of [FatalFailureException] + FatalFailureException(this.message); + + /// Initializes an instance of [FatalFailureException] with an [HttpRequest] + FatalFailureException.httpRequest(Response response) + : message = ''' +Failed to perform an HTTP request to YouTube due to a fatal failure. +In most cases, this error indicates that YouTube most likely changed something, which broke the library. +If this issue persists, please report it on the project's GitHub page. +Request: ${response.request} +Response: $response +'''; + + @override + String toString() => 'FatalFailureException: $message'; +} diff --git a/lib/src/exceptions/request_limit_exceeded_exception.dart b/lib/src/exceptions/request_limit_exceeded_exception.dart new file mode 100644 index 0000000..edc90f1 --- /dev/null +++ b/lib/src/exceptions/request_limit_exceeded_exception.dart @@ -0,0 +1,25 @@ +import 'package:http/http.dart'; + +import 'youtube_explode_exception.dart'; + +/// Exception thrown when a fatal failure occurs. +class RequestLimitExceeded implements YoutubeExplodeException { + final String message; + + /// Initializes an instance of [FatalFailureException] + RequestLimitExceeded(this.message); + + /// Initializes an instance of [FatalFailureException] with an [HttpRequest] + RequestLimitExceeded.httpRequest(Response response) + : message = ''' +Failed to perform an HTTP request to YouTube because of rate limiting. +This error indicates that YouTube thinks there were too many requests made from this IP and considers it suspicious. +To resolve this error, please wait some time and try again -or- try injecting an HttpClient that has cookies for an authenticated user. +Unfortunately, there's nothing the library can do to work around this error. +Request: ${response.request} +Response: $response +'''; + + @override + String toString() => 'FatalFailureException: $message'; +} diff --git a/lib/src/exceptions/youtube_explode_exception.dart b/lib/src/exceptions/youtube_explode_exception.dart new file mode 100644 index 0000000..e8df27d --- /dev/null +++ b/lib/src/exceptions/youtube_explode_exception.dart @@ -0,0 +1,5 @@ +/// Parent class for domain exceptions thrown by [YoutubeExplode] +abstract class YoutubeExplodeException implements Exception { + final String message; + YoutubeExplodeException(this.message); +} diff --git a/lib/src/extensions/helpers_extension.dart b/lib/src/extensions/helpers_extension.dart index be74c5a..56d7830 100644 --- a/lib/src/extensions/helpers_extension.dart +++ b/lib/src/extensions/helpers_extension.dart @@ -1,4 +1,4 @@ -import '../cipher/cipher_operations.dart'; +import '../reverse_engineering/cipher/cipher_operations.dart'; /// Utility for Strings. extension StringUtility on String { diff --git a/lib/src/cipher/cipher.dart b/lib/src/reverse_engineering/cipher/cipher.dart similarity index 97% rename from lib/src/cipher/cipher.dart rename to lib/src/reverse_engineering/cipher/cipher.dart index 3af8e34..1d78c85 100644 --- a/lib/src/cipher/cipher.dart +++ b/lib/src/reverse_engineering/cipher/cipher.dart @@ -2,8 +2,8 @@ library youtube_explode.cipher; import 'package:http/http.dart' as http; -import '../exceptions/exceptions.dart'; -import '../extensions/helpers_extension.dart'; +import '../../exceptions/exceptions.dart'; +import '../../extensions/helpers_extension.dart'; import 'cipher_operations.dart'; final _deciphererFuncNameExp = RegExp( diff --git a/lib/src/cipher/cipher_operations.dart b/lib/src/reverse_engineering/cipher/cipher_operations.dart similarity index 100% rename from lib/src/cipher/cipher_operations.dart rename to lib/src/reverse_engineering/cipher/cipher_operations.dart diff --git a/lib/src/reverse_engineering/responses/channel_page.dart b/lib/src/reverse_engineering/responses/channel_page.dart new file mode 100644 index 0000000..a424f9d --- /dev/null +++ b/lib/src/reverse_engineering/responses/channel_page.dart @@ -0,0 +1,54 @@ +import 'package:html/dom.dart'; +import 'package:html/parser.dart' as parser; + +class ChannelPage { + final Document _root; + + + bool get isOk => _root.querySelector('meta[property="og:url"]') != null; + + String get channelUrl => _root + .querySelectorThrow('meta[property="og:url"]') + .getAttributeThrow('content'); + + String get channelId => channelId.substringAfter('channel/'); + + String get channelTitle => _root + .querySelectorThrow('meta[property="og:title"]') + .getAttributeThrow('content'); + + String get channelLogoUrl => _root + .querySelectorThrow('meta[property="og:image"]') + .getAttributeThrow('content'); + + ChannelPage(this._root); + + ChannelPage.parse(String raw) : _root = parser.parse(raw); + + static Future hello() {} +} + +extension on Document { + Element querySelectorThrow(String selectors) { + var element = querySelector(selectors); + if (element == null) { + //TODO: throw + } + return element; + } +} + +extension on Element { + String getAttributeThrow(String name) { + var attribute = attributes[name]; + if (attribute == null) { + //TODO: throw + } + return attribute; + } +} + +extension on String { + String substringAfter(String separator) => + substring(indexOf(separator) + length); +} diff --git a/lib/src/reverse_engineering/reverse_engineering.dart b/lib/src/reverse_engineering/reverse_engineering.dart new file mode 100644 index 0000000..6f48309 --- /dev/null +++ b/lib/src/reverse_engineering/reverse_engineering.dart @@ -0,0 +1 @@ +export 'youtube_http_client.dart'; \ No newline at end of file diff --git a/lib/src/reverse_engineering/youtube_http_client.dart b/lib/src/reverse_engineering/youtube_http_client.dart new file mode 100644 index 0000000..eea35d6 --- /dev/null +++ b/lib/src/reverse_engineering/youtube_http_client.dart @@ -0,0 +1,69 @@ +import 'package:http/http.dart'; + +class YoutubeHttpClient { + final Client _httpClient = Client(); + + /// Throws if something is wrong with the response. + void _validateResponse(Request request, int statusCode) { + if (request.url.host.endsWith('.google.com') && + request.url.path.startsWith('/sorry/')) { + //TODO: throw RequestLimitExceededException.FailedHttpRequest(response); + } + + if (statusCode >= 500) { + //TODO: TransientFailureException.FailedHttpRequest(response); + } + + if (statusCode == 429) { + //TODO: throw RequestLimitExceededException.FailedHttpRequest(response); + } + + if (statusCode >= 400) { + //TODO: throw FatalFailureException.FailedHttpRequest(response); + } + } + + Future get(dynamic url, {Map headers}) { + return _httpClient.get(url, headers: headers); + } + + Future head(dynamic url, {Map headers}) { + return _httpClient.head(url, headers: headers); + } + + Future getString(dynamic url, + {Map headers, bool validate = true}) async { + var response = await _httpClient.get(url, headers: headers); + + if (validate) { + _validateResponse(response.request, response.statusCode); + } + + return response.body; + } + + Stream> getStream(dynamic url, + {Map headers, + int from, + int to, + bool validate = true}) async* { + var request = Request('get', url); + request.headers['range'] = 'bytes=$from-$to'; + var response = await request.send(); + if (validate) { + _validateResponse(response.request, response.statusCode); + } + yield* response.stream; + } + + Future getContentLength(dynamic url, + {Map headers, bool validate = true}) async { + var response = await head(url, headers: headers); + + if (validate) { + _validateResponse(response.request, response.statusCode); + } + + return int.parse(response.headers['content-length']); + } +} diff --git a/lib/src/youtube_explode_base.dart b/lib/src/youtube_explode_base.dart index 5d2756b..70f4c5d 100644 --- a/lib/src/youtube_explode_base.dart +++ b/lib/src/youtube_explode_base.dart @@ -11,6 +11,8 @@ import 'extensions/extensions.dart'; import 'models/models.dart'; import 'parser.dart' as parser; +import 'channel/channel_client.dart'; + /// YoutubeExplode entry class. class YoutubeExplode { static final _regMatchExp = RegExp(r'youtube\..+?/watch.*?v=(.*?)(?:&|/|$)'); @@ -24,7 +26,7 @@ class YoutubeExplode { /// HTTP Client. // Visible only for extensions. - http.Client client; + final http.Client client; /// Initialize [YoutubeExplode] class and http client. YoutubeExplode() : client = http.Client();