diff --git a/anime/source_generator.dart b/anime/source_generator.dart index 521a8e25..298c0ff9 100644 --- a/anime/source_generator.dart +++ b/anime/source_generator.dart @@ -27,6 +27,7 @@ import 'src/id/nimegami/source.dart'; import 'src/id/oploverz/source.dart'; import 'src/id/otakudesu/source.dart'; import 'src/it/animesaturn/source.dart'; +import 'src/pt/animesvision/source.dart'; import 'src/sq/filma24/source.dart'; void main() { @@ -56,7 +57,8 @@ void main() { nyaaSource, yomirollSource, animepaheSource, - animetoast + animetoast, + animesvision ]; final List> jsonList = _sourcesList.map((source) => source.toJson()).toList(); diff --git a/anime/src/pt/animesvision/animesvision.dart b/anime/src/pt/animesvision/animesvision.dart new file mode 100644 index 00000000..84784a30 --- /dev/null +++ b/anime/src/pt/animesvision/animesvision.dart @@ -0,0 +1,287 @@ +import 'package:mangayomi/bridge_lib.dart'; +import 'dart:math'; + +class AnimesVision extends MProvider { + AnimesVision({required this.source}); + + MSource source; + + final Client client = Client(source); + + @override + String get baseUrl => source.baseUrl; + + Map get headers => { + "Referer": baseUrl, + "Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7" + }; + + @override + Future getPopular(int page) async { + final res = (await client.get(Uri.parse(baseUrl), headers: headers)).body; + final document = parseHtml(res); + final elements = + document.select("div#anime-trending div.item > a.film-poster"); + List animeList = []; + for (var element in elements) { + var anime = MManga(); + var img = element.selectFirst("img"); + anime.name = img.attr("title"); + anime.link = getUrlWithoutDomain(element.attr("href")); + anime.imageUrl = img.attr("src"); + animeList.add(anime); + } + return MPages(animeList, hasNextPage(document)); + } + + @override + Future getLatestUpdates(int page) async { + final res = (await client.get(Uri.parse("$baseUrl/lancamentos?page=$page"), + headers: headers)) + .body; + final document = parseHtml(res); + final elements = + document.select("div.container div.screen-items > div.item"); + List animeList = []; + for (var element in elements) { + var anime = MManga(); + anime.name = substringAfter(element.selectFirst("h3").text, "-").trim(); + anime.link = getUrlWithoutDomain(element.selectFirst("a").attr("href")); + anime.imageUrl = element.selectFirst("img")?.attr("src") ?? ""; + animeList.add(anime); + } + return MPages(animeList, hasNextPage(document)); + } + + @override + Future search(String query, int page, FilterList filterList) async { + final res = (await client + .get(Uri.parse("$baseUrl/search-anime?nome=$query&page=$page"))) + .body; + final document = parseHtml(res); + final elements = document.select("div.film_list-wrap div.film-poster"); + List animeList = []; + for (var element in elements) { + var anime = MManga(); + final elementA = element.selectFirst("a"); + anime.name = elementA.attr("title"); + anime.link = getUrlWithoutDomain(elementA.attr("href")); + anime.imageUrl = element.selectFirst("img").attr("data-src"); + animeList.add(anime); + } + return MPages(animeList, hasNextPage(document)); + } + + @override + Future getDetail(String url) async { + final statusList = [ + {"Atualmente sendo exibido": 0, "Fim da exibição": 1} + ]; + MManga anime = MManga(); + final res = (await client.get(Uri.parse("$baseUrl$url"))).body; + var document = await getRealDoc(parseHtml(res), "$baseUrl$url"); + final content = document.selectFirst("div#ani_detail div.anis-content"); + final detail = content.selectFirst("div.anisc-detail"); + final infos = content.selectFirst("div.anisc-info"); + anime.imageUrl = content.selectFirst("img")?.attr("src"); + anime.name = detail.selectFirst("h2.film-name").text; + anime.genre = getInfo(infos, "Gêneros").split(","); + anime.author = getInfo(infos, "Produtores"); + anime.artist = getInfo(infos, "Estúdios"); + anime.status = parseStatus(getInfo(infos, "Status"), statusList); + String description = getInfo(infos, "Sinopse"); + if (getInfo(infos, "Inglês").isNotEmpty) + description += '\n\nTítulo em inglês: ${getInfo(infos, "Inglês")}'; + anime.description = description; + if (getInfo(infos, "Japonês").isNotEmpty) + description += '\nTítulo em Japonês: ${getInfo(infos, "Japonês")}'; + if (getInfo(infos, "Foi ao ar em").isNotEmpty) + description += '\nFoi ao ar em: ${getInfo(infos, "Foi ao ar em")}'; + if (getInfo(infos, "Temporada").isNotEmpty) + description += '\nTemporada: ${getInfo(infos, "Temporada")}'; + if (getInfo(infos, "Duração").isNotEmpty) + description += '\nDuração: ${getInfo(infos, "Duração")}'; + if (getInfo(infos, "Fansub").isNotEmpty) + description += '\nFansub: ${getInfo(infos, "Fansub")}'; + anime.description = description; + List episodeList = []; + for (var element + in document.select("div.container div.screen-items > div.item") ?? []) { + episodeList.add(episodeFromElement(element)); + } + + while (hasNextPage(document)) { + if (episodeList.isNotEmpty) { + final nextUrl = + nextPageElements(document)[0].selectFirst("a").attr("href"); + document = parseHtml((await client.get(Uri.parse(nextUrl))).body); + } + for (var element + in document.select("div.container div.screen-items > div.item") ?? + []) { + episodeList.add(episodeFromElement(element)); + } + } + anime.chapters = episodeList.reversed.toList(); + return anime; + } + + @override + Future> getVideoList(String url) async { + final res = (await client.get(Uri.parse("$baseUrl$url"))).body; + final document = parseHtml(res); + final encodedScript = document + .selectFirst("div.player-frame div#playerglobalapi ~ script") + .text; + final decodedScript = decodeScriptFromString(encodedScript); + List videos = []; + for (RegExpMatch match in RegExp(r'"file":"(\S+?)",.*?"label":"(.*?)"') + .allMatches(decodedScript)) { + final videoUrl = match.group(1)!.replaceAll('\\', ''); + final qualityName = match.group(2); + var video = MVideo(); + video.url = videoUrl; + video.headers = headers; + video.quality = 'PlayerVision $qualityName'; + video.originalUrl = videoUrl; + videos.add(video); + } + return videos; + } + + bool hasNextPage(MDocument document) { + return nextPageElements(document).isNotEmpty; + } + + List nextPageElements(MDocument document) { + final elements = document + .select("ul.pagination li.page-item") + .where((MElement e) => + e.outerHtml.contains("›") && !e.outerHtml.contains("disabled")) + .toList(); + return elements; + } + + Future getRealDoc(MDocument document, String originalUrl) async { + if (["/episodio-", "/filme-"].any((e) => originalUrl.contains(e))) { + final url = document.selectFirst("h2.film-name > a").attr("href"); + final res = (await client.get(Uri.parse(url))).body; + return parseHtml(res); + } + return document; + } + + String getInfo(MElement element, String key) { + final divs = element + .select("div.item") + .where((MElement e) => e.outerHtml.contains(key)) + .toList(); + String text = ""; + if (divs.isNotEmpty) { + MElement div = divs[0]; + var elementsA = div.select("a[href]"); + if (elementsA.isEmpty) { + String selector = + div.outerHtml.contains("w-hide") ? "div.text" : "span.name"; + text = div.selectFirst(selector).text.trim(); + } else { + text = elementsA.map((MElement e) => e.text.trim()).toList().join(', '); + } + } + return text; + } + + MChapter episodeFromElement(MElement element) { + var anime = MChapter(); + anime.url = getUrlWithoutDomain(element.selectFirst("a").attr("href")); + anime.name = element.selectFirst("h3").text.trim(); + return anime; + } + + List sortVideos(List videos, int sourceId) { + String quality = getPreferenceValue(sourceId, "preferred_quality"); + + videos.sort((MVideo a, MVideo b) { + int qualityMatchA = 0; + + if (a.quality.contains(quality)) { + qualityMatchA = 1; + } + int qualityMatchB = 0; + if (b.quality.contains(quality)) { + qualityMatchB = 1; + } + if (qualityMatchA != qualityMatchB) { + return qualityMatchB - qualityMatchA; + } + + final regex = RegExp(r'(\d+)p'); + final matchA = regex.firstMatch(a.quality); + final matchB = regex.firstMatch(b.quality); + final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0; + final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0; + return qualityNumB - qualityNumA; + }); + return videos; + } + + @override + List getSourcePreferences() { + return [ + ListPreference( + key: "preferred_quality", + title: "Qualidade preferida", + summary: "", + valueIndex: 1, + entries: ["480p", "720p", "1080p", "4K"], + entryValues: ["1080", "720", "480", "4K"]), + ]; + } + + int convertToNum(String thing, int limit) { + int result = 0; + int i = 0; + for (var n in thing.split('').reversed.toList()) { + final a = int.tryParse(n) ?? 0; + result += a * pow(limit, i).toInt(); + i++; + } + return result; + } + + String decodeScript( + String encodedString, String magicStr, int offset, int limit) { + RegExp regex = RegExp('\\w'); + List parts = encodedString.split(magicStr[limit]); + List decodedParts = []; + for (String part in parts.sublist(0, parts.length - 1)) { + String replaced = part; + for (Match match in regex.allMatches(part)) { + replaced = replaced.replaceFirst( + match.group(0)!, magicStr.indexOf(match.group(0)!).toString()); + } + int charInt = convertToNum(replaced, limit) - offset; + decodedParts.add(String.fromCharCode(charInt)); + } + return decodedParts.join(''); + } + + String decodeScriptFromString(String script) { + RegExp regex = RegExp(r'\}\("(\w+)",.*?"(\w+)",(\d+),(\d+),.*?\)'); + Match? match = regex.firstMatch(script); + if (match != null) { + return decodeScript( + match.group(1)!, + match.group(2)!, + int.tryParse(match.group(3)!) ?? 0, + int.tryParse(match.group(4)!) ?? 0, + ); + } else { + return script; + } + } +} + +AnimesVision main(MSource source) { + return AnimesVision(source: source); +} diff --git a/anime/src/pt/animesvision/icon.png b/anime/src/pt/animesvision/icon.png new file mode 100644 index 00000000..523b2380 Binary files /dev/null and b/anime/src/pt/animesvision/icon.png differ diff --git a/anime/src/pt/animesvision/source.dart b/anime/src/pt/animesvision/source.dart new file mode 100644 index 00000000..a6784b57 --- /dev/null +++ b/anime/src/pt/animesvision/source.dart @@ -0,0 +1,16 @@ +import '../../../../model/source.dart'; + +Source get animesvision => _animesvision; +const _animesvisionVersion = "0.0.1"; +const _animesvisionCodeUrl = + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/anime/src/pt/animesvision/animesvision.dart"; +Source _animesvision = Source( + name: "AnimesVision", + baseUrl: "https://animes.vision", + lang: "pt-br", + typeSource: "single", + iconUrl: + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/anime/src/pt/animesvision/icon.png", + sourceCodeUrl: _animesvisionCodeUrl, + version: _animesvisionVersion, + isManga: false);