From 694c8a8b99f6e9ea7bd3b9ea2d4b3840df90324c Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Wed, 27 Dec 2023 20:41:52 +0100 Subject: [PATCH 1/2] feat: Aniwave (EN): add Video extractors --- CONTRIBUTING.md | 3 +- .../datalifeengine/datalifeengine.dart | 4 +- anime/src/en/aniwave/aniwave.dart | 113 +++++++++++++++++- anime/src/fr/otakufr/otakufr.dart | 2 +- anime/src/id/nimegami/nimegami.dart | 2 +- manga/src/en/mangahere/mangahere.dart | 6 +- 6 files changed, 121 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac7c1885..0f6a6fe7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -333,7 +333,8 @@ See [`MDocument` model](https://github.com/kodjodevf/mangayomi/blob/main/lib/eva - `String` substringBeforeLast(`String text`, `String pattern`) ### Crypto utils -- `String` evalJs(`String code`); +- `String` unpackJs(`String code`); +- `Future` evalJs(`String code`); - `String` deobfuscateJsPassword(`String inputString`) - `String` encryptAESCryptoJS(`String plainText`, `String passphrase`) - `String` decryptAESCryptoJS(`String encrypted`, `String passphrase`) diff --git a/anime/multisrc/datalifeengine/datalifeengine.dart b/anime/multisrc/datalifeengine/datalifeengine.dart index e1dbe215..adc0728d 100644 --- a/anime/multisrc/datalifeengine/datalifeengine.dart +++ b/anime/multisrc/datalifeengine/datalifeengine.dart @@ -168,7 +168,7 @@ class DataLifeEngine extends MProvider { final res = await http('GET', json.encode({"url": url})); final masterUrl = substringBefore( substringAfter( - substringAfter(substringAfter(evalJs(res), "sources:"), "file:\""), + substringAfter(substringAfter(unpackJs(res), "sources:"), "file:\""), "src:\""), '"'); final masterPlaylistRes = @@ -203,7 +203,7 @@ class DataLifeEngine extends MProvider { return []; } final masterUrl = - substringBefore(substringAfter(evalJs(js.first), "{file:\""), "\"}"); + substringBefore(substringAfter(unpackJs(js.first), "{file:\""), "\"}"); final masterPlaylistRes = await http('GET', json.encode({"url": masterUrl})); List videos = []; diff --git a/anime/src/en/aniwave/aniwave.dart b/anime/src/en/aniwave/aniwave.dart index cbfd7b47..ec01807b 100644 --- a/anime/src/en/aniwave/aniwave.dart +++ b/anime/src/en/aniwave/aniwave.dart @@ -204,7 +204,13 @@ class Aniwave extends MProvider { final hosterSelection = preferenceHosterSelection(source.id); final typeSelection = preferenceTypeSelection(source.id); if (typeSelection.contains(type.toLowerCase())) { - if (url.contains("mp4upload") && + if (url.contains("vidplay") || url.contains("mcloud")) { + final hosterName = + url.contains("vidplay") ? "VidPlay" : "MyCloud"; + if (hosterSelection.contains(hosterName.toLowerCase())) { + a = await vidsrcExtractor(url, hosterName, type); + } + } else if (url.contains("mp4upload") && hosterSelection.contains("mp4upload")) { a = await mp4UploadExtractor(url, null, "", type); } else if (url.contains("streamtape") && @@ -305,6 +311,111 @@ class Aniwave extends MProvider { return vrf; } + Future> vidsrcExtractor( + String url, String name, String type) async { + List keys = json.decode(await http( + 'GET', + json.encode({ + "url": + "https://raw.githubusercontent.com/Claudemirovsky/worstsource-keys/keys/keys.json" + }))); + final host = Uri.parse(url).host; + final apiUrl = await getApiUrl(url, keys); + final headers = { + "Accept": "application/json, text/javascript, */*; q=0.01", + "Host": host, + "Referer": Uri.decodeComponent(url), + "X-Requested-With": "XMLHttpRequest" + }; + final res = + await http('GET', json.encode({"url": apiUrl, "headers": headers})); + if (res == "error") return []; + String masterUrl = + ((json.decode(res)['result']['sources'] as List>) + .first)['file']; + final tracks = (json.decode(res)['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 (_) {} + } + List videoList = []; + final masterPlaylistRes = + await http('GET', json.encode({"url": masterUrl})); + 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; + } + + Future getApiUrl(String url, List keyList) async { + 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, "/"), "?"); + var encodedID = encodeID(vidId, keyList); + final apiSlug = await callFromFuToken(host, encodedID); + String apiUrlString = ""; + apiUrlString += "https://$host/$apiSlug"; + if (paramsToString.isNotEmpty) { + apiUrlString += "?$paramsToString"; + } + + return apiUrlString; + } + + String encodeID(String vidId, List keyList) { + var rc4Key1 = keyList[0]; + var rc4Key2 = keyList[1]; + final rc4 = rc4Encrypt(rc4Key1, vidId.codeUnits); + final rc41 = rc4Encrypt(rc4Key2, rc4); + return base64.encode(rc41).replaceAll("/", "_").trim(); + } + + Future callFromFuToken(String host, String data) async { + final fuTokenScript = + await http('GET', json.encode({"url": "https://$host/futoken"})); + 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(MSource source) { return [ diff --git a/anime/src/fr/otakufr/otakufr.dart b/anime/src/fr/otakufr/otakufr.dart index 86fca4d9..03b3fcc2 100644 --- a/anime/src/fr/otakufr/otakufr.dart +++ b/anime/src/fr/otakufr/otakufr.dart @@ -362,7 +362,7 @@ class OtakuFr extends MProvider { return []; } final masterUrl = - substringBefore(substringAfter(evalJs(js.first), "{file:\""), "\"}"); + substringBefore(substringAfter(unpackJs(js.first), "{file:\""), "\"}"); final masterPlaylistRes = await http('GET', json.encode({"url": masterUrl})); List videos = []; diff --git a/anime/src/id/nimegami/nimegami.dart b/anime/src/id/nimegami/nimegami.dart index 61bc269e..b7402e08 100644 --- a/anime/src/id/nimegami/nimegami.dart +++ b/anime/src/id/nimegami/nimegami.dart @@ -152,7 +152,7 @@ class NimeGami extends MProvider { '//script[contains(text(), "eval") and contains(text(), "p,a,c,k,e,d")]/text()'); if (script.isNotEmpty) { final videoUrl = substringBefore( - substringAfter(substringAfter(evalJs(script.first), "sources:[", ""), + substringAfter(substringAfter(unpackJs(script.first), "sources:[", ""), "file\":\"", ""), '"'); if (videoUrl.isNotEmpty) { diff --git a/manga/src/en/mangahere/mangahere.dart b/manga/src/en/mangahere/mangahere.dart index 91188591..ea4b431d 100644 --- a/manga/src/en/mangahere/mangahere.dart +++ b/manga/src/en/mangahere/mangahere.dart @@ -181,7 +181,7 @@ class MangaHere extends MProvider { res, "//script[contains(text(),'function(p,a,c,k,e,d)')]/text()") .first .replaceAll("eval", ""); - String deobfuscatedScript = evalJs(script); + String deobfuscatedScript = unpackJs(script); int a = deobfuscatedScript.indexOf("newImgs=['") + 10; int b = deobfuscatedScript.indexOf("'];"); List urls = deobfuscatedScript.substring(a, b).split("','"); @@ -197,7 +197,7 @@ class MangaHere extends MProvider { String secretKeyScript = res .substring(secretKeyScriptLocation, secretKeyScriptEndLocation) .replaceAll("eval", ""); - String secretKeyDeobfuscatedScript = evalJs(secretKeyScript); + String secretKeyDeobfuscatedScript = unpackJs(secretKeyScript); int secretKeyStartLoc = secretKeyDeobfuscatedScript.indexOf("'"); int secretKeyEndLoc = secretKeyDeobfuscatedScript.indexOf(";"); @@ -231,7 +231,7 @@ class MangaHere extends MProvider { } } } - String deobfuscatedScript = evalJs(responseText.replaceAll("eval", "")); + String deobfuscatedScript = unpackJs(responseText.replaceAll("eval", "")); int baseLinkStartPos = deobfuscatedScript.indexOf("pix=") + 5; int baseLinkEndPos = From cdbcf4fa90eda2055cec56d6460e252a48042497 Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Thu, 28 Dec 2023 19:37:28 +0100 Subject: [PATCH 2/2] New source: Anime-Sama (FR) --- anime/source_generator.dart | 4 +- anime/src/en/aniwave/source.dart | 1 + anime/src/fr/animesama/animesama.dart | 405 ++++++++++++++++++++++++++ anime/src/fr/animesama/icon.png | Bin 0 -> 4666 bytes anime/src/fr/animesama/source.dart | 17 ++ 5 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 anime/src/fr/animesama/animesama.dart create mode 100644 anime/src/fr/animesama/icon.png create mode 100644 anime/src/fr/animesama/source.dart diff --git a/anime/source_generator.dart b/anime/source_generator.dart index e5a03c19..91e5a54e 100644 --- a/anime/source_generator.dart +++ b/anime/source_generator.dart @@ -9,6 +9,7 @@ import 'src/ar/okanime/source.dart'; import 'src/en/aniwave/source.dart'; import 'src/en/dramacool/source.dart'; import 'src/en/gogoanime/source.dart'; +import 'src/fr/animesama/source.dart'; import 'src/hi/yomovies/source.dart'; import 'src/en/kisskh/source.dart'; import 'src/en/uhdmovies/source.dart'; @@ -40,7 +41,8 @@ void main() { ...datalifeengineSourcesList, filma24, dramacoolSource, - yomoviesSource + yomoviesSource, + animesamaSource ]; final List> jsonList = _sourcesList.map((source) => source.toJson()).toList(); diff --git a/anime/src/en/aniwave/source.dart b/anime/src/en/aniwave/source.dart index ed031573..66551732 100644 --- a/anime/src/en/aniwave/source.dart +++ b/anime/src/en/aniwave/source.dart @@ -13,4 +13,5 @@ Source _aniwave = Source( "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/anime/src/en/aniwave/icon.png", sourceCodeUrl: _aniwaveCodeUrl, version: _aniwaveVersion, + appMinVerReq: "0.1.5", isManga: false); diff --git a/anime/src/fr/animesama/animesama.dart b/anime/src/fr/animesama/animesama.dart new file mode 100644 index 00000000..ad8287ba --- /dev/null +++ b/anime/src/fr/animesama/animesama.dart @@ -0,0 +1,405 @@ +import 'package:mangayomi/bridge_lib.dart'; +import 'dart:convert'; + +class AnimeSama extends MProvider { + AnimeSama(); + + @override + Future getPopular(MSource source, int page) async { + final data = {"url": "${source.baseUrl}/#$page"}; + final doc = await http('GET', json.encode(data)); + 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(MSource source, int page) async { + final res = await http('GET', json.encode({"url": source.baseUrl})); + 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( + MSource source, String query, int page, FilterList filterList) async { + final filters = filterList.filters; + final res = await http('GET', + json.encode({"url": "${source.baseUrl}/catalogue/listing_all.php"})); + 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(MSource source, String url) async { + var animeUrl = + "${source.baseUrl}${substringBeforeLast(Uri.parse(url).path, "/")}"; + 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(MSource source, 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(MSource source) { + 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(MSource source) { + 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 http('GET', json.encode({"url": url})); + 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 http('GET', json.encode({"url": moviesUrl})); + 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 http('GET', json.encode({"url": docUrl})); + + 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() { + return AnimeSama(); +} diff --git a/anime/src/fr/animesama/icon.png b/anime/src/fr/animesama/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cca54e146a7a80b3fbcc6b540fd7fe27c96aadf8 GIT binary patch literal 4666 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 S#4PYn2y`2T(XG&S2>l _animesama; +const animesamaVersion = "0.0.1"; +const animesamaCodeUrl = + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/anime/src/fr/animesama/animesama.dart"; +Source _animesama = Source( + name: "Anime-Sama", + baseUrl: "https://anime-sama.fr", + lang: "fr", + typeSource: "single", + iconUrl: + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/anime/src/fr/animesama/icon.png", + sourceCodeUrl: animesamaCodeUrl, + appMinVerReq: "0.1.5", + version: animesamaVersion, + isManga: false);