import 'package:mangayomi/bridge_lib.dart'; import 'dart:convert'; class AnimeSama extends MProvider { AnimeSama({required this.source}); MSource source; final Client client = Client(source); @override Future getPopular(int page) async { final doc = (await client.get(Uri.parse("${source.baseUrl}/#$page"))).body; final regex = RegExp(r"""^\s*carteClassique\(\s*.*?\s*,\s*"(.*?)".*\)""", multiLine: true); var matches = regex.allMatches(doc).toList(); List> chunks = chunked(matches, 5); List seasons = []; if (page > 0 && page <= chunks.length) { for (RegExpMatch match in chunks[page - 1]) { seasons.addAll(await fetchAnimeSeasons( "${source.baseUrl}/catalogue/${match.group(1)}")); } } return MPages(seasons, page < chunks.length); } @override Future getLatestUpdates(int page) async { final res = (await client.get(Uri.parse(source.baseUrl))).body; var document = parseHtml(res); final latest = document .select("h2") .where((MElement e) => e.outerHtml.toLowerCase().contains("derniers ajouts")) .toList(); final seasonElements = (latest.first.nextElementSibling as MElement) .select(".scrollBarStyled > div") .toList(); List seasons = []; for (var seasonElement in seasonElements) { seasons.addAll(await fetchAnimeSeasons( (seasonElement as MElement).getElementsByTagName("a").first.getHref)); } return MPages(seasons, false); } @override Future search(String query, int page, FilterList filterList) async { final filters = filterList.filters; final res = (await client .get(Uri.parse("${source.baseUrl}/catalogue/listing_all.php"))) .body; var databaseElements = parseHtml(res).select(".cardListAnime"); List elements = []; elements = databaseElements .where((MElement element) => element.select("h1, p").any((MElement e) => e.text.toLowerCase().contains(query.toLowerCase().trim()))) .toList(); for (var filter in filters) { if (filter.type == "TypeFilter") { final types = (filter.state as List).where((e) => e.state).toList(); elements = elements .where((MElement element) => types.isEmpty || types.any((p) => element.className.contains(p.value))) .toList(); } else if (filter.type == "LanguageFilter") { final language = (filter.state as List).where((e) => e.state).toList(); elements = elements .where((MElement element) => language.isEmpty || language.any((p) => element.className.contains(p.value))) .toList(); } else if (filter.type == "GenreFilter") { final included = (filter.state as List) .where((e) => e.state == 1 ? true : false) .toList(); final excluded = (filter.state as List) .where((e) => e.state == 2 ? true : false) .toList(); if (included.isNotEmpty) { elements = elements .where((MElement element) => included.every((p) => element.className.contains(p.value))) .toList(); } if (excluded.isNotEmpty) { elements = elements .where((MElement element) => excluded.every((p) => element.className.contains(p.value))) .toList(); } } } List> chunks = chunked(elements, 5); if (chunks.isEmpty) return MPages([], false); List seasons = []; for (var seasonElement in chunks[page - 1]) { seasons.addAll(await fetchAnimeSeasons( seasonElement.getElementsByTagName("a").first.getHref)); } return MPages(seasons, page < chunks.length); } @override Future getDetail(String url) async { var animeUrl = "${source.baseUrl}${substringBeforeLast(getUrlWithoutDomain(url), "/")}"; var movie = int.tryParse(url.split("#").length >= 2 ? url.split("#")[1] : ""); List> playersList = []; for (var lang in ["vostfr", "vf"]) { final players = await fetchPlayers("$animeUrl/$lang"); if (players.isNotEmpty) { playersList.add({"players": players, "lang": lang}); } } int maxLength = 0; for (var sublist in playersList) { for (var innerList in sublist["players"]) { if (innerList.length > maxLength) { maxLength = innerList.length; } } } List? episodesList = []; for (var episodeNumber = 0; episodeNumber < maxLength; episodeNumber++) { List langs = []; List> players = []; for (var playerListt in playersList) { for (var player in playerListt["players"]) { if (player.length > episodeNumber) { langs.add(playerListt["lang"]); players.add( {"lang": playerListt["lang"], "player": player[episodeNumber]}); } } } MChapter episode = MChapter(); episode.name = movie == null ? 'Episode ${episodeNumber + 1}' : 'Film'; episode.scanlator = langs.toSet().toList().join(', ').toUpperCase(); episode.url = json.encode(players); episodesList.add(episode); } MManga anime = MManga(); anime.chapters = movie == null ? episodesList.reversed.toList() : [episodesList[movie]]; return anime; } @override Future> getVideoList(String url) async { final players = json.decode(url); List videos = []; for (var player in players) { String lang = (player["lang"] as String).toUpperCase(); String playerUrl = player["player"]; List a = []; if (playerUrl.contains("sendvid")) { a = await sendVidExtractor(playerUrl, null, lang); } else if (playerUrl.contains("anime-sama.fr")) { MVideo video = MVideo(); video ..url = playerUrl ..originalUrl = playerUrl ..quality = "${lang} - AS Player"; a = [video]; } else if (playerUrl.contains("sibnet.ru")) { a = await sibnetExtractor(playerUrl, lang); } videos.addAll(a); } return sortVideos(videos, source.id); } @override List getFilterList() { return [ GroupFilter("TypeFilter", "Type", [ CheckBoxFilter("Anime", "Anime"), CheckBoxFilter("Film", "Film"), CheckBoxFilter("Autres", "Autres"), ]), GroupFilter("LanguageFilter", "Langue", [ CheckBoxFilter("VF", "VF"), CheckBoxFilter("VOSTFR", "VOSTFR"), ]), GroupFilter("GenreFilter", "Genre", [ TriStateFilter("Action", "Action"), TriStateFilter("Aventure", "Aventure"), TriStateFilter("Combats", "Combats"), TriStateFilter("Comédie", "Comédie"), TriStateFilter("Drame", "Drame"), TriStateFilter("Ecchi", "Ecchi"), TriStateFilter("École", "School-Life"), TriStateFilter("Fantaisie", "Fantasy"), TriStateFilter("Horreur", "Horreur"), TriStateFilter("Isekai", "Isekai"), TriStateFilter("Josei", "Josei"), TriStateFilter("Mystère", "Mystère"), TriStateFilter("Psychologique", "Psychologique"), TriStateFilter("Quotidien", "Slice-of-Life"), TriStateFilter("Romance", "Romance"), TriStateFilter("Seinen", "Seinen"), TriStateFilter("Shônen", "Shônen"), TriStateFilter("Shôjo", "Shôjo"), TriStateFilter("Sports", "Sports"), TriStateFilter("Surnaturel", "Surnaturel"), TriStateFilter("Tournois", "Tournois"), TriStateFilter("Yaoi", "Yaoi"), TriStateFilter("Yuri", "Yuri"), ]), ]; } @override List getSourcePreferences() { return [ ListPreference( key: "preferred_quality", title: "Qualité préférée", summary: "", valueIndex: 0, entries: ["1080p", "720p", "480p", "360p"], entryValues: ["1080", "720", "480", "360"]), ListPreference( key: "voices_preference", title: "Préférence des voix", summary: "", valueIndex: 0, entries: ["Préférer VOSTFR", "Préférer VF"], entryValues: ["vostfr", "vf"]), ]; } Future> fetchAnimeSeasons(String url) async { final res = (await client.get(Uri.parse(url))).body; var document = parseHtml(res); String animeName = document.getElementById("titreOeuvre")?.text ?? ""; var seasonRegex = RegExp("^\\s*panneauAnime\\(\"(.*)\", \"(.*)\"\\)", multiLine: true); var scripts = document .select("h2 + p + div > script, h2 + div > script") .map((MElement element) => element.text) .toList() .join(""); List animeList = []; List seasonRegexReg = seasonRegex.allMatches(scripts).toList(); for (var animeIndex = 0; animeIndex < seasonRegexReg.length; animeIndex++) { final seasonName = seasonRegexReg[animeIndex].group(1); final seasonStem = seasonRegexReg[animeIndex].group(2); if (seasonStem.toLowerCase().contains("film")) { var moviesUrl = "$url/$seasonStem"; var movies = await fetchPlayers(moviesUrl); if (movies.isNotEmpty) { var movieNameRegex = RegExp("^\\s*newSPF\\(\"(.*)\"\\);", multiLine: true); var moviesDoc = (await client.get(Uri.parse(moviesUrl))).body; List matches = movieNameRegex.allMatches(moviesDoc).toList(); for (var i = 0; i < movies.length; i++) { var title = ""; if (animeIndex == 0 && movies.length == 1) { title = animeName; } else if (matches.length > i) { title = "$animeName ${(matches[i]).group(1)}"; } else if (movies.length == 1) { title = "$animeName Film"; } else { title = "$animeName Film ${i + 1}"; } MManga anime = MManga(); anime.imageUrl = document.getElementById("coverOeuvre")?.getSrc; anime.genre = (document.xpathFirst( '//h2[contains(text(),"Genres")]/following-sibling::a/text()') ?? "") .split(","); anime.description = document.xpathFirst( '//h2[contains(text(),"Synopsis")]/following-sibling::p/text()') ?? ""; anime.name = title; anime.link = "$moviesUrl#$i"; anime.status = MStatus.completed; animeList.add(anime); } } } else { MManga anime = MManga(); anime.imageUrl = document.getElementById("coverOeuvre")?.getSrc; anime.genre = (document.xpathFirst( '//h2[contains(text(),"Genres")]/following-sibling::a/text()') ?? "") .split(","); anime.description = document.xpathFirst( '//h2[contains(text(),"Synopsis")]/following-sibling::p/text()') ?? ""; anime.name = '$animeName ${substringBefore(seasonName, ',').replaceAll('"', "")}'; anime.link = "$url/$seasonStem"; animeList.add(anime); } } return animeList; } Future>> fetchPlayers(String url) async { var docUrl = "$url/episodes.js"; List> players = []; var response = (await client.get(Uri.parse(docUrl))).body; if (response == "error") { return []; } var sanitizedDoc = sanitizeEpisodesJs(response); for (var i = 1; i <= 8; i++) { final numPlayers = getPlayers("eps$i", sanitizedDoc); if (numPlayers != null) players.add(numPlayers); } final asPlayers = getPlayers("epsAS", sanitizedDoc); if (asPlayers != null) players.add(asPlayers); if (players.isEmpty) return []; List> finalPlayers = []; for (var i = 0; i <= players[0].length; i++) { for (var playerList in players) { if (playerList.length > i) { finalPlayers.add(playerList); } } } return finalPlayers.toSet().toList(); } List? getPlayers(String playerName, String doc) { var playerRegex = RegExp('$playerName\\s*=\\s*(\\[.*?\\])', dotAll: true); var match = playerRegex.firstMatch(doc); if (match == null) return null; final regex = RegExp(r"""https?://[^\s\',\[\]]+"""); final matches = regex.allMatches(match.group(1)); List urls = []; for (var match in matches.toList()) { urls.add((match as RegExpMatch).group(0).toString()); } return urls; } String sanitizeEpisodesJs(String doc) { return doc.replaceAll( RegExp(r'(?<=\[|\,)\s*\"\s*(https?://[^\s\"]+)\s*\"\s*(?=\,|\])'), ''); } List> chunked(List list, int size) { List> chunks = []; for (int i = 0; i < list.length; i += size) { int end = list.length; if (i + size < list.length) { end = i + size; } chunks.add(list.sublist(i, end)); } return chunks; } List sortVideos(List videos, int sourceId) { String quality = getPreferenceValue(sourceId, "preferred_quality"); String voice = getPreferenceValue(sourceId, "voices_preference"); videos.sort((MVideo a, MVideo b) { int qualityMatchA = 0; if (a.quality.contains(quality) && a.quality.toLowerCase().contains(voice)) { qualityMatchA = 1; } int qualityMatchB = 0; if (b.quality.contains(quality) && b.quality.toLowerCase().contains(voice)) { 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; } } AnimeSama main(MSource source) { return AnimeSama(source: source); }