import 'package:mangayomi/bridge_lib.dart'; import 'dart:convert'; class Aniwave extends MProvider { Aniwave({required this.source}); MSource source; final Client client = Client(source); @override String get baseUrl => getPreferenceValue(source.id, "preferred_domain2"); @override Future getPopular(int page) async { final res = (await client .get(Uri.parse("$baseUrl/filter?sort=trending&page=$page"))) .body; return parseAnimeList(res); } @override Future getLatestUpdates(int page) async { final res = (await client .get(Uri.parse("$baseUrl/filter?sort=recently_updated&page=$page"))) .body; return parseAnimeList(res); } @override Future search(String query, int page, FilterList filterList) async { final filters = filterList.filters; String url = "$baseUrl/filter?keyword=$query"; for (var filter in filters) { if (filter.type == "OrderFilter") { final order = filter.values[filter.state].value; url += "${ll(url)}sort=$order"; } else if (filter.type == "GenreFilter") { final genre = (filter.state as List).where((e) => e.state).toList(); if (genre.isNotEmpty) { for (var st in genre) { url += "${ll(url)}genre[]=${st.value}"; } } } else if (filter.type == "CountryFilter") { final country = (filter.state as List).where((e) => e.state).toList(); if (country.isNotEmpty) { for (var st in country) { url += "${ll(url)}country[]=${st.value}"; } } } else if (filter.type == "SeasonFilter") { final season = (filter.state as List).where((e) => e.state).toList(); if (season.isNotEmpty) { for (var st in season) { url += "${ll(url)}season[]=${st.value}"; } } } else if (filter.type == "YearFilter") { final year = (filter.state as List).where((e) => e.state).toList(); if (year.isNotEmpty) { for (var st in year) { url += "${ll(url)}year[]=${st.value}"; } } } else if (filter.type == "TypeFilter") { final type = (filter.state as List).where((e) => e.state).toList(); if (type.isNotEmpty) { for (var st in type) { url += "${ll(url)}type[]=${st.value}"; } } } else if (filter.type == "StatusFilter") { final status = (filter.state as List).where((e) => e.state).toList(); if (status.isNotEmpty) { for (var st in status) { url += "${ll(url)}status[]=${st.value}"; } } } else if (filter.type == "LanguageFilter") { final language = (filter.state as List).where((e) => e.state).toList(); if (language.isNotEmpty) { for (var st in language) { url += "${ll(url)}language[]=${st.value}"; } } } else if (filter.type == "RatingFilter") { final rating = (filter.state as List).where((e) => e.state).toList(); if (rating.isNotEmpty) { for (var st in rating) { url += "${ll(url)}rating[]=${st.value}"; } } } } final res = (await client.get(Uri.parse("$url&page=$page"))).body; return parseAnimeList(res); } @override Future getDetail(String url) async { final statusList = [ {"Releasing": 0, "Completed": 1} ]; final res = (await client.get(Uri.parse("$baseUrl$url"))).body; MManga anime = MManga(); final status = xpath(res, '//div[contains(text(),"Status")]/span/text()'); if (status.isNotEmpty) { anime.status = parseStatus(status.first, statusList); } final description = xpath(res, '//*[contains(@class,"synopsis")]/div[@class="shorting"]/div[@class="content"]/text()'); if (description.isNotEmpty) { anime.description = description.first; } final author = xpath(res, '//div[contains(text(),"Studio")]/span/text()'); if (author.isNotEmpty) { anime.author = author.first; } anime.genre = xpath(res, '//div[contains(text(),"Genre")]/span/a/text()'); final id = parseHtml(res).selectFirst("div[data-id]").attr("data-id"); final encrypt = vrfEncrypt(id); final vrf = "vrf=${Uri.encodeComponent(encrypt)}"; final resEp = (await client.get(Uri.parse("$baseUrl/ajax/episode/list/$id?$vrf"))) .body; final html = json.decode(resEp)["result"]; List? episodesList = []; final epsHtmls = parseHtml(html).select("div.episodes ul > li"); for (var epH in epsHtmls) { final epHtml = epH.outerHtml; final title = xpath(epHtml, '//li/@title').isNotEmpty ? xpath(epHtml, '//li/@title').first : ""; final ids = xpath(epHtml, '//a/@data-ids').first; final sub = xpath(epHtml, '//a/@data-sub').first; final dub = xpath(epHtml, '//a/@data-dub').first; final softsub = title.toLowerCase().contains("softsub") ? "1" : ""; final fillerEp = title.toLowerCase().contains("filler") ? "1" : ""; final epNum = xpath(epHtml, '//a/@data-num').first; String scanlator = ""; if (sub == "1") { scanlator += "Sub"; } if (softsub == "1") { scanlator += ", Softsub"; } if (dub == "1") { scanlator += ", Dub"; } if (fillerEp == "1") { scanlator += ", • Filler Episode"; } MChapter episode = MChapter(); episode.name = "Episode $epNum"; episode.scanlator = scanlator; episode.url = "$ids&epurl=$url/ep-$epNum"; episodesList.add(episode); } anime.chapters = episodesList.reversed.toList(); return anime; } @override Future> getVideoList(String url) async { final ids = substringBefore(url, "&"); final encrypt = vrfEncrypt(ids); final vrf = "vrf=${Uri.encodeComponent(encrypt)}"; final res = (await client .get(Uri.parse("$baseUrl/ajax/server/list/$ids?$vrf"), headers: { "Referer": baseUrl + url, "X-Requested-With": "XMLHttpRequest" })) .body; final html = json.decode(res)["result"]; final vidsHtmls = parseHtml(html).select("div.servers > div"); List videos = []; for (var vidH in vidsHtmls) { final vidHtml = vidH.outerHtml; final type = xpath(vidHtml, '//div/@data-type').first; final serversIds = xpath(vidHtml, '//li/@data-link-id'); final serversNames = xpath(vidHtml, '//li/text()'); for (int i = 0; i < serversIds.length; i++) { final serverId = serversIds[i]; final serverName = serversNames[i].toLowerCase(); final encrypt = vrfEncrypt(serverId); final vrf = "vrf=${Uri.encodeComponent(encrypt)}"; final res = (await client.get(Uri.parse("$baseUrl/ajax/server/$serverId?$vrf"))) .body; final status = json.decode(res)["status"]; if (status == 200) { List a = []; final url = vrfDecrypt(json.decode(res)["result"]["url"]); final hosterSelection = preferenceHosterSelection(source.id); final typeSelection = preferenceTypeSelection(source.id); if (typeSelection.contains(type.toLowerCase())) { if (serverName.contains("vidstream") || url.contains("megaf")) { final hosterName = serverName.contains("vidstream") ? "Vidstream" : "MegaF"; if (hosterSelection.contains(hosterName.toLowerCase())) { a = await vidsrcExtractor(url, hosterName, type); } } else if (serverName.contains("mp4u") && hosterSelection.contains("mp4u")) { a = await mp4UploadExtractor(url, null, "", type); } else if (serverName.contains("streamtape") && hosterSelection.contains("streamtape")) { a = await streamTapeExtractor(url, "StreamTape - $type"); } else if (serverName.contains("moonf") && hosterSelection.contains("moonf")) { a = await filemoonExtractor(url, "MoonF", type); } videos.addAll(a); } } } } return sortVideos(videos, source.id); } MPages parseAnimeList(String res) { List animeList = []; final urls = xpath(res, '//div[@class="item "]/div/div/div/a/@href'); final names = xpath(res, '//div[@class="item "]/div/div/div/a/text()'); final images = xpath(res, '//div[@class="item "]/div/div/a/img/@src'); for (var i = 0; i < names.length; i++) { MManga anime = MManga(); anime.name = names[i]; anime.imageUrl = images[i]; anime.link = urls[i]; animeList.add(anime); } return MPages(animeList, true); } List rc4Encrypt(String key, List message) { List _key = utf8.encode(key); int _i = 0, _j = 0; List _box = List.generate(256, (i) => i); int x = 0; for (int i = 0; i < 256; i++) { x = (x + _box[i] + _key[i % _key.length]) % 256; var tmp = _box[i]; _box[i] = _box[x]; _box[x] = tmp; } List out = []; for (var char in message) { _i = (_i + 1) % 256; _j = (_j + _box[_i]) % 256; var tmp = _box[_i]; _box[_i] = _box[_j]; _box[_j] = tmp; final c = char ^ (_box[(_box[_i] + _box[_j]) % 256]); out.add(c); } return out; } String vrfEncrypt(String input) { final rc4 = rc4Encrypt("T78s2WjTc7hSIZZR", input.codeUnits); final vrf = base64Url.encode(rc4); return utf8.decode(vrf.codeUnits); } String vrfDecrypt(String input, {String key = "ctpAbOz5u7S6OMkx"}) { final decode = base64Url.decode(input); final rc4 = rc4Encrypt(key, decode); return Uri.decodeComponent(utf8.decode(rc4)); } List vrfShift(List vrf) { var shifts = [-2, -4, -5, 6, 2, -3, 3, 6]; for (var i = 0; i < vrf.length; i++) { var shift = shifts[i % 8]; vrf[i] = (vrf[i] + shift) & 0xFF; } return vrf; } Future> vidsrcExtractor( String url, String name, String type) async { List videoList = []; final host = Uri.parse(url).host; final apiUrl = getApiUrl(url); final res = await client.get(Uri.parse(apiUrl)); if (res.body.startsWith("{")) { final result = json.decode( vrfDecrypt(json.decode(res.body)['result'], key: "9jXDYBZUcTcTZveM")); if (result != 404) { String masterUrl = ((result['sources'] as List>).first)['file']; final tracks = (result['tracks'] as List) .where((e) => e['kind'] == 'captions' ? true : false) .toList(); List subtitles = []; for (var sub in tracks) { try { MTrack subtitle = MTrack(); subtitle ..label = sub["label"] ..file = sub["file"]; subtitles.add(subtitle); } catch (_) {} } final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body; for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:") .split("#EXT-X-STREAM-INF:")) { final quality = "${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p"; String videoUrl = substringBefore(substringAfter(it, "\n"), "\n"); if (!videoUrl.startsWith("http")) { videoUrl = "${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl"; } MVideo video = MVideo(); video ..url = videoUrl ..originalUrl = videoUrl ..quality = "$name - $type - $quality" ..headers = {"Referer": "https://$host/"} ..subtitles = subtitles; videoList.add(video); } } } return videoList; } String getApiUrl(String url) { final host = Uri.parse(url).host; final paramsToString = Uri.parse(url) .queryParameters .entries .map((e) => "${e.key}=${e.value}") .join("&"); var vidId = substringBefore(substringAfterLast(url, "/"), "?"); final apiSlug = encodeID(vidId, "8Qy3mlM2kod80XIK"); final h = encodeID(vidId, "BgKVSrzpH2Enosgm"); String apiUrlString = ""; apiUrlString += "https://$host/mediainfo/$apiSlug"; if (paramsToString.isNotEmpty) { apiUrlString += "?$paramsToString&h=$h"; } return apiUrlString; } String encodeID(String vidId, String key) { final rc4 = rc4Encrypt(key, vidId.codeUnits); return base64.encode(rc4).replaceAll("+", "-").replaceAll("/", "_").trim(); } Future callFromFuToken(String host, String data, String url) async { final fuTokenScript = (await client .get(Uri.parse("https://$host/futoken"), headers: {"Referer": url})) .body; String js = ""; js += "(function"; js += substringBefore( substringAfter(substringAfter(fuTokenScript, "window"), "function") .replaceAll("jQuery.ajax(", ""), "+location.search"); js += "}(\"$data\"))"; final jsRes = await evalJs(js); if (jsRes == "error") return ""; return jsRes; } @override List getFilterList() { return [ SelectFilter("OrderFilter", "Sort order", 0, [ SelectFilterOption("Most relevance", "most_relevance"), SelectFilterOption("Recently updated", "recently_updated"), SelectFilterOption("Recently added", "recently_added"), SelectFilterOption("Release date", "release_date"), SelectFilterOption("Trending", "trending"), SelectFilterOption("Name A-Z", "title_az"), SelectFilterOption("Scores", "scores"), SelectFilterOption("MAL scores", "mal_scores"), SelectFilterOption("Most watched", "most_watched"), SelectFilterOption("Most favourited", "most_favourited"), SelectFilterOption("Number of episodes", "number_of_episodes"), ]), SeparatorFilter(), GroupFilter("GenreFilter", "Genre", [ CheckBoxFilter("Action", "1"), CheckBoxFilter("Adventure", "2"), CheckBoxFilter("Avant Garde", "2262888"), CheckBoxFilter("Boys Love", "2262603"), CheckBoxFilter("Comedy", "4"), CheckBoxFilter("Demons", "4424081"), CheckBoxFilter("Drama", "7"), CheckBoxFilter("Ecchi", "8"), CheckBoxFilter("Fantasy", "9"), CheckBoxFilter("Girls Love", "2263743"), CheckBoxFilter("Gourmet", "2263289"), CheckBoxFilter("Harem", "11"), CheckBoxFilter("Horror", "14"), CheckBoxFilter("Isekai", "3457284"), CheckBoxFilter("Iyashikei", "4398552"), CheckBoxFilter("Josei", "15"), CheckBoxFilter("Kids", "16"), CheckBoxFilter("Magic", "4424082"), CheckBoxFilter("Mahou Shoujo", "3457321"), CheckBoxFilter("Martial Arts", "18"), CheckBoxFilter("Mecha", "19"), CheckBoxFilter("Military", "20"), CheckBoxFilter("Music", "21"), CheckBoxFilter("Mystery", "22"), CheckBoxFilter("Parody", "23"), CheckBoxFilter("Psychological", "25"), CheckBoxFilter("Reverse Harem", "4398403"), CheckBoxFilter("Romance", "26"), CheckBoxFilter("School", "28"), CheckBoxFilter("Sci-Fi", "29"), CheckBoxFilter("Seinen", "30"), CheckBoxFilter("Shoujo", "31"), CheckBoxFilter("Shounen", "33"), CheckBoxFilter("Slice of Life", "35"), CheckBoxFilter("Space", "36"), CheckBoxFilter("Sports", "37"), CheckBoxFilter("Super Power", "38"), CheckBoxFilter("Supernatural", "39"), CheckBoxFilter("Suspense", "2262590"), CheckBoxFilter("Thriller", "40"), CheckBoxFilter("Vampire", "41") ]), GroupFilter("CountryFilter", "Country", [ CheckBoxFilter("China", "120823"), CheckBoxFilter("Japan", "120822") ]), GroupFilter("SeasonFilter", "Season", [ CheckBoxFilter("Fall", "fall"), CheckBoxFilter("Summer", "summer"), CheckBoxFilter("Spring", "spring"), CheckBoxFilter("Winter", "winter"), CheckBoxFilter("Unknown", "unknown") ]), GroupFilter("YearFilter", "Year", [ CheckBoxFilter("2024", "2024"), CheckBoxFilter("2023", "2023"), CheckBoxFilter("2022", "2022"), CheckBoxFilter("2021", "2021"), CheckBoxFilter("2020", "2020"), CheckBoxFilter("2019", "2019"), CheckBoxFilter("2018", "2018"), CheckBoxFilter("2017", "2017"), CheckBoxFilter("2016", "2016"), CheckBoxFilter("2015", "2015"), CheckBoxFilter("2014", "2014"), CheckBoxFilter("2013", "2013"), CheckBoxFilter("2012", "2012"), CheckBoxFilter("2011", "2011"), CheckBoxFilter("2010", "2010"), CheckBoxFilter("2009", "2009"), CheckBoxFilter("2008", "2008"), CheckBoxFilter("2007", "2007"), CheckBoxFilter("2006", "2006"), CheckBoxFilter("2005", "2005"), CheckBoxFilter("2004", "2004"), CheckBoxFilter("2003", "2003"), CheckBoxFilter("2000s", "2000s"), CheckBoxFilter("1990s", "1990s"), CheckBoxFilter("1980s", "1980s"), CheckBoxFilter("1970s", "1970s"), CheckBoxFilter("1960s", "1960s"), CheckBoxFilter("1950s", "1950s"), CheckBoxFilter("1940s", "1940s"), CheckBoxFilter("1930s", "1930s"), CheckBoxFilter("1920s", "1920s"), CheckBoxFilter("1910s", "1910s") ]), GroupFilter("TypeFilter", "Type", [ CheckBoxFilter("Movie", "movie"), CheckBoxFilter("TV", "tv"), CheckBoxFilter("OVA", "ova"), CheckBoxFilter("ONA", "ona"), CheckBoxFilter("Special", "special"), CheckBoxFilter("Music", "music") ]), GroupFilter("StatusFilter", "Status", [ CheckBoxFilter("Not Yet Aired", "info"), CheckBoxFilter("Releasing", "releasing"), CheckBoxFilter("Completed", "completed") ]), GroupFilter("LanguageFilter", "Language", [ CheckBoxFilter("Sub and Dub", "subdub"), CheckBoxFilter("Sub", "sub"), CheckBoxFilter("Dub", "dub") ]), GroupFilter("RatingFilter", "Rating", [ CheckBoxFilter("G - All Ages", "g"), CheckBoxFilter("PG - Children", "pg"), CheckBoxFilter("PG 13 - Teens 13 and Older", "pg_13"), CheckBoxFilter("R - 17+, Violence & Profanity", "r"), CheckBoxFilter("R+ - Profanity & Mild Nudity", "r+"), CheckBoxFilter("Rx - Hentai", "rx") ]), ]; } @override List getSourcePreferences() { return [ ListPreference( key: "preferred_domain2", title: "Preferred domain", summary: "", valueIndex: 0, entries: ["aniwave.to", "aniwavetv.to (unofficial)"], entryValues: ["https://aniwave.to", "https://aniwavetv.to"]), ListPreference( key: "preferred_quality", title: "Preferred Quality", summary: "", valueIndex: 0, entries: ["1080p", "720p", "480p", "360p"], entryValues: ["1080", "720", "480", "360"]), ListPreference( key: "preferred_language", title: "Preferred Type", summary: "", valueIndex: 0, entries: ["Sub", "Softsub", "Dub"], entryValues: ["Sub", "Softsub", "Dub"]), ListPreference( key: "preferred_server1", title: "Preferred server", summary: "", valueIndex: 0, entries: [ "Vidstream", "Megaf", "MoonF", "StreamTape", "MP4u" ], entryValues: [ "vidstream", "megaf", "moonf", "streamtape", "mp4upload" ]), MultiSelectListPreference( key: "hoster_selection1", title: "Enable/Disable Hosts", summary: "", entries: ["Vidstream", "Megaf", "MoonF", "StreamTape", "MP4u"], entryValues: ["vidstream", "megaf", "moonf", "streamtape", "mp4u"], values: ["vidstream", "megaf", "moonf", "streamtape", "mp4u"]), MultiSelectListPreference( key: "type_selection", title: "Enable/Disable Type", summary: "", entries: ["Sub", "Softsub", "Dub"], entryValues: ["sub", "softsub", "dub"], values: ["sub", "softsub", "dub"]), ]; } List preferenceHosterSelection(int sourceId) { return getPreferenceValue(sourceId, "hoster_selection1"); } List preferenceTypeSelection(int sourceId) { return getPreferenceValue(sourceId, "type_selection"); } List sortVideos(List videos, int sourceId) { String quality = getPreferenceValue(sourceId, "preferred_quality"); String server = getPreferenceValue(sourceId, "preferred_server1"); String lang = getPreferenceValue(sourceId, "preferred_language"); videos.sort((MVideo a, MVideo b) { int qualityMatchA = 0; if (a.quality.contains(quality) && a.quality.toLowerCase().contains(lang.toLowerCase()) && a.quality.toLowerCase().contains(server.toLowerCase())) { qualityMatchA = 1; } int qualityMatchB = 0; if (b.quality.contains(quality) && b.quality.toLowerCase().contains(lang.toLowerCase()) && b.quality.toLowerCase().contains(server.toLowerCase())) { 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; } String ll(String url) { if (url.contains("?")) { return "&"; } return "?"; } } Aniwave main(MSource source) { return Aniwave(source: source); }