From 834a79672abb91c1c9baa075184611078c3fcbec Mon Sep 17 00:00:00 2001 From: Kylart Date: Tue, 3 Oct 2023 11:48:57 +0200 Subject: [PATCH] Replaced consumet data provider by consumet.ts implementation moved to Dart. This is done because of https://github.com/consumet/api.consumet.org/issues/486 --- lib/data/consumet/consumet.dart | 1 - lib/data/consumet/extractors/extractors.dart | 2 + lib/data/consumet/extractors/gogocdn.dart | 163 +++++++++++++++ lib/data/consumet/extractors/streamsb.dart | 81 ++++++++ lib/data/consumet/models/anime/anime.dart | 4 - .../anime_info_response.dart | 107 ---------- .../anime/anime_info_response/episode.dart | 41 ---- .../consumet/models/anime/anime_provider.dart | 28 --- .../anime_search_response.dart | 54 ----- .../anime_search_response_result.dart | 68 ------- .../anime_watch_reponse.dart | 50 ----- .../anime/anime_watch_reponse/headers.dart | 62 ------ .../anime/anime_watch_reponse/source.dart | 41 ---- lib/data/consumet/models/anime_episode.dart | 23 +++ lib/data/consumet/models/anime_result.dart | 26 +++ lib/data/consumet/models/anime_source.dart | 23 +++ lib/data/consumet/models/errors/anime.dart | 46 ----- lib/data/consumet/models/errors/errors.dart | 1 - .../models/exceptions/exceptions.dart | 1 + lib/data/consumet/models/extractor.dart | 5 + lib/data/consumet/models/models.dart | 37 +++- lib/data/consumet/models/video_source.dart | 31 +++ lib/data/consumet/providers/animefox.dart | 51 ----- lib/data/consumet/providers/animepahe.dart | 50 ----- lib/data/consumet/providers/gogoanime.dart | 190 ++++++++++++++---- lib/data/consumet/providers/providers.dart | 3 - lib/data/consumet/providers/zoro.dart | 51 ----- lib/data/consumet/utils/utils.dart | 2 + pubspec.lock | 16 ++ pubspec.yaml | 1 + 30 files changed, 563 insertions(+), 696 deletions(-) create mode 100644 lib/data/consumet/extractors/extractors.dart create mode 100644 lib/data/consumet/extractors/gogocdn.dart create mode 100644 lib/data/consumet/extractors/streamsb.dart delete mode 100644 lib/data/consumet/models/anime/anime.dart delete mode 100644 lib/data/consumet/models/anime/anime_info_response/anime_info_response.dart delete mode 100644 lib/data/consumet/models/anime/anime_info_response/episode.dart delete mode 100644 lib/data/consumet/models/anime/anime_provider.dart delete mode 100644 lib/data/consumet/models/anime/anime_search_response/anime_search_response.dart delete mode 100644 lib/data/consumet/models/anime/anime_search_response/anime_search_response_result.dart delete mode 100644 lib/data/consumet/models/anime/anime_watch_reponse/anime_watch_reponse.dart delete mode 100644 lib/data/consumet/models/anime/anime_watch_reponse/headers.dart delete mode 100644 lib/data/consumet/models/anime/anime_watch_reponse/source.dart create mode 100644 lib/data/consumet/models/anime_episode.dart create mode 100644 lib/data/consumet/models/anime_result.dart create mode 100644 lib/data/consumet/models/anime_source.dart delete mode 100644 lib/data/consumet/models/errors/anime.dart delete mode 100644 lib/data/consumet/models/errors/errors.dart create mode 100644 lib/data/consumet/models/exceptions/exceptions.dart create mode 100644 lib/data/consumet/models/extractor.dart create mode 100644 lib/data/consumet/models/video_source.dart delete mode 100644 lib/data/consumet/providers/animefox.dart delete mode 100644 lib/data/consumet/providers/animepahe.dart delete mode 100644 lib/data/consumet/providers/zoro.dart create mode 100644 lib/data/consumet/utils/utils.dart diff --git a/lib/data/consumet/consumet.dart b/lib/data/consumet/consumet.dart index b352c5be..8185d2c7 100644 --- a/lib/data/consumet/consumet.dart +++ b/lib/data/consumet/consumet.dart @@ -1,2 +1 @@ export 'providers/providers.dart'; -export 'models/models.dart'; diff --git a/lib/data/consumet/extractors/extractors.dart b/lib/data/consumet/extractors/extractors.dart new file mode 100644 index 00000000..0e1ba5f8 --- /dev/null +++ b/lib/data/consumet/extractors/extractors.dart @@ -0,0 +1,2 @@ +export 'gogocdn.dart'; +export 'streamsb.dart'; diff --git a/lib/data/consumet/extractors/gogocdn.dart b/lib/data/consumet/extractors/gogocdn.dart new file mode 100644 index 00000000..81922a53 --- /dev/null +++ b/lib/data/consumet/extractors/gogocdn.dart @@ -0,0 +1,163 @@ +import 'dart:convert'; + +import 'package:encrypt/encrypt.dart'; +import 'package:html/dom.dart'; +import 'package:html/parser.dart'; +import 'package:http/http.dart'; + +import '../models/models.dart'; + +class CDNKeys { + CDNKeys({ + required this.key, + required this.secondKey, + required this.iv, + }); + + final Key key; + final Key secondKey; + final IV iv; +} + +/// Adapted from `https://github.com/consumet/consumet.ts/blob/master/src/extractors/gogocdn.ts` +class GogoCDN extends Extractor { + final client = Client(); + final serverName = 'goload'; + + final _keys = CDNKeys( + key: Key.fromUtf8('37911490979715163134003223491201'), + secondKey: Key.fromUtf8('54674138327930866480207815084989'), + iv: IV.fromUtf8('3134003223491201'), + ); + + var referrer = ''; + + @override + Future> extract(Uri uri) async { + referrer = uri.toString(); + + final List results = []; + + final page = await client.get(uri); + final document = parse(page.body); + + final encyptedParams = + _generateEncryptedAjaxParams(document, uri.queryParameters['id'] ?? ''); + + final encryptedData = await client.get( + Uri( + scheme: uri.scheme, + host: uri.host, + path: 'encrypt-ajax.php', + query: encyptedParams, + ), + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + ); + + final decryptedData = _decryptAjaxData( + json.decode(encryptedData.body)['data'], + ); + if (decryptedData['source'] == null) throw NoEpisodeSourceException; + + final String? url = decryptedData['source'][0]['file']; + + if (url?.contains('.m3u8') == true) { + final resResult = await client.get(Uri.parse(url!)); + final regexp = RegExp(r'(RESOLUTION=)(.*)(\s*?)(\s*.*)'); + final resolutions = regexp.allMatches(resResult.body); + final baseUrl = url.substring(0, url.lastIndexOf('/')); + + for (final resolution in resolutions) { + final res = resolution[0] as String; + + results.add( + VideoSource( + url: '$baseUrl/${res.split('\n')[1]}', + isM3U8: (baseUrl + res.split('\n')[1]).contains('.m3u8'), + quality: '${res.split('\n')[0].split('x')[1].split(',')[0]}p', + ), + ); + } + + decryptedData['source'].forEach((dynamic source) { + results.add( + VideoSource( + url: url, + isM3U8: source['file'].contains('.m3u8'), + quality: 'default', + ), + ); + }); + } else { + decryptedData['source'].forEach((dynamic source) { + results.add( + VideoSource( + url: source['file'], + isM3U8: source['file'].contains('.m3u8'), + quality: source.label.split(' ')[0] + 'p', + ), + ); + }); + } + + decryptedData['source_bk'].forEach((dynamic source) { + results.add( + VideoSource( + url: source['file'], + isM3U8: source['file'].contains('.m3u8'), + quality: 'backup', + ), + ); + }); + + return results; + } + + String _generateEncryptedAjaxParams( + Document document, + String id, + ) { + final encrypter = Encrypter( + AES( + _keys.key, + mode: AESMode.cbc, + ), + ); + + final encryptedKey = encrypter.encrypt( + id, + iv: _keys.iv, + ); + + final scriptValue = document + .querySelector("script[data-name='episode']") + ?.attributes["data-value"]; + + if (scriptValue == null) throw NoEpisodeSourceException; + + final decryptedToken = encrypter.decrypt( + Encrypted.from64(scriptValue), + iv: _keys.iv, + ); + + return 'id=${encryptedKey.base64}&alias=$decryptedToken'; + } + + Map _decryptAjaxData(String encryptedData) { + final encrypter = Encrypter( + AES( + _keys.secondKey, + mode: AESMode.cbc, + ), + ); + + final decryptedData = encrypter.decrypt( + Encrypted.from64(encryptedData), + iv: _keys.iv, + ); + + return json.decode(decryptedData); + } +} diff --git a/lib/data/consumet/extractors/streamsb.dart b/lib/data/consumet/extractors/streamsb.dart new file mode 100644 index 00000000..9f53d8a1 --- /dev/null +++ b/lib/data/consumet/extractors/streamsb.dart @@ -0,0 +1,81 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + +import '../models/models.dart'; +import '../utils/utils.dart'; + +/// Adapted from `https://github.com/consumet/consumet.ts/blob/master/src/extractors/streamsb.ts` +class StreamSB extends Extractor { + final host = 'https://streamsss.net/sources50'; + final host2 = 'https://watchsb.com/sources50'; + + final client = Client(); + + String payload(String hex) => + '566d337678566f743674494a7c7c${hex}7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362'; + + @override + Future> extract(Uri uri) async { + final List results = []; + Map headers = { + 'watchsb': 'sbstream', + 'User-Agent': userAgent, + 'Referer': uri.toString(), + }; + + var id = uri.toString().split('/e/').lastOrNull; + if (id?.contains('html') == true) id = id?.split('.html').firstOrNull; + + if (id == null) throw 'cannot find ID'; + + final res = await client.get( + Uri.parse( + '$host/${payload(utf8.encode(id).map((e) => e.toRadixString(16)).join())}', + ), + headers: headers, + ); + final jsonResponse = json.decode(res.body); + + if (jsonResponse['stream_data'] == null) { + throw 'No source found. Try a different server.'; + } + + headers = { + 'User-Agent': userAgent, + 'Referer': uri.toString().split('e/').first, + }; + + final m3u8Urls = await client.get( + Uri.parse(jsonResponse['stream_data']['file']), + headers: headers, + ); + + final videoList = m3u8Urls.body.split('#EXT-X-STREAM-INF:'); + + for (final video in videoList) { + if (!video.contains('m3u8')) continue; + + final url = video.split('\n')[1]; + final quality = video.split('RESOLUTION=')[1].split(',')[0].split('x')[1]; + + results.add( + VideoSource( + url: url, + quality: '${quality}p', + isM3U8: true, + ), + ); + } + + results.add( + VideoSource( + url: jsonResponse['stream_data']['file'], + quality: 'auto', + isM3U8: jsonResponse['stream_data']['file'].contains('.m3u8'), + ), + ); + + return results; + } +} diff --git a/lib/data/consumet/models/anime/anime.dart b/lib/data/consumet/models/anime/anime.dart deleted file mode 100644 index ccd3bfe5..00000000 --- a/lib/data/consumet/models/anime/anime.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'anime_provider.dart'; -export 'anime_info_response/anime_info_response.dart'; -export 'anime_search_response/anime_search_response.dart'; -export 'anime_watch_reponse/anime_watch_reponse.dart'; diff --git a/lib/data/consumet/models/anime/anime_info_response/anime_info_response.dart b/lib/data/consumet/models/anime/anime_info_response/anime_info_response.dart deleted file mode 100644 index e6f6338b..00000000 --- a/lib/data/consumet/models/anime/anime_info_response/anime_info_response.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; - -import 'episode.dart'; - -class AnimeInfoResponse extends Equatable { - final String? id; - final String? title; - final String? url; - final List? genres; - final int? totalEpisodes; - final String? image; - final String? releaseDate; - final String? description; - final String? subOrDub; - final String? type; - final String? status; - final String? otherName; - final List? episodes; - - const AnimeInfoResponse({ - this.id, - this.title, - this.url, - this.genres, - this.totalEpisodes, - this.image, - this.releaseDate, - this.description, - this.subOrDub, - this.type, - this.status, - this.otherName, - this.episodes, - }); - - factory AnimeInfoResponse.fromMap(Map data) { - return AnimeInfoResponse( - id: data['id'] as String?, - title: data['title'] as String?, - url: data['url'] as String?, - genres: data['genres'] as List?, - totalEpisodes: data['totalEpisodes'] as int?, - image: data['image'] as String?, - releaseDate: data['releaseDate'] as String?, - description: data['description'] as String?, - subOrDub: data['subOrDub'] as String?, - type: data['type'] as String?, - status: data['status'] as String?, - otherName: data['otherName'] as String?, - episodes: (data['episodes'] as List?) - ?.map((e) => Episode.fromMap(e as Map)) - .toList(), - ); - } - - Map toMap() => { - 'id': id, - 'title': title, - 'url': url, - 'genres': genres, - 'totalEpisodes': totalEpisodes, - 'image': image, - 'releaseDate': releaseDate, - 'description': description, - 'subOrDub': subOrDub, - 'type': type, - 'status': status, - 'otherName': otherName, - 'episodes': episodes?.map((e) => e.toMap()).toList(), - }; - - /// `dart:convert` - /// - /// Parses the string and returns the resulting Json object as [AnimeInfoResponse]. - factory AnimeInfoResponse.fromJson(String data) { - return AnimeInfoResponse.fromMap(json.decode(data) as Map); - } - - /// `dart:convert` - /// - /// Converts [AnimeInfoResponse] to a JSON string. - String toJson() => json.encode(toMap()); - - @override - bool get stringify => true; - - @override - List get props { - return [ - id, - title, - url, - genres, - totalEpisodes, - image, - releaseDate, - description, - subOrDub, - type, - status, - otherName, - episodes, - ]; - } -} diff --git a/lib/data/consumet/models/anime/anime_info_response/episode.dart b/lib/data/consumet/models/anime/anime_info_response/episode.dart deleted file mode 100644 index d87eb6b4..00000000 --- a/lib/data/consumet/models/anime/anime_info_response/episode.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; - -class Episode extends Equatable { - final String? id; - final double? number; - final String? url; - - const Episode({this.id, this.number, this.url}); - - factory Episode.fromMap(Map data) => Episode( - id: data['id'] as String?, - number: (data['number'] as num?)?.toDouble(), - url: data['url'] as String?, - ); - - Map toMap() => { - 'id': id, - 'number': number, - 'url': url, - }; - - /// `dart:convert` - /// - /// Parses the string and returns the resulting Json object as [Episode]. - factory Episode.fromJson(String data) { - return Episode.fromMap(json.decode(data) as Map); - } - - /// `dart:convert` - /// - /// Converts [Episode] to a JSON string. - String toJson() => json.encode(toMap()); - - @override - bool get stringify => true; - - @override - List get props => [id, number, url]; -} diff --git a/lib/data/consumet/models/anime/anime_provider.dart b/lib/data/consumet/models/anime/anime_provider.dart deleted file mode 100644 index 6e498a9d..00000000 --- a/lib/data/consumet/models/anime/anime_provider.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:http/http.dart'; - -import 'anime_info_response/anime_info_response.dart'; -import 'anime_search_response/anime_search_response_result.dart'; -import 'anime_watch_reponse/anime_watch_reponse.dart'; - -enum AnimeProviderName { - animefox, - animepahe, - gogoanime, - zoro, - unknown, -} - -abstract class AnimeProvider { - AnimeProvider({ - required this.providerName, - }); - - final Client client = Client(); - final AnimeProviderName providerName; - - String get baseUrl => 'https://api.consumet.org/anime/${providerName.name}'; - - Future> search(String term); - Future info(String id); - Future watch(String id); -} diff --git a/lib/data/consumet/models/anime/anime_search_response/anime_search_response.dart b/lib/data/consumet/models/anime/anime_search_response/anime_search_response.dart deleted file mode 100644 index 9b398c97..00000000 --- a/lib/data/consumet/models/anime/anime_search_response/anime_search_response.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; - -import 'anime_search_response_result.dart'; -export 'anime_search_response_result.dart'; - -class AnimeSearchResponse extends Equatable { - final String? currentPage; - final bool? hasNextPage; - final List? results; - - const AnimeSearchResponse({ - this.currentPage, - this.hasNextPage, - this.results, - }); - - factory AnimeSearchResponse.fromMap(Map data) { - return AnimeSearchResponse( - currentPage: data['currentPage'] as String?, - hasNextPage: data['hasNextPage'] as bool?, - results: (data['results'] as List?) - ?.map((e) => - AnimeSearchResponseResult.fromMap(e as Map)) - .toList(), - ); - } - - Map toMap() => { - 'currentPage': currentPage, - 'hasNextPage': hasNextPage, - 'results': results?.map((e) => e.toMap()).toList(), - }; - - /// `dart:convert` - /// - /// Parses the string and returns the resulting Json object as [AnimeSearchResponse]. - factory AnimeSearchResponse.fromJson(String data) { - return AnimeSearchResponse.fromMap( - json.decode(data) as Map); - } - - /// `dart:convert` - /// - /// Converts [AnimeSearchResponse] to a JSON string. - String toJson() => json.encode(toMap()); - - @override - bool get stringify => true; - - @override - List get props => [currentPage, hasNextPage, results]; -} diff --git a/lib/data/consumet/models/anime/anime_search_response/anime_search_response_result.dart b/lib/data/consumet/models/anime/anime_search_response/anime_search_response_result.dart deleted file mode 100644 index 68db9797..00000000 --- a/lib/data/consumet/models/anime/anime_search_response/anime_search_response_result.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; - -class AnimeSearchResponseResult extends Equatable { - final String? id; - final String? title; - final String? url; - final String? image; - final String? releaseDate; - final String? subOrDub; - - const AnimeSearchResponseResult({ - this.id, - this.title, - this.url, - this.image, - this.releaseDate, - this.subOrDub, - }); - - factory AnimeSearchResponseResult.fromMap(Map data) => - AnimeSearchResponseResult( - id: data['id'] as String?, - title: data['title'] as String?, - url: data['url'] as String?, - image: data['image'] as String?, - releaseDate: data['releaseDate'] as String?, - subOrDub: data['subOrDub'] as String?, - ); - - Map toMap() => { - 'id': id, - 'title': title, - 'url': url, - 'image': image, - 'releaseDate': releaseDate, - 'subOrDub': subOrDub, - }; - - /// `dart:convert` - /// - /// Parses the string and returns the resulting Json object as [AnimeSearchResponseResult]. - factory AnimeSearchResponseResult.fromJson(String data) { - return AnimeSearchResponseResult.fromMap( - json.decode(data) as Map); - } - - /// `dart:convert` - /// - /// Converts [AnimeSearchResponseResult] to a JSON string. - String toJson() => json.encode(toMap()); - - @override - bool get stringify => true; - - @override - List get props { - return [ - id, - title, - url, - image, - releaseDate, - subOrDub, - ]; - } -} diff --git a/lib/data/consumet/models/anime/anime_watch_reponse/anime_watch_reponse.dart b/lib/data/consumet/models/anime/anime_watch_reponse/anime_watch_reponse.dart deleted file mode 100644 index 093c7668..00000000 --- a/lib/data/consumet/models/anime/anime_watch_reponse/anime_watch_reponse.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; - -import 'headers.dart'; -import 'source.dart'; - -class AnimeWatchReponse extends Equatable { - final Headers? headers; - final List? sources; - final String? download; - - const AnimeWatchReponse({this.headers, this.sources, this.download}); - - factory AnimeWatchReponse.fromMap(Map data) { - return AnimeWatchReponse( - headers: data['headers'] == null - ? null - : Headers.fromMap(data['headers'] as Map), - sources: (data['sources'] as List?) - ?.map((e) => Source.fromMap(e as Map)) - .toList(), - download: data['download'] as String?, - ); - } - - Map toMap() => { - 'headers': headers?.toMap(), - 'sources': sources?.map((e) => e.toMap()).toList(), - 'download': download, - }; - - /// `dart:convert` - /// - /// Parses the string and returns the resulting Json object as [AnimeWatchReponse]. - factory AnimeWatchReponse.fromJson(String data) { - return AnimeWatchReponse.fromMap(json.decode(data) as Map); - } - - /// `dart:convert` - /// - /// Converts [AnimeWatchReponse] to a JSON string. - String toJson() => json.encode(toMap()); - - @override - bool get stringify => true; - - @override - List get props => [headers, sources, download]; -} diff --git a/lib/data/consumet/models/anime/anime_watch_reponse/headers.dart b/lib/data/consumet/models/anime/anime_watch_reponse/headers.dart deleted file mode 100644 index 033d753e..00000000 --- a/lib/data/consumet/models/anime/anime_watch_reponse/headers.dart +++ /dev/null @@ -1,62 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; - -class Headers extends Equatable { - final String? referer; - final String? watchsb; - final String? userAgent; - - const Headers({ - this.referer, - this.watchsb, - this.userAgent, - }); - - factory Headers.fromMap(Map map) { - return Headers( - referer: map['referer'] != null ? map['referer'] as String : null, - watchsb: map['watchsb'] != null ? map['watchsb'] as String : null, - userAgent: map['User-Agent'] != null ? map['User-Agent'] as String : null, - ); - } - - Map toMap() { - return { - 'referer': referer, - 'watchsb': watchsb, - 'userAgent': userAgent, - }; - } - - factory Headers.fromJson(String source) => - Headers.fromMap(json.decode(source) as Map); - - /// `dart:convert` - /// - /// Converts [Headers] to a JSON string. - String toJson() => json.encode(toMap()); - - @override - bool get stringify => true; - - @override - List get props => [ - referer, - watchsb, - userAgent, - ]; - - Headers copyWith({ - String? referer, - String? watchsb, - String? userAgent, - }) { - return Headers( - referer: referer ?? this.referer, - watchsb: watchsb ?? this.watchsb, - userAgent: userAgent ?? this.userAgent, - ); - } -} diff --git a/lib/data/consumet/models/anime/anime_watch_reponse/source.dart b/lib/data/consumet/models/anime/anime_watch_reponse/source.dart deleted file mode 100644 index 8073dcce..00000000 --- a/lib/data/consumet/models/anime/anime_watch_reponse/source.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; - -class Source extends Equatable { - final String? url; - final bool? isM3U8; - final String? quality; - - const Source({this.url, this.isM3U8, this.quality}); - - factory Source.fromMap(Map data) => Source( - url: data['url'] as String?, - isM3U8: data['isM3U8'] as bool?, - quality: data['quality'] as String?, - ); - - Map toMap() => { - 'url': url, - 'isM3U8': isM3U8, - 'quality': quality, - }; - - /// `dart:convert` - /// - /// Parses the string and returns the resulting Json object as [Source]. - factory Source.fromJson(String data) { - return Source.fromMap(json.decode(data) as Map); - } - - /// `dart:convert` - /// - /// Converts [Source] to a JSON string. - String toJson() => json.encode(toMap()); - - @override - bool get stringify => true; - - @override - List get props => [url, isM3U8, quality]; -} diff --git a/lib/data/consumet/models/anime_episode.dart b/lib/data/consumet/models/anime_episode.dart new file mode 100644 index 00000000..f107be69 --- /dev/null +++ b/lib/data/consumet/models/anime_episode.dart @@ -0,0 +1,23 @@ +part of 'models.dart'; + +class AnimeEpisode extends Equatable { + const AnimeEpisode({ + required this.number, + required this.id, + required this.url, + }); + + final int? number; + final String? id; + final String? url; + + @override + List get props => [ + number, + id, + url, + ]; + + @override + bool get stringify => true; +} diff --git a/lib/data/consumet/models/anime_result.dart b/lib/data/consumet/models/anime_result.dart new file mode 100644 index 00000000..b8389091 --- /dev/null +++ b/lib/data/consumet/models/anime_result.dart @@ -0,0 +1,26 @@ +part of 'models.dart'; + +class AnimeResult extends Equatable { + const AnimeResult({ + this.id, + this.title, + this.url, + this.subOrDub = SubOrDub.sub, + }); + + final String? id; + final String? title; + final String? url; + final SubOrDub subOrDub; + + @override + List get props => [ + id, + title, + url, + subOrDub, + ]; + + @override + bool get stringify => true; +} diff --git a/lib/data/consumet/models/anime_source.dart b/lib/data/consumet/models/anime_source.dart new file mode 100644 index 00000000..77c1897e --- /dev/null +++ b/lib/data/consumet/models/anime_source.dart @@ -0,0 +1,23 @@ +part of 'models.dart'; + +class AnimeSource extends Equatable { + const AnimeSource({ + required this.headers, + required this.sources, + required this.download, + }); + + final Map headers; + final List sources; + final String download; + + @override + List get props => [ + headers, + sources, + download, + ]; + + @override + bool get stringify => true; +} diff --git a/lib/data/consumet/models/errors/anime.dart b/lib/data/consumet/models/errors/anime.dart deleted file mode 100644 index 2bd8ddfa..00000000 --- a/lib/data/consumet/models/errors/anime.dart +++ /dev/null @@ -1,46 +0,0 @@ -import '../anime/anime_provider.dart'; - -abstract class AnimeConsumetError implements Exception { - const AnimeConsumetError({ - this.details, - }); - - final AnimeProviderName name = AnimeProviderName.unknown; - final String? details; -} - -class ZoroError extends AnimeConsumetError { - const ZoroError({ - super.details, - }); - - @override - AnimeProviderName get name => AnimeProviderName.zoro; -} - -class GogoanimeError extends AnimeConsumetError { - const GogoanimeError({ - super.details, - }); - - @override - AnimeProviderName get name => AnimeProviderName.gogoanime; -} - -class AnimepaheError extends AnimeConsumetError { - const AnimepaheError({ - super.details, - }); - - @override - AnimeProviderName get name => AnimeProviderName.animepahe; -} - -class AnimefoxError extends AnimeConsumetError { - const AnimefoxError({ - super.details, - }); - - @override - AnimeProviderName get name => AnimeProviderName.animefox; -} diff --git a/lib/data/consumet/models/errors/errors.dart b/lib/data/consumet/models/errors/errors.dart deleted file mode 100644 index 7d1c1baa..00000000 --- a/lib/data/consumet/models/errors/errors.dart +++ /dev/null @@ -1 +0,0 @@ -export 'anime.dart'; diff --git a/lib/data/consumet/models/exceptions/exceptions.dart b/lib/data/consumet/models/exceptions/exceptions.dart new file mode 100644 index 00000000..c7164d8a --- /dev/null +++ b/lib/data/consumet/models/exceptions/exceptions.dart @@ -0,0 +1 @@ +sealed class NoEpisodeSourceException implements Exception {} diff --git a/lib/data/consumet/models/extractor.dart b/lib/data/consumet/models/extractor.dart new file mode 100644 index 00000000..ed2fbd30 --- /dev/null +++ b/lib/data/consumet/models/extractor.dart @@ -0,0 +1,5 @@ +part of 'models.dart'; + +abstract class Extractor { + Future> extract(Uri uri); +} diff --git a/lib/data/consumet/models/models.dart b/lib/data/consumet/models/models.dart index 4857ef17..c79e95ba 100644 --- a/lib/data/consumet/models/models.dart +++ b/lib/data/consumet/models/models.dart @@ -1,2 +1,35 @@ -export 'anime/anime.dart'; -export 'errors/errors.dart'; +import 'package:equatable/equatable.dart'; + +export 'exceptions/exceptions.dart'; + +part 'anime_episode.dart'; +part 'anime_result.dart'; +part 'anime_source.dart'; +part 'extractor.dart'; +part 'video_source.dart'; + +enum StreamingServers { + asianload, + gogocdn, + streamsb, + mixdrop, + mp4upload, + upcloud, + vidcloud, + streamtape, + vizcloud, + + // same as vizcloud + mycloud, + filemoon, + vidstreaming, + smashystream, + streamhub, + streamwish, + vidmoly, +} + +enum SubOrDub { + sub, + dub, +} diff --git a/lib/data/consumet/models/video_source.dart b/lib/data/consumet/models/video_source.dart new file mode 100644 index 00000000..eaaaeec7 --- /dev/null +++ b/lib/data/consumet/models/video_source.dart @@ -0,0 +1,31 @@ +part of 'models.dart'; + +class VideoSource extends Equatable { + const VideoSource({ + required this.url, + this.quality, + this.isM3U8, + this.isDASH, + this.size, + }); + + final String url; + final String? quality; + final bool? isM3U8; + final bool? isDASH; + final double? size; + + @override + List get props { + return [ + url, + quality, + isM3U8, + isDASH, + size, + ]; + } + + @override + bool get stringify => true; +} diff --git a/lib/data/consumet/providers/animefox.dart b/lib/data/consumet/providers/animefox.dart deleted file mode 100644 index 6375e179..00000000 --- a/lib/data/consumet/providers/animefox.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:convert'; - -import 'package:anikki/data/consumet/models/models.dart'; - -class AnimefoxProvider extends AnimeProvider { - AnimefoxProvider({ - super.providerName = AnimeProviderName.animefox, - }); - - @override - Future info(String id) async { - try { - final response = await client.get(Uri.parse('$baseUrl/info?id=$id')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; - - return AnimeInfoResponse.fromMap(decodedResponse); - } catch (e) { - throw AnimefoxError(details: e.toString()); - } - } - - @override - Future> search(String term) async { - try { - final response = await client.get(Uri.parse('$baseUrl/search/$term')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; - - final results = AnimeSearchResponse.fromMap(decodedResponse).results; - - return results ?? []; - } catch (e) { - throw AnimefoxError(details: e.toString()); - } - } - - @override - Future watch(String id) async { - try { - final response = - await client.get(Uri.parse('$baseUrl/watch?episodeId=$id')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; - - return AnimeWatchReponse.fromMap(decodedResponse); - } catch (e) { - throw AnimefoxError(details: e.toString()); - } - } -} diff --git a/lib/data/consumet/providers/animepahe.dart b/lib/data/consumet/providers/animepahe.dart deleted file mode 100644 index 8c7088d9..00000000 --- a/lib/data/consumet/providers/animepahe.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:convert'; - -import 'package:anikki/data/consumet/models/models.dart'; - -class AnimepaheProvider extends AnimeProvider { - AnimepaheProvider({ - super.providerName = AnimeProviderName.animepahe, - }); - - @override - Future info(String id) async { - try { - final response = await client.get(Uri.parse('$baseUrl/info/$id')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; - - return AnimeInfoResponse.fromMap(decodedResponse); - } catch (e) { - throw AnimepaheError(details: e.toString()); - } - } - - @override - Future> search(String term) async { - try { - final response = await client.get(Uri.parse('$baseUrl/search/$term')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; - - final results = AnimeSearchResponse.fromMap(decodedResponse).results; - - return results ?? []; - } catch (e) { - throw AnimepaheError(details: e.toString()); - } - } - - @override - Future watch(String id) async { - try { - final response = await client.get(Uri.parse('$baseUrl/watch/$id')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; - - return AnimeWatchReponse.fromMap(decodedResponse); - } catch (e) { - throw AnimepaheError(details: e.toString()); - } - } -} diff --git a/lib/data/consumet/providers/gogoanime.dart b/lib/data/consumet/providers/gogoanime.dart index 3009cc1f..97f03a70 100644 --- a/lib/data/consumet/providers/gogoanime.dart +++ b/lib/data/consumet/providers/gogoanime.dart @@ -1,50 +1,168 @@ -import 'dart:convert'; +import 'package:html/parser.dart'; +import 'package:http/http.dart'; -import 'package:anikki/data/consumet/models/models.dart'; +import '../extractors/extractors.dart'; +import '../models/models.dart'; +import '../utils/utils.dart'; -class GogoanimeProvider extends AnimeProvider { - GogoanimeProvider({ - super.providerName = AnimeProviderName.gogoanime, - }); +/// Adapted from `https://github.com/consumet/consumet.ts/blob/master/src/providers/anime/gogoanime.ts` +class Gogoanime { + final baseUrl = 'https://gogoanimehd.io'; + final logo = + 'https://play-lh.googleusercontent.com/MaGEiAEhNHAJXcXKzqTNgxqRmhuKB1rCUgb15UrN_mWUNRnLpO5T1qja64oRasO7mn0'; + final ajaxUrl = 'https://ajax.gogo-load.com'; - @override - Future info(String id) async { - try { - final response = await client.get(Uri.parse('$baseUrl/info/$id')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; + final client = Client(); - return AnimeInfoResponse.fromMap(decodedResponse); - } catch (e) { - throw GogoanimeError(details: e.toString()); - } + Future> search(String query) async { + final List results = []; + + final res = await client.get( + Uri.parse( + '$baseUrl/search.html?keyword=${Uri.encodeComponent(query)}&page=1', + ), + ); + + final document = parse(res.body); + + document.querySelectorAll('div.last_episodes > ul > li').forEach( + (element) => results.add( + AnimeResult( + id: element + .querySelector('p.name > a') + ?.attributes['href'] + ?.split('/')[2], + title: element.querySelector('p.name > a')?.attributes['title'], + url: + '$baseUrl/${element.querySelector('p.name > a')?.attributes['href']}', + subOrDub: element + .querySelector('p.name > a') + ?.text + .toLowerCase() + .contains('dub') == + true + ? SubOrDub.dub + : SubOrDub.sub, + ), + ), + ); + + return results; } - @override - Future> search(String term) async { - try { - final response = await client.get(Uri.parse('$baseUrl/search/$term')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; + Future> fetchAnimeEpisodes(String id) async { + if (!id.contains('gogoanime')) id = '$baseUrl/category/$id'; - final results = AnimeSearchResponse.fromMap(decodedResponse).results; + final res = await client.get(Uri.parse(id)); + final document = parse(res.body); - return results ?? []; - } catch (e) { - throw GogoanimeError(details: e.toString()); - } + final epStart = document + .querySelectorAll('#episode_page > li') + .first + .querySelector('a') + ?.attributes['ep_start']; + final epEnd = document + .querySelectorAll('#episode_page > li') + .last + .querySelector('a') + ?.attributes['ep_end']; + final movieId = document.querySelector('#movie_id')?.attributes['value']; + final alias = document.querySelector('#alias_anime')?.attributes['value']; + + final res2 = await client.get( + Uri.parse(ajaxUrl).replace( + pathSegments: [ + 'ajax', + 'load-list-episode', + ], + queryParameters: { + 'ep_start': epStart, + 'ep_end': epEnd, + 'id': movieId, + 'alias': alias, + 'default_ep': '0' + }, + ), + ); + final document2 = parse(res2.body); + + final episodes = document2 + .querySelectorAll('#episode_related > li') + .map( + (el) => AnimeEpisode( + id: el.querySelector('a')?.attributes['href']?.split('/')[1], + number: int.tryParse( + el.querySelector('div.name')?.text.replaceAll('EP ', '') ?? ''), + url: + '$baseUrl/${el.querySelector('a')?.attributes['href']?.trim()}', + ), + ) + .toList() + .reversed + .toList(); + + return episodes; } - @override - Future watch(String id) async { - try { - final response = await client.get(Uri.parse('$baseUrl/watch/$id')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; + Future fetchEpisodeSources( + String episodeId, { + StreamingServers server = StreamingServers.vidstreaming, + }) async { + final res = await client.get(Uri.parse('$baseUrl/$episodeId')); + final document = parse(res.body); + + Uri? uri; + + switch (server) { + case StreamingServers.vidstreaming: + final uriString = document + .querySelector( + 'div.anime_video_body > div.anime_muti_link > ul > li.vidcdn > a') + ?.attributes['data-video']; + + uri = Uri.tryParse(uriString ?? ''); + break; + case StreamingServers.streamsb: + final uriString = document + .querySelector( + 'div.anime_video_body > div.anime_muti_link > ul > li.streamsb > a') + ?.attributes['data-video']; + + uri = Uri.tryParse(uriString ?? ''); + break; + case StreamingServers.gogocdn: + default: + final uriString = document + .querySelector('#load_anime > div > div > iframe') + ?.attributes['src']; + + uri = Uri.tryParse(uriString ?? ''); + break; + } + + if (uri == null) throw NoEpisodeSourceException; + + switch (server) { + case StreamingServers.streamsb: + return AnimeSource( + headers: { + 'Referer': uri.toString(), + 'watchsb': 'streamsb', + 'User-Agent': userAgent, + }, + sources: await StreamSB().extract(uri), + download: 'https://gogohd.net/download?${uri.query}', + ); - return AnimeWatchReponse.fromMap(decodedResponse); - } catch (e) { - throw GogoanimeError(details: e.toString()); + case StreamingServers.gogocdn: + default: + return AnimeSource( + headers: { + 'Referer': uri.toString(), + }, + sources: await GogoCDN().extract(uri), + download: 'https://gogohd.net/download?${uri.query}', + ); } } } diff --git a/lib/data/consumet/providers/providers.dart b/lib/data/consumet/providers/providers.dart index dfc4d891..b9393ffe 100644 --- a/lib/data/consumet/providers/providers.dart +++ b/lib/data/consumet/providers/providers.dart @@ -1,4 +1 @@ -export 'animefox.dart'; -export 'animepahe.dart'; export 'gogoanime.dart'; -export 'zoro.dart'; diff --git a/lib/data/consumet/providers/zoro.dart b/lib/data/consumet/providers/zoro.dart deleted file mode 100644 index e500b1e0..00000000 --- a/lib/data/consumet/providers/zoro.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:convert'; - -import 'package:anikki/data/consumet/models/models.dart'; - -class ZoroProvider extends AnimeProvider { - ZoroProvider({ - super.providerName = AnimeProviderName.zoro, - }); - - @override - Future info(String id) async { - try { - final response = await client.get(Uri.parse('$baseUrl/info?id=$id')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; - - return AnimeInfoResponse.fromMap(decodedResponse); - } catch (e) { - throw ZoroError(details: e.toString()); - } - } - - @override - Future> search(String term) async { - try { - final response = await client.get(Uri.parse('$baseUrl/search/$term')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; - - final results = AnimeSearchResponse.fromMap(decodedResponse).results; - - return results ?? []; - } catch (e) { - throw ZoroError(details: e.toString()); - } - } - - @override - Future watch(String id) async { - try { - final response = - await client.get(Uri.parse('$baseUrl/watch?episodeId=$id')); - final decodedResponse = - jsonDecode(utf8.decode(response.bodyBytes)) as Map; - - return AnimeWatchReponse.fromMap(decodedResponse); - } catch (e) { - throw ZoroError(details: e.toString()); - } - } -} diff --git a/lib/data/consumet/utils/utils.dart b/lib/data/consumet/utils/utils.dart new file mode 100644 index 00000000..cb55ab5c --- /dev/null +++ b/lib/data/consumet/utils/utils.dart @@ -0,0 +1,2 @@ +const userAgent = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'; diff --git a/pubspec.lock b/pubspec.lock index 6ad10bd8..d5e478c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -40,6 +40,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" + url: "https://pub.dev" + source: hosted + version: "1.5.0" async: dependency: transitive description: @@ -288,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + encrypt: + dependency: "direct main" + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" equatable: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 61182022..3086e477 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,6 +72,7 @@ dependencies: url: https://github.com/Kylart/marqueer ref: main ionicons: ^0.2.2 + encrypt: ^5.0.3 dev_dependencies: flutter_test: