From e0c2be06a362c1998c59224ecded255e355a35cf Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Sat, 2 Dec 2023 18:58:02 +0100 Subject: [PATCH 1/5] add source preference --- ...flix-v0.0.15.dart => dopeflix-v0.0.2.dart} | 158 ++++++++++++-- anime/multisrc/dopeflix/sources.dart | 2 +- anime/multisrc/zorotheme/sources.dart | 2 +- ...eme-v0.0.5.dart => zorotheme-v0.0.55.dart} | 116 +++++++++- ...iwave-v0.0.15.dart => aniwave-v0.0.2.dart} | 200 +++++++++++++++--- anime/src/en/aniwave/source.dart | 2 +- ...nime-v0.0.4.dart => gogoanime-v0.0.5.dart} | 164 ++++++++++++-- anime/src/en/gogoanime/source.dart | 2 +- model/source.dart | 2 +- 9 files changed, 571 insertions(+), 77 deletions(-) rename anime/multisrc/dopeflix/{dopeflix-v0.0.15.dart => dopeflix-v0.0.2.dart} (75%) rename anime/multisrc/zorotheme/{zorotheme-v0.0.5.dart => zorotheme-v0.0.55.dart} (79%) rename anime/src/en/aniwave/{aniwave-v0.0.15.dart => aniwave-v0.0.2.dart} (72%) rename anime/src/en/gogoanime/{gogoanime-v0.0.4.dart => gogoanime-v0.0.5.dart} (87%) diff --git a/anime/multisrc/dopeflix/dopeflix-v0.0.15.dart b/anime/multisrc/dopeflix/dopeflix-v0.0.2.dart similarity index 75% rename from anime/multisrc/dopeflix/dopeflix-v0.0.15.dart rename to anime/multisrc/dopeflix/dopeflix-v0.0.2.dart index f4866fd2..38c62203 100644 --- a/anime/multisrc/dopeflix/dopeflix-v0.0.15.dart +++ b/anime/multisrc/dopeflix/dopeflix-v0.0.2.dart @@ -6,18 +6,21 @@ class DopeFlix extends MProvider { @override Future getPopular(MSource source, int page) async { - final data = {"url": "${source.baseUrl}/movie?page=$page"}; + final data = { + "url": + "${preferenceBaseUrl(source.id)}/${getPreferenceValue(source.id, "preferred_popular_page")}?page=$page" + }; final res = await http('GET', json.encode(data)); return parseAnimeList(res); } @override Future getLatestUpdates(MSource source, int page) async { - final data = {"url": "${source.baseUrl}/home"}; + final data = {"url": "${preferenceBaseUrl(source.id)}/home"}; final res = await http('GET', json.encode(data)); List animeList = []; final path = - '//section[contains(text(),"Latest Movies")]/div/div[@class="film_list-wrap"]/div[@class="flw-item"]/div[@class="film-poster"]'; + '//section[contains(text(),"${getPreferenceValue(source.id, "preferred_latest_page")}")]/div/div[@class="film_list-wrap"]/div[@class="flw-item"]/div[@class="film-poster"]'; final urls = xpath(res, '$path/a/@href'); final names = xpath(res, '$path/a/@title'); final images = xpath(res, '$path/img/@data-src'); @@ -36,7 +39,7 @@ class DopeFlix extends MProvider { Future search( MSource source, String query, int page, FilterList filterList) async { final filters = filterList.filters; - String url = "${source.baseUrl}"; + String url = "${preferenceBaseUrl(source.id)}"; if (query.isNotEmpty) { url += "/search/${query.replaceAll(" ", "-")}?page=$page"; @@ -79,7 +82,7 @@ class DopeFlix extends MProvider { @override Future getDetail(MSource source, String url) async { url = Uri.parse(url).path; - final data = {"url": "${source.baseUrl}$url"}; + final data = {"url": "${preferenceBaseUrl(source.id)}$url"}; final res = await http('GET', json.encode(data)); MManga anime = MManga(); final description = xpath(res, '//div[@class="description"]/text()'); @@ -98,10 +101,12 @@ class DopeFlix extends MProvider { if (dataType == "1") { MChapter episode = MChapter(); episode.name = "Movie"; - episode.url = "${source.baseUrl}/ajax/movie/episodes/$id"; + episode.url = "${preferenceBaseUrl(source.id)}/ajax/movie/episodes/$id"; episodesList.add(episode); } else { - final dataS = {"url": "${source.baseUrl}/ajax/v2/tv/seasons/$id"}; + final dataS = { + "url": "${preferenceBaseUrl(source.id)}/ajax/v2/tv/seasons/$id" + }; final resS = await http('GET', json.encode(dataS)); final seasonIds = @@ -112,7 +117,8 @@ class DopeFlix extends MProvider { final seasonId = seasonIds[i]; final seasonName = seasonNames[i]; final dataE = { - "url": "${source.baseUrl}/ajax/v2/season/episodes/$seasonId" + "url": + "${preferenceBaseUrl(source.id)}/ajax/v2/season/episodes/$seasonId" }; final html = await http('GET', json.encode(dataE)); final epsHtml = querySelectorAll(html, @@ -120,7 +126,8 @@ class DopeFlix extends MProvider { typeElement: 2, attributes: "", typeRegExp: 0); - print("${source.baseUrl}/ajax/v2/season/episodes/$seasonId"); + print( + "${preferenceBaseUrl(source.id)}/ajax/v2/season/episodes/$seasonId"); for (var epHtml in epsHtml) { final episodeId = xpath(epHtml, '//div[contains(@class,"eps-item")]/@data-id') @@ -130,7 +137,8 @@ class DopeFlix extends MProvider { final epName = xpath(epHtml, '//h3[@class="film-name"]/text()').first; MChapter episode = MChapter(); episode.name = "$seasonName $epNum $epName"; - episode.url = "${source.baseUrl}/ajax/v2/episode/servers/$episodeId"; + episode.url = + "${preferenceBaseUrl(source.id)}/ajax/v2/episode/servers/$episodeId"; episodesList.add(episode); } } @@ -142,8 +150,8 @@ class DopeFlix extends MProvider { @override Future> getVideoList(MSource source, String url) async { url = Uri.parse(url).path; - final res = - await http('GET', json.encode({"url": "${source.baseUrl}/$url"})); + final res = await http( + 'GET', json.encode({"url": "${preferenceBaseUrl(source.id)}/$url"})); final vidsHtml = querySelectorAll(res, selector: "ul.fss-list a.btn-play", typeElement: 2, @@ -154,7 +162,9 @@ class DopeFlix extends MProvider { final id = xpath(vidHtml, '//a/@data-id').first; final name = xpath(vidHtml, '//span/text()').first; final resSource = await http( - 'GET', json.encode({"url": "${source.baseUrl}/ajax/sources/$id"})); + 'GET', + json.encode( + {"url": "${preferenceBaseUrl(source.id)}/ajax/sources/$id"})); final vidUrl = substringBefore(substringAfter(resSource, "\"link\":\""), "\""); List a = []; @@ -212,6 +222,7 @@ class DopeFlix extends MProvider { subtitles.add(subtitle); } catch (_) {} } + subtitles = sortSubs(subtitles, source.id); if (type == "hls") { final masterPlaylistRes = await http('GET', json.encode({"url": masterUrl})); @@ -248,7 +259,7 @@ class DopeFlix extends MProvider { videos.addAll(a); } - return videos; + return sortVideos(videos, source.id); } Future>> generateIndexPairs() async { @@ -261,8 +272,8 @@ class DopeFlix extends MProvider { script = script.substring(0, script.lastIndexOf(',')); final list = script .split(",") - .map((e) { - String value = substringAfter((e as String), "="); + .map((String e) { + String value = substringAfter(e, "="); if (value.contains("0x")) { return int.parse(substringAfter(value, "0x"), radix: 16); } else { @@ -273,7 +284,7 @@ class DopeFlix extends MProvider { .skip(1) .toList(); return chunked(list, 2) - .map((list) => (list as List).reversed.toList()) + .map((List list) => list.reversed.toList()) .toList(); } @@ -405,6 +416,119 @@ class DopeFlix extends MProvider { ]; } + @override + List getSourcePreferences(MSource source) { + return [ + ListPreference( + key: "preferred_domain", + title: "Preferred domain", + summary: "", + valueIndex: 0, + entries: ["dopebox.to", "dopebox.se"], + entryValues: ["https://dopebox.to", "https://dopebox.se"]), + ListPreference( + key: "preferred_quality", + title: "Preferred Quality", + summary: "", + valueIndex: 0, + entries: ["1080p", "720p", "480p", "360p"], + entryValues: ["1080p", "720p", "480p", "360p"]), + ListPreference( + key: "preferred_subLang", + title: "Preferred sub language", + summary: "", + valueIndex: 1, + entries: [ + "Arabic", + "English", + "French", + "German", + "Hungarian", + "Italian", + "Japanese", + "Portuguese", + "Romanian", + "Russian", + "Spanish" + ], + entryValues: [ + "Arabic", + "English", + "French", + "German", + "Hungarian", + "Italian", + "Japanese", + "Portuguese", + "Romanian", + "Russian", + "Spanish" + ]), + ListPreference( + key: "preferred_latest_page", + title: "Preferred latest page", + summary: "", + valueIndex: 0, + entries: ["Movies", "TV Shows"], + entryValues: ["Latest Movies", "Latest TV Shows"]), + ListPreference( + key: "preferred_popular_page", + title: "Preferred popular page", + summary: "", + valueIndex: 0, + entries: ["Movies", "TV Shows"], + entryValues: ["movie", "tv-show"]), + ]; + } + + 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; + } + + List sortSubs(List subs, int sourceId) { + String lang = getPreferenceValue(sourceId, "preferred_subLang"); + + subs.sort((MTrack a, MTrack b) { + int langMatchA = 0; + if (a.label.toLowerCase().contains(lang.toLowerCase())) { + langMatchA = 1; + } + int langMatchB = 0; + if (b.label.toLowerCase().contains(lang.toLowerCase())) { + langMatchB = 1; + } + return langMatchB - langMatchA; + }); + return subs; + } + + String preferenceBaseUrl(int sourceId) { + return getPreferenceValue(sourceId, "preferred_domain"); + } + String ll(String url) { if (url.contains("?")) { return "&"; diff --git a/anime/multisrc/dopeflix/sources.dart b/anime/multisrc/dopeflix/sources.dart index b86b0fef..4bb6f18f 100644 --- a/anime/multisrc/dopeflix/sources.dart +++ b/anime/multisrc/dopeflix/sources.dart @@ -1,7 +1,7 @@ import '../../../model/source.dart'; import '../../../utils/utils.dart'; -const dopeflixVersion = "0.0.15"; +const dopeflixVersion = "0.0.2"; const dopeflixSourceCodeUrl = "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/anime/multisrc/dopeflix/dopeflix-v$dopeflixVersion.dart"; diff --git a/anime/multisrc/zorotheme/sources.dart b/anime/multisrc/zorotheme/sources.dart index 80a15686..66715cea 100644 --- a/anime/multisrc/zorotheme/sources.dart +++ b/anime/multisrc/zorotheme/sources.dart @@ -1,7 +1,7 @@ import '../../../model/source.dart'; import '../../../utils/utils.dart'; -const zorothemeVersion = "0.0.5"; +const zorothemeVersion = "0.0.55"; const zorothemeSourceCodeUrl = "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/anime/multisrc/zorotheme/zorotheme-v$zorothemeVersion.dart"; diff --git a/anime/multisrc/zorotheme/zorotheme-v0.0.5.dart b/anime/multisrc/zorotheme/zorotheme-v0.0.55.dart similarity index 79% rename from anime/multisrc/zorotheme/zorotheme-v0.0.5.dart rename to anime/multisrc/zorotheme/zorotheme-v0.0.55.dart index affc3b4a..ad0cb3b1 100644 --- a/anime/multisrc/zorotheme/zorotheme-v0.0.5.dart +++ b/anime/multisrc/zorotheme/zorotheme-v0.0.55.dart @@ -223,7 +223,8 @@ class ZoroTheme extends MProvider { typeRegExp: 0); List videos = []; - + final hosterSelection = preferenceHosterSelection(source.id); + final typeSelection = preferenceTypeSelection(source.id); for (var i = 0; i < names.length; i++) { final name = names[i]; final id = ids[i]; @@ -236,19 +237,22 @@ class ZoroTheme extends MProvider { final resE = await http('GET', json.encode(datasE)); String epUrl = substringBefore(substringAfter(resE, "\"link\":\""), "\""); - print(epUrl); + List a = []; - if (name.contains("Vidstreaming")) { - a = await rapidCloudExtractor(epUrl, "Vidstreaming - $subDub"); - } else if (name.contains("Vidcloud")) { - a = await rapidCloudExtractor(epUrl, "Vidcloud - $subDub"); - } else if (name.contains("StreamTape")) { - a = await streamTapeExtractor(epUrl, "StreamTape - $subDub"); + + if (hosterSelection.contains(name) && typeSelection.contains(subDub)) { + if (name.contains("Vidstreaming")) { + a = await rapidCloudExtractor(epUrl, "Vidstreaming - $subDub"); + } else if (name.contains("Vidcloud")) { + a = await rapidCloudExtractor(epUrl, "Vidcloud - $subDub"); + } else if (name.contains("StreamTape")) { + a = await streamTapeExtractor(epUrl, "StreamTape - $subDub"); + } + videos.addAll(a); } - videos.addAll(a); } - return videos; + return sortVideos(videos, source.id); } MPages animeElementM(String res) { @@ -421,6 +425,98 @@ class ZoroTheme extends MProvider { ]; } + @override + List getSourcePreferences(MSource source) { + return [ + ListPreference( + key: "preferred_quality", + title: "Preferred Quality", + summary: "", + valueIndex: 1, + entries: ["1080p", "720p", "480p", "360p"], + entryValues: ["1080", "720", "480", "360"]), + ListPreference( + key: "preferred_server", + title: "Preferred server", + summary: "", + valueIndex: 0, + entries: ["Vidstreaming", "VidCloud", "StreamTape"], + entryValues: ["Vidstreaming", "VidCloud", "StreamTape"]), + ListPreference( + key: "preferred_type", + title: "Preferred Type", + summary: "", + valueIndex: 0, + entries: ["Sub", "Dub"], + entryValues: ["sub", "dub"]), + MultiSelectListPreference( + key: "hoster_selection", + title: "Enable/Disable Hosts", + summary: "", + entries: ["Vidstreaming", "VidCloud", "StreamTape"], + entryValues: ["Vidstreaming", "Vidcloud", "StreamTape"], + values: ["Vidstreaming", "Vidcloud", "StreamTape"]), + MultiSelectListPreference( + key: "type_selection", + title: "Enable/Disable Types", + summary: "", + entries: ["Sub", "Dub"], + entryValues: ["sub", "dub"], + values: ["sub", "dub"]), + ]; + } + + List sortVideos(List videos, int sourceId) { + String quality = getPreferenceValue(sourceId, "preferred_quality"); + String server = getPreferenceValue(sourceId, "preferred_server"); + String type = getPreferenceValue(sourceId, "preferred_type"); + videos = videos + .where( + (MVideo e) => e.quality.toLowerCase().contains(type.toLowerCase())) + .toList(); + 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; + }); + + videos.sort((MVideo a, MVideo b) { + int serverMatchA = 0; + if (a.quality.toLowerCase().contains(server.toLowerCase())) { + serverMatchA = 1; + } + int serverMatchB = 0; + if (b.quality.toLowerCase().contains(server.toLowerCase())) { + serverMatchB = 1; + } + return serverMatchB - serverMatchA; + }); + return videos; + } + + List preferenceHosterSelection(int sourceId) { + return getPreferenceValue(sourceId, "hoster_selection"); + } + + List preferenceTypeSelection(int sourceId) { + return getPreferenceValue(sourceId, "type_selection"); + } + String ll(String url) { if (url.contains("?")) { return "&"; diff --git a/anime/src/en/aniwave/aniwave-v0.0.15.dart b/anime/src/en/aniwave/aniwave-v0.0.2.dart similarity index 72% rename from anime/src/en/aniwave/aniwave-v0.0.15.dart rename to anime/src/en/aniwave/aniwave-v0.0.2.dart index eacd89e7..beb8d443 100644 --- a/anime/src/en/aniwave/aniwave-v0.0.15.dart +++ b/anime/src/en/aniwave/aniwave-v0.0.2.dart @@ -6,7 +6,9 @@ class Aniwave extends MProvider { @override Future getPopular(MSource source, int page) async { - final data = {"url": "${source.baseUrl}/filter?sort=trending&page=$page"}; + final data = { + "url": "${preferenceBaseUrl(source.id)}/filter?sort=trending&page=$page" + }; final res = await http('GET', json.encode(data)); return parseAnimeList(res); } @@ -14,7 +16,8 @@ class Aniwave extends MProvider { @override Future getLatestUpdates(MSource source, int page) async { final data = { - "url": "${source.baseUrl}/filter?sort=recently_updated&page=$page" + "url": + "${preferenceBaseUrl(source.id)}/filter?sort=recently_updated&page=$page" }; final res = await http('GET', json.encode(data)); return parseAnimeList(res); @@ -24,7 +27,7 @@ class Aniwave extends MProvider { Future search( MSource source, String query, int page, FilterList filterList) async { final filters = filterList.filters; - String url = "${source.baseUrl}/filter?keyword=$query"; + String url = "${preferenceBaseUrl(source.id)}/filter?keyword=$query"; for (var filter in filters) { if (filter.type == "OrderFilter") { @@ -98,7 +101,7 @@ class Aniwave extends MProvider { final statusList = [ {"Releasing": 0, "Completed": 1} ]; - final data = {"url": "${source.baseUrl}${url}"}; + final data = {"url": "${preferenceBaseUrl(source.id)}${url}"}; final res = await http('GET', json.encode(data)); MManga anime = MManga(); final status = xpath(res, '//div[contains(text(),"Status")]/span/text()'); @@ -124,7 +127,9 @@ class Aniwave extends MProvider { .first; final encrypt = vrfEncrypt(id); final vrf = "vrf=${Uri.encodeComponent(encrypt)}"; - final dataEp = {"url": "${source.baseUrl}/ajax/episode/list/$id?$vrf"}; + final dataEp = { + "url": "${preferenceBaseUrl(source.id)}/ajax/episode/list/$id?$vrf" + }; final resEp = await http('GET', json.encode(dataEp)); final html = json.decode(resEp)["result"]; List? episodesList = []; @@ -172,8 +177,11 @@ class Aniwave extends MProvider { final ids = substringBefore(url, "&"); final encrypt = vrfEncrypt(ids); final vrf = "vrf=${Uri.encodeComponent(encrypt)}"; - final res = await http('GET', - json.encode({"url": "${source.baseUrl}/ajax/server/list/$ids?$vrf"})); + final res = await http( + 'GET', + json.encode({ + "url": "${preferenceBaseUrl(source.id)}/ajax/server/list/$ids?$vrf" + })); final html = json.decode(res)["result"]; final vidsHtml = querySelectorAll(html, selector: "div.servers > div", @@ -191,25 +199,34 @@ class Aniwave extends MProvider { final vrf = "vrf=${Uri.encodeComponent(encrypt)}"; final res = await http( 'GET', - json.encode( - {"url": "${source.baseUrl}/ajax/server/$serverId?$vrf"})); + json.encode({ + "url": + "${preferenceBaseUrl(source.id)}/ajax/server/$serverId?$vrf" + })); final status = json.decode(res)["status"]; if (status == 200) { List a = []; final url = vrfDecrypt(json.decode(res)["result"]["url"]); - if (url.contains("mp4upload")) { - a = await mp4UploadExtractor(url, null, "", type); - } else if (url.contains("streamtape")) { - a = await streamTapeExtractor(url, "StreamTape - $type"); - } else if (url.contains("filemoon")) { - a = await filemoonExtractor(url, "", type); + final hosterSelection = preferenceHosterSelection(source.id); + final typeSelection = preferenceTypeSelection(source.id); + if (typeSelection.contains(type.toLowerCase())) { + if (url.contains("mp4upload") && + hosterSelection.contains("mp4upload")) { + a = await mp4UploadExtractor(url, null, "", type); + } else if (url.contains("streamtape") && + hosterSelection.contains("streamtape")) { + a = await streamTapeExtractor(url, "StreamTape - $type"); + } else if (url.contains("filemoon") && + hosterSelection.contains("filemoon")) { + a = await filemoonExtractor(url, "", type); + } + videos.addAll(a); } - videos.addAll(a); } } } - return videos; + return sortVideos(videos, source.id); } MPages parseAnimeList(String res) { @@ -427,6 +444,147 @@ class Aniwave extends MProvider { ]; } + @override + List getSourcePreferences(MSource source) { + return [ + ListPreference( + key: "preferred_domain", + title: "Preferred domain", + summary: "", + valueIndex: 0, + entries: [ + "aniwave.to", + "aniwave.bz", + "aniwave.ws" + ], + entryValues: [ + "https://aniwave.to", + "https://aniwave.bz", + "https://aniwave.ws" + ]), + 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_server", + title: "Preferred server", + summary: "", + valueIndex: 0, + entries: [ + "VidPlay", + "MyCloud", + "Filemoon", + "StreamTape", + "Mp4Upload" + ], + entryValues: [ + "vidplay", + "mycloud", + "filemoon", + "streamtape", + "mp4upload" + ]), + MultiSelectListPreference( + key: "hoster_selection", + title: "Enable/Disable Hosts", + summary: "", + entries: [ + "VidPlay", + "MyCloud", + "Filemoon", + "StreamTape", + "Mp4Upload" + ], + entryValues: [ + "vidplay", + "mycloud", + "filemoon", + "streamtape", + "mp4upload" + ], + values: [ + "vidplay", + "mycloud", + "filemoon", + "streamtape", + "mp4upload" + ]), + MultiSelectListPreference( + key: "type_selection", + title: "Enable/Disable Type", + summary: "", + entries: ["Sub", "Softsub", "Dub"], + entryValues: ["sub", "softsub", "dub"], + values: ["sub", "softsub", "dub"]), + ]; + } + + String preferenceBaseUrl(int sourceId) { + return getPreferenceValue(sourceId, "preferred_domain"); + } + + List preferenceHosterSelection(int sourceId) { + return getPreferenceValue(sourceId, "hoster_selection"); + } + + 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_server"); + String lang = getPreferenceValue(sourceId, "preferred_language"); + videos = videos + .where( + (MVideo e) => e.quality.toLowerCase().contains(lang.toLowerCase())) + .toList(); + 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; + }); + + videos.sort((MVideo a, MVideo b) { + int serverMatchA = 0; + if (a.quality.toLowerCase().contains(server.toLowerCase())) { + serverMatchA = 1; + } + int serverMatchB = 0; + if (b.quality.toLowerCase().contains(server.toLowerCase())) { + serverMatchB = 1; + } + return serverMatchB - serverMatchA; + }); + return videos; + } + String ll(String url) { if (url.contains("?")) { return "&"; @@ -435,14 +593,6 @@ class Aniwave extends MProvider { } } -Map getMirrorPref() { - return { - "aniwave.to": "https://aniwave.to", - "aniwave.bz": "https://aniwave.bz", - "aniwave.ws": "https://aniwave.ws", - }; -} - Aniwave main() { return Aniwave(); } diff --git a/anime/src/en/aniwave/source.dart b/anime/src/en/aniwave/source.dart index f0e53a31..abcbad96 100644 --- a/anime/src/en/aniwave/source.dart +++ b/anime/src/en/aniwave/source.dart @@ -2,7 +2,7 @@ import '../../../../model/source.dart'; import '../../../../utils/utils.dart'; Source get aniwave => _aniwave; -const aniwaveVersion = "0.0.15"; +const aniwaveVersion = "0.0.2"; const aniwaveCodeUrl = "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/anime/src/en/aniwave/aniwave-v$aniwaveVersion.dart"; Source _aniwave = Source( diff --git a/anime/src/en/gogoanime/gogoanime-v0.0.4.dart b/anime/src/en/gogoanime/gogoanime-v0.0.5.dart similarity index 87% rename from anime/src/en/gogoanime/gogoanime-v0.0.4.dart rename to anime/src/en/gogoanime/gogoanime-v0.0.5.dart index b6eab5d9..2d63ecf0 100644 --- a/anime/src/en/gogoanime/gogoanime-v0.0.4.dart +++ b/anime/src/en/gogoanime/gogoanime-v0.0.5.dart @@ -6,7 +6,9 @@ class GogoAnime extends MProvider { @override Future getPopular(MSource source, int page) async { - final data = {"url": "${source.baseUrl}/popular.html?page=$page"}; + final data = { + "url": "${preferenceBaseUrl(source.id)}/popular.html?page=$page" + }; final res = await http('GET', json.encode(data)); List animeList = []; @@ -130,14 +132,15 @@ class GogoAnime extends MProvider { } } if (genre.isNotEmpty) { - url = "${source.baseUrl}/genre/$genre?page=$page"; + url = "${preferenceBaseUrl(source.id)}/genre/$genre?page=$page"; } else if (recent.isNotEmpty) { url = "https://ajax.gogo-load.com/ajax/page-recent-release.html?page=$page&type=$recent"; } else if (season.isNotEmpty) { - url = "${source.baseUrl}/$season?page=$page"; + url = "${preferenceBaseUrl(source.id)}/$season?page=$page"; } else { - url = "${source.baseUrl}/filter.html?keyword=$query$filterStr&page=$page"; + url = + "${preferenceBaseUrl(source.id)}/filter.html?keyword=$query$filterStr&page=$page"; } final data = {"url": url}; @@ -167,7 +170,7 @@ class GogoAnime extends MProvider { "Completed": 1, } ]; - final data = {"url": "${source.baseUrl}$url"}; + final data = {"url": "${preferenceBaseUrl(source.id)}$url"}; final res = await http('GET', json.encode(data)); MManga anime = MManga(); final status = xpath( @@ -213,7 +216,7 @@ class GogoAnime extends MProvider { @override Future> getVideoList(MSource source, String url) async { - final datas = {"url": "${source.baseUrl}$url"}; + final datas = {"url": "${preferenceBaseUrl(source.id)}$url"}; final res = await http('GET', json.encode(datas)); final serverUrls = @@ -221,27 +224,30 @@ class GogoAnime extends MProvider { final serverNames = xpath(res, '//*[@class="anime_muti_link"]/ul/li/@class'); List videos = []; + final hosterSelection = preferenceHosterSelection(source.id); for (var i = 0; i < serverNames.length; i++) { final name = serverNames[i]; final url = serverUrls[i]; List a = []; - if (name.contains("anime")) { - a = await gogoCdnExtractor(url); - } else if (name.contains("vidcdn")) { - a = await gogoCdnExtractor(url); - } else if (name.contains("doodstream")) { - a = await doodExtractor(url); - } else if (name.contains("mp4upload")) { - a = await mp4UploadExtractor(url, null, "", ""); - } else if (name.contains("filelions")) { - a = await streamWishExtractor(url, "FileLions"); - } else if (name.contains("streamwish")) { - a = await streamWishExtractor(url, "StreamWish"); + if (hosterSelection.contains(name)) { + if (name.contains("anime")) { + a = await gogoCdnExtractor(url); + } else if (name.contains("vidcdn")) { + a = await gogoCdnExtractor(url); + } else if (name.contains("doodstream")) { + a = await doodExtractor(url); + } else if (name.contains("mp4upload")) { + a = await mp4UploadExtractor(url, null, "", ""); + } else if (name.contains("filelions")) { + a = await streamWishExtractor(url, "FileLions"); + } else if (name.contains("streamwish")) { + a = await streamWishExtractor(url, "StreamWish"); + } + videos.addAll(a); } - videos.addAll(a); } - return videos; + return sortVideos(videos, source.id); } @override @@ -1025,6 +1031,124 @@ class GogoAnime extends MProvider { ]), ]; } + + @override + List getSourcePreferences(MSource source) { + return [ + EditTextPreference( + key: "override_baseurl_v${source.id}", + title: "Override BaseUrl", + summary: + "For temporary uses. Updating the extension will erase this setting.", + value: "https://gogoanime3.net", + dialogTitle: "Override BaseUrl", + dialogMessage: "Default: https://gogoanime3.net", + text: "https://gogoanime3.net"), + ListPreference( + key: "preferred_quality", + title: "Preferred quality", + summary: "", + valueIndex: 0, + entries: ["1080p", "720p", "480p", "360p"], + entryValues: ["1080", "720", "480", "360"]), + ListPreference( + key: "preferred_server", + title: "Preferred server", + summary: "", + valueIndex: 0, + entries: [ + "Gogostream", + "Vidstreaming", + "Doodstream", + "StreamWish", + "Mp4upload", + "FileLions" + ], + entryValues: [ + "Gogostream", + "Vidstreaming", + "Doodstream", + "StreamWish", + "Mp4upload", + "FileLions" + ]), + MultiSelectListPreference( + key: "hoster_selection", + title: "Enable/Disable Hosts", + summary: "", + entries: [ + "Gogostream", + "Vidstreaming", + "Doodstream", + "StreamWish", + "Mp4upload", + "FileLions" + ], + entryValues: [ + "vidcdn", + "anime", + "doodstream", + "streamwish", + "mp4upload", + "filelions" + ], + values: [ + "vidcdn", + "anime", + "doodstream", + "streamwish", + "mp4upload", + "filelions" + ]), + ]; + } + + String preferenceBaseUrl(int sourceId) { + return getPreferenceValue(sourceId, "override_baseurl_v$sourceId"); + } + + List preferenceHosterSelection(int sourceId) { + return getPreferenceValue(sourceId, "hoster_selection"); + } + + List sortVideos(List videos, int sourceId) { + String quality = getPreferenceValue(sourceId, "preferred_quality"); + String server = getPreferenceValue(sourceId, "preferred_server"); + + 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; + }); + + videos.sort((MVideo a, MVideo b) { + int serverMatchA = 0; + if (a.quality.contains(server)) { + serverMatchA = 1; + } + int serverMatchB = 0; + if (b.quality.contains(server)) { + serverMatchB = 1; + } + return serverMatchB - serverMatchA; + }); + return videos; + } } GogoAnime main() { diff --git a/anime/src/en/gogoanime/source.dart b/anime/src/en/gogoanime/source.dart index 88ef15c7..31bb14f5 100644 --- a/anime/src/en/gogoanime/source.dart +++ b/anime/src/en/gogoanime/source.dart @@ -2,7 +2,7 @@ import '../../../../model/source.dart'; import '../../../../utils/utils.dart'; Source get gogoanimeSource => _gogoanimeSource; -const gogoanimeVersion = "0.0.4"; +const gogoanimeVersion = "0.0.5"; const gogoanimeSourceCodeUrl = "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/anime/src/en/gogoanime/gogoanime-v$gogoanimeVersion.dart"; Source _gogoanimeSource = Source( diff --git a/model/source.dart b/model/source.dart index ce9b7335..23f29ba8 100644 --- a/model/source.dart +++ b/model/source.dart @@ -47,7 +47,7 @@ class Source { this.version = "", this.isManga = true, this.isFullData = false, - this.appMinVerReq = "0.1.1"}); + this.appMinVerReq = "0.1.2"}); Map toJson() { return { From a2abfc537f9c16540ad37de419b73c70910c0db6 Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:23:34 +0100 Subject: [PATCH 2/5] add source preference --- anime/multisrc/dopeflix/dopeflix-v0.0.2.dart | 25 +- .../multisrc/zorotheme/zorotheme-v0.0.55.dart | 2 +- anime/src/ar/okanime/okanime-v0.0.3.dart | 95 +++- anime/src/en/aniwave/aniwave-v0.0.2.dart | 2 +- anime/src/en/gogoanime/gogoanime-v0.0.5.dart | 2 +- ...anime-v0.0.4.dart => franime-v0.0.45.dart} | 20 +- anime/src/fr/franime/source.dart | 2 +- anime/src/fr/otakufr/otakufr-v0.0.4.dart | 197 -------- anime/src/fr/otakufr/otakufr-v0.0.45.dart | 448 ++++++++++++++++++ anime/src/fr/otakufr/source.dart | 2 +- ...verz-v0.0.15.dart => oploverz-v0.0.2.dart} | 7 +- anime/src/id/oploverz/source.dart | 2 +- manga/multisrc/madara/madara-v0.0.4.dart | 2 +- .../mangareader/mangareader-v0.0.5.dart | 2 +- manga/multisrc/mmrcms/mmrcms-v0.0.4.dart | 2 +- manga/multisrc/nepnep/nepnep-v0.0.4.dart | 12 +- ...batoto-v0.0.4.dart => batoto-v0.0.45.dart} | 87 ++-- manga/src/all/batoto/sources.dart | 2 +- manga/src/all/comick/comick-v0.0.4.dart | 2 +- ...adex-v0.0.45.dart => mangadex-v0.0.5.dart} | 130 +++-- manga/src/all/mangadex/sources.dart | 2 +- manga/src/en/mangahere/mangahere-v0.0.4.dart | 2 +- 22 files changed, 722 insertions(+), 325 deletions(-) rename anime/src/fr/franime/{franime-v0.0.4.dart => franime-v0.0.45.dart} (94%) delete mode 100644 anime/src/fr/otakufr/otakufr-v0.0.4.dart create mode 100644 anime/src/fr/otakufr/otakufr-v0.0.45.dart rename anime/src/id/oploverz/{oploverz-v0.0.15.dart => oploverz-v0.0.2.dart} (95%) rename manga/src/all/batoto/{batoto-v0.0.4.dart => batoto-v0.0.45.dart} (97%) rename manga/src/all/mangadex/{mangadex-v0.0.45.dart => mangadex-v0.0.5.dart} (85%) diff --git a/anime/multisrc/dopeflix/dopeflix-v0.0.2.dart b/anime/multisrc/dopeflix/dopeflix-v0.0.2.dart index 38c62203..41d0c328 100644 --- a/anime/multisrc/dopeflix/dopeflix-v0.0.2.dart +++ b/anime/multisrc/dopeflix/dopeflix-v0.0.2.dart @@ -321,7 +321,7 @@ class DopeFlix extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ SelectFilter("TypeFilter", "Type", 0, [ SelectFilterOption("All", "all"), @@ -419,13 +419,22 @@ class DopeFlix extends MProvider { @override List getSourcePreferences(MSource source) { return [ - ListPreference( - key: "preferred_domain", - title: "Preferred domain", - summary: "", - valueIndex: 0, - entries: ["dopebox.to", "dopebox.se"], - entryValues: ["https://dopebox.to", "https://dopebox.se"]), + if (source.name == "DopeBox") + ListPreference( + key: "preferred_domain", + title: "Preferred domain", + summary: "", + valueIndex: 0, + entries: ["dopebox.to", "dopebox.se"], + entryValues: ["https://dopebox.to", "https://dopebox.se"]), + if (source.name == "SFlix") + ListPreference( + key: "preferred_domain", + title: "Preferred domain", + summary: "", + valueIndex: 0, + entries: ["sflix.to", "sflix.se"], + entryValues: ["https://sflix.to", "https://sflix.se"]), ListPreference( key: "preferred_quality", title: "Preferred Quality", diff --git a/anime/multisrc/zorotheme/zorotheme-v0.0.55.dart b/anime/multisrc/zorotheme/zorotheme-v0.0.55.dart index ad0cb3b1..c684e952 100644 --- a/anime/multisrc/zorotheme/zorotheme-v0.0.55.dart +++ b/anime/multisrc/zorotheme/zorotheme-v0.0.55.dart @@ -292,7 +292,7 @@ class ZoroTheme extends MProvider { ]; @override - List getFilterList() { + List getFilterList(MSource source) { return [ SelectFilter("TypeFilter", "Type", 0, [ SelectFilterOption("All", ""), diff --git a/anime/src/ar/okanime/okanime-v0.0.3.dart b/anime/src/ar/okanime/okanime-v0.0.3.dart index 8b5fdf02..6f7f1e77 100644 --- a/anime/src/ar/okanime/okanime-v0.0.3.dart +++ b/anime/src/ar/okanime/okanime-v0.0.3.dart @@ -10,12 +10,11 @@ class OkAnime extends MProvider { final res = await http('GET', json.encode(data)); List animeList = []; - final urls = xpath(res, - '//div[@class="section" and contains(text(),"افضل انميات")]/div[@class="section-content"]/div/div/div[contains(@class,"anime-card")]/div[@class="anime-title")]/h4/a/@href'); - final names = xpath(res, - '//div[@class="section" and contains(text(),"افضل انميات")]/div[@class="section-content"]/div/div/div[contains(@class,"anime-card")]/div[@class="anime-title")]/h4/a/text()'); - final images = xpath(res, - '//div[@class="section" and contains(text(),"افضل انميات")]/div[@class="section-content"]/div/div/div[contains(@class,"anime-card")]/div[@class="anime-image")]/img/@src'); + String path = + '//div[@class="section" and contains(text(),"افضل انميات")]/div[@class="section-content"]/div/div/div[contains(@class,"anime-card")]'; + final urls = xpath(res, '$path/div[@class="anime-title")]/h4/a/@href'); + final names = xpath(res, '$path/div[@class="anime-title")]/h4/a/text()'); + final images = xpath(res, '$path/div[@class="anime-image")]/img/@src'); for (var i = 0; i < names.length; i++) { MManga anime = MManga(); @@ -33,12 +32,10 @@ class OkAnime extends MProvider { final res = await http('GET', json.encode(data)); List animeList = []; - final urls = xpath(res, - '//*[contains(@class,"anime-card")]/div[@class="anime-title")]/h4/a/@href'); - final names = xpath(res, - '//*[contains(@class,"anime-card")]/div[@class="anime-title")]/h4/a/text()'); - final images = xpath(res, - '//*[contains(@class,"anime-card")]/div[@class="episode-image")]/img/@src'); + String path = '//*[contains(@class,"anime-card")]'; + final urls = xpath(res, '$path/div[@class="anime-title")]/h4/a/@href'); + final names = xpath(res, '$path/div[@class="anime-title")]/h4/a/text()'); + final images = xpath(res, '$path/div[@class="episode-image")]/img/@src'); for (var i = 0; i < names.length; i++) { MManga anime = MManga(); @@ -63,12 +60,10 @@ class OkAnime extends MProvider { final res = await http('GET', json.encode(data)); List animeList = []; - final urls = xpath(res, - '//*[contains(@class,"anime-card")]/div[@class="anime-title")]/h4/a/@href'); - final names = xpath(res, - '//*[contains(@class,"anime-card")]/div[@class="anime-title")]/h4/a/text()'); - final images = xpath(res, - '//*[contains(@class,"anime-card")]/div[@class="anime-image")]/img/@src'); + String path = '//*[contains(@class,"anime-card")]'; + final urls = xpath(res, '$path/div[@class="anime-title")]/h4/a/@href'); + final names = xpath(res, '$path/div[@class="anime-title")]/h4/a/text()'); + final images = xpath(res, '$path/div[@class="anime-image")]/img/@src'); for (var i = 0; i < names.length; i++) { MManga anime = MManga(); @@ -125,29 +120,81 @@ class OkAnime extends MProvider { final urls = xpath(res, '//*[@id="streamlinks"]/a/@data-src'); final qualities = xpath(res, '//*[@id="streamlinks"]/a/span/text()'); - + final hosterSelection = preferenceHosterSelection(source.id); List videos = []; for (var i = 0; i < urls.length; i++) { final url = urls[i]; final quality = getQuality(qualities[i]); List a = []; - if (url.contains("https://doo")) { + if (url.contains("https://doo") && hosterSelection.contains("Dood")) { a = await doodExtractor(url, "DoodStream - $quality"); - } else if (url.contains("mp4upload")) { + } else if (url.contains("mp4upload") && + hosterSelection.contains("Mp4upload")) { a = await mp4UploadExtractor(url, null, "", ""); - } else if (url.contains("ok.ru")) { + } else if (url.contains("ok.ru") && hosterSelection.contains("Okru")) { a = await okruExtractor(url); - } else if (url.contains("voe.sx")) { + } else if (url.contains("voe.sx") && hosterSelection.contains("Voe")) { a = await voeExtractor(url, "VoeSX $quality"); - } else if (containsVidBom(url)) { + } else if (containsVidBom(url) && hosterSelection.contains("VidBom")) { a = await vidBomExtractor(url); } videos.addAll(a); } + return sortVideos(videos, source.id); + } + + @override + List getSourcePreferences(MSource source) { + return [ + ListPreference( + key: "preferred_quality", + title: "Preferred Quality", + summary: "", + valueIndex: 1, + entries: ["1080p", "720p", "480p", "360p"], + entryValues: ["1080", "720", "480", "360"]), + MultiSelectListPreference( + key: "hoster_selection", + title: "Enable/Disable Hosts", + summary: "", + entries: ["Dood", "Voe", "Mp4upload", "VidBom", "Okru"], + entryValues: ["Dood", "Voe", "Mp4upload", "VidBom", "Okru"], + values: ["Dood", "Voe", "Mp4upload", "VidBom", "Okru"]), + ]; + } + + 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; } + List preferenceHosterSelection(int sourceId) { + return getPreferenceValue(sourceId, "hoster_selection"); + } + String getQuality(String quality) { quality = quality.replaceAll(" ", ""); if (quality == "HD") { diff --git a/anime/src/en/aniwave/aniwave-v0.0.2.dart b/anime/src/en/aniwave/aniwave-v0.0.2.dart index beb8d443..2ec4582a 100644 --- a/anime/src/en/aniwave/aniwave-v0.0.2.dart +++ b/anime/src/en/aniwave/aniwave-v0.0.2.dart @@ -312,7 +312,7 @@ class Aniwave extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ SelectFilter("OrderFilter", "Sort order", 0, [ SelectFilterOption("Most relevance", "most_relevance"), diff --git a/anime/src/en/gogoanime/gogoanime-v0.0.5.dart b/anime/src/en/gogoanime/gogoanime-v0.0.5.dart index 2d63ecf0..4ae2c1b2 100644 --- a/anime/src/en/gogoanime/gogoanime-v0.0.5.dart +++ b/anime/src/en/gogoanime/gogoanime-v0.0.5.dart @@ -251,7 +251,7 @@ class GogoAnime extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ HeaderFilter("Advanced search"), GroupFilter("GenreFilter", "Genre", [ diff --git a/anime/src/fr/franime/franime-v0.0.4.dart b/anime/src/fr/franime/franime-v0.0.45.dart similarity index 94% rename from anime/src/fr/franime/franime-v0.0.4.dart rename to anime/src/fr/franime/franime-v0.0.45.dart index 9aedeb81..f411dc06 100644 --- a/anime/src/fr/franime/franime-v0.0.4.dart +++ b/anime/src/fr/franime/franime-v0.0.45.dart @@ -136,6 +136,7 @@ class FrAnime extends MProvider { } else if (language == "vf" && hasVf) { players = vfPlayers; } + print(players); List videos = []; for (var i = 0; i < players.length; i++) { String apiUrl = "$videoBaseUrl/$language/$i"; @@ -155,8 +156,6 @@ class FrAnime extends MProvider { ..url = playerUrl ..originalUrl = playerUrl ..quality = "FRAnime (Vido)"); - } else if (playerName.contains("myvi")) { - a = await myTvExtractor(playerUrl); } else if (playerName.contains("sendvid")) { a = await sendVidExtractor( playerUrl, json.encode({"Referer": "https://franime.fr/"}), ""); @@ -191,7 +190,7 @@ class FrAnime extends MProvider { } } - final titleO = animeJson["titleO"]; + String titleO = animeJson["titleO"]; final title = animeJson["title"]; final genre = animeJson["themes"]; final description = animeJson["description"]; @@ -228,7 +227,7 @@ class FrAnime extends MProvider { anime.name = seasonTitle; anime.imageUrl = imageUrl; anime.link = - "/anime/${regExp(titleO, "[^A-Za-z0-9 ]", "", 0, 0).replaceAll(" ", "-").toLowerCase()}?lang=$lang&s=$ind"; + "/anime/${titleO.replaceAll(RegExp("[^A-Za-z0-9 ]"), "").replaceAll(" ", "-").toLowerCase()}?lang=$lang&s=$ind"; animeList.add(anime); } @@ -276,7 +275,7 @@ class FrAnime extends MProvider { vfListName.add(vf["lecteurs"].isNotEmpty); } } - final titleO = animeJson["titleO"]; + String titleO = animeJson["titleO"]; final title = animeJson["title"]; final genre = animeJson["themes"]; final description = animeJson["description"]; @@ -314,7 +313,7 @@ class FrAnime extends MProvider { anime.name = seasonTitle; anime.imageUrl = imageUrl; anime.link = - "/anime/${regExp(titleO, "[^A-Za-z0-9 ]", "", 0, 0).replaceAll(" ", "-").toLowerCase()}?lang=$lang&s=$ind"; + "/anime/${titleO.replaceAll(RegExp("[^A-Za-z0-9 ]"), "").replaceAll(" ", "-").toLowerCase()}?lang=$lang&s=$ind"; animeList.add(anime); } @@ -335,12 +334,11 @@ class FrAnime extends MProvider { String databaseAnimeByTitleO(String res, String titleO) { print(titleO); - final datas = json.decode(res) as List; + final datas = json.decode(res) as List>; for (var data in datas) { - if (regExp(data["titleO"], "[^A-Za-z0-9 ]", "", 0, 0) - .replaceAll(" ", "-") - .toLowerCase() == - "${titleO}") { + String title = + (data["titleO"] as String).replaceAll(RegExp("[^A-Za-z0-9 ]"), ""); + if (title.replaceAll(" ", "-").toLowerCase() == "${titleO}") { return json.encode(data); } } diff --git a/anime/src/fr/franime/source.dart b/anime/src/fr/franime/source.dart index 0643414b..1073e464 100644 --- a/anime/src/fr/franime/source.dart +++ b/anime/src/fr/franime/source.dart @@ -2,7 +2,7 @@ import '../../../../model/source.dart'; import '../../../../utils/utils.dart'; Source get franimeSource => _franimeSource; -const franimeVersion = "0.0.4"; +const franimeVersion = "0.0.45"; const franimeSourceCodeUrl = "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/anime/src/fr/franime/franime-v$franimeVersion.dart"; Source _franimeSource = Source( diff --git a/anime/src/fr/otakufr/otakufr-v0.0.4.dart b/anime/src/fr/otakufr/otakufr-v0.0.4.dart deleted file mode 100644 index 5e0d0cc3..00000000 --- a/anime/src/fr/otakufr/otakufr-v0.0.4.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:mangayomi/bridge_lib.dart'; -import 'dart:convert'; - -class OtakuFr extends MProvider { - OtakuFr(); - - @override - Future getPopular(MSource source, int page) async { - final data = { - "url": "${source.baseUrl}/toute-la-liste-affiches/page/$page/?q=." - }; - final res = await http('GET', json.encode(data)); - - List animeList = []; - final urls = - xpath(res, '//*[@class="list"]/article/div/div/figure/a/@href'); - final names = - xpath(res, '//*[@class="list"]/article/div/div/figure/a/img/@title'); - final images = - xpath(res, '//*[@class="list"]/article/div/div/figure/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); - } - final nextPage = xpath(res, '//a[@class="next page-link"]/@href'); - return MPages(animeList, nextPage.isNotEmpty); - } - - @override - Future getLatestUpdates(MSource source, int page) async { - final data = {"url": "${source.baseUrl}/page/$page/"}; - final res = await http('GET', json.encode(data)); - - List animeList = []; - final urls = xpath(res, '//*[@class="episode"]/div/a/@href'); - final namess = xpath(res, '//*[@class="episode"]/div/a/text()'); - List names = []; - for (var name in namess) { - names.add(regExp( - name, - r'(?<=\bS\d\s*|)\d{2}\s*(?=\b(Vostfr|vostfr|VF|Vf|vf|\(VF\)|\(vf\)|\(Vf\)|\(Vostfr\)\b))?', - '', - 0, - 0) - .replaceAll(' vostfr', '') - .replaceAll(' Vostfr', '') - .replaceAll(' VF', '') - .replaceAll(' Vf', '') - .replaceAll(' vf', '') - .replaceAll(' (VF)', '') - .replaceAll(' (vf)', '') - .replaceAll(' (vf)', '') - .replaceAll(' (Vf)', '') - .replaceAll(' (Vostfr)', '')); - } - final images = xpath(res, '//*[@class="episode"]/div/figure/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); - } - final nextPage = xpath(res, '//a[@class="next page-link"]/@href'); - return MPages(animeList, nextPage.isNotEmpty); - } - - @override - Future search( - MSource source, String query, int page, FilterList filterList) async { - final data = { - "url": "${source.baseUrl}/toute-la-liste-affiches/page/$page/?q=$query" - }; - final res = await http('GET', json.encode(data)); - - List animeList = []; - final urls = - xpath(res, '//*[@class="list"]/article/div/div/figure/a/@href'); - final names = - xpath(res, '//*[@class="list"]/article/div/div/figure/a/img/@title'); - final images = - xpath(res, '//*[@class="list"]/article/div/div/figure/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); - } - final nextPage = xpath(res, '//a[@class="next page-link"]/@href'); - return MPages(animeList, nextPage.isNotEmpty); - } - - @override - Future getDetail(MSource source, String url) async { - final statusList = [ - { - "En cours": 0, - "Terminé": 1, - } - ]; - final data = {"url": url}; - String res = await http('GET', json.encode(data)); - MManga anime = MManga(); - final originalUrl = xpath(res, - '//*[@class="breadcrumb"]/li[@class="breadcrumb-item"][2]/a/@href') - .first; - if (originalUrl.isNotEmpty) { - final newData = {"url": originalUrl}; - res = await http('GET', json.encode(newData)); - } - - anime.description = xpath(res, '//*[@class="episode fz-sm synop"]/p/text()') - .first - .replaceAll("Synopsis:", ""); - final status = xpath(res, - '//*[@class="list-unstyled"]/li[contains(text(),"Statut")]/text()') - .first - .replaceAll("Statut: ", ""); - anime.status = parseStatus(status, statusList); - anime.genre = xpath(res, - '//*[@class="list-unstyled"]/li[contains(text(),"Genre")]/ul/li/a/text()'); - - final epUrls = xpath(res, '//*[@class="list-episodes list-group"]/a/@href'); - final dates = - xpath(res, '//*[@class="list-episodes list-group"]/a/span/text()'); - final names = xpath(res, '//*[@class="list-episodes list-group"]/a/text()'); - List episodes = []; - - for (var i = 0; i < names.length; i++) { - final date = dates[i]; - final name = names[i]; - episodes.add( - "Episode ${regExp(name.replaceAll(date, ""), r".* (\d*) [VvfF]{1,1}", '', 1, 1)}"); - } - final dateUploads = parseDates(dates, "dd MMMM yyyy", "fr"); - - List? episodesList = []; - for (var i = 0; i < episodes.length; i++) { - MChapter episode = MChapter(); - episode.name = episodes[i]; - episode.url = epUrls[i]; - episode.dateUpload = dateUploads[i]; - episodesList.add(episode); - } - - anime.chapters = episodesList; - return anime; - } - - @override - Future> getVideoList(MSource source, String url) async { - final res = await http('GET', json.encode({"url": url})); - - final servers = xpath(res, '//*[@id="nav-tabContent"]/div/iframe/@src'); - List videos = []; - for (var url in servers) { - final datasServer = { - "url": fixUrl(url), - "headers": {"X-Requested-With": "XMLHttpRequest"} - }; - - final resServer = await http('GET', json.encode(datasServer)); - final serverUrl = - fixUrl(regExp(resServer, r"data-url='([^']+)'", '', 1, 1)); - List a = []; - if (serverUrl.contains("https://streamwish")) { - a = await streamWishExtractor(serverUrl, "StreamWish"); - } else if (serverUrl.contains("sibnet")) { - a = await sibnetExtractor(serverUrl); - } else if (serverUrl.contains("https://doo")) { - a = await doodExtractor(serverUrl); - } else if (serverUrl.contains("https://voe.sx")) { - a = await voeExtractor(serverUrl, null); - } else if (serverUrl.contains("https://ok.ru")) { - a = await okruExtractor(serverUrl); - } - videos.addAll(a); - } - - return videos; - } - - String fixUrl(String url) { - return regExp(url, r"^(?:(?:https?:)?//|www\.)", 'https://', 0, 0); - } -} - -OtakuFr main() { - return OtakuFr(); -} diff --git a/anime/src/fr/otakufr/otakufr-v0.0.45.dart b/anime/src/fr/otakufr/otakufr-v0.0.45.dart new file mode 100644 index 00000000..29b40b13 --- /dev/null +++ b/anime/src/fr/otakufr/otakufr-v0.0.45.dart @@ -0,0 +1,448 @@ +import 'package:mangayomi/bridge_lib.dart'; +import 'dart:convert'; + +class OtakuFr extends MProvider { + OtakuFr(); + + @override + Future getPopular(MSource source, int page) async { + final data = { + "url": "${source.baseUrl}/toute-la-liste-affiches/page/$page/?q=." + }; + final res = await http('GET', json.encode(data)); + + List animeList = []; + final urls = + xpath(res, '//*[@class="list"]/article/div/div/figure/a/@href'); + final names = + xpath(res, '//*[@class="list"]/article/div/div/figure/a/img/@title'); + final images = + xpath(res, '//*[@class="list"]/article/div/div/figure/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); + } + final nextPage = xpath(res, '//a[@class="next page-link"]/@href'); + return MPages(animeList, nextPage.isNotEmpty); + } + + @override + Future getLatestUpdates(MSource source, int page) async { + final data = {"url": "${source.baseUrl}/page/$page/"}; + final res = await http('GET', json.encode(data)); + + List animeList = []; + final urls = xpath(res, '//*[@class="episode"]/div/a/@href'); + final namess = xpath(res, '//*[@class="episode"]/div/a/text()'); + List names = []; + for (var name in namess) { + names.add(regExp( + name, + r'(?<=\bS\d\s*|)\d{2}\s*(?=\b(Vostfr|vostfr|VF|Vf|vf|\(VF\)|\(vf\)|\(Vf\)|\(Vostfr\)\b))?', + '', + 0, + 0) + .replaceAll(' vostfr', '') + .replaceAll(' Vostfr', '') + .replaceAll(' VF', '') + .replaceAll(' Vf', '') + .replaceAll(' vf', '') + .replaceAll(' (VF)', '') + .replaceAll(' (vf)', '') + .replaceAll(' (vf)', '') + .replaceAll(' (Vf)', '') + .replaceAll(' (Vostfr)', '')); + } + final images = xpath(res, '//*[@class="episode"]/div/figure/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); + } + final nextPage = xpath(res, '//a[@class="next page-link"]/@href'); + return MPages(animeList, nextPage.isNotEmpty); + } + + @override + Future search( + MSource source, String query, int page, FilterList filterList) async { + final filters = filterList.filters; + String url = ""; + if (query.isNotEmpty) { + url = "${source.baseUrl}/toute-la-liste-affiches/page/$page/?q=$query"; + } else { + for (var filter in filters) { + if (filter.type == "GenreFilter") { + if (filter.state != 0) { + url = + "${source.baseUrl}/${filter.values[filter.state].value}page/$page"; + } + } else if (filter.type == "SubPageFilter") { + if (url.isEmpty) { + if (filter.state != 0) { + url = + "${source.baseUrl}/${filter.values[filter.state].value}page/$page"; + } + } + } + } + } + final data = {"url": url}; + final res = await http('GET', json.encode(data)); + + List animeList = []; + final urls = + xpath(res, '//*[@class="list"]/article/div/div/figure/a/@href'); + final names = + xpath(res, '//*[@class="list"]/article/div/div/figure/a/img/@title'); + final images = + xpath(res, '//*[@class="list"]/article/div/div/figure/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); + } + final nextPage = xpath(res, '//a[@class="next page-link"]/@href'); + return MPages(animeList, nextPage.isNotEmpty); + } + + @override + Future getDetail(MSource source, String url) async { + final statusList = [ + {"En cours": 0, "Terminé": 1} + ]; + final data = {"url": url}; + String res = await http('GET', json.encode(data)); + MManga anime = MManga(); + final originalUrl = xpath(res, + '//*[@class="breadcrumb"]/li[@class="breadcrumb-item"][2]/a/@href'); + if (originalUrl.isNotEmpty) { + final newData = {"url": originalUrl.first}; + res = await http('GET', json.encode(newData)); + } + final description = + xpath(res, '//*[@class="episode fz-sm synop"]/p/text()'); + if (description.isNotEmpty) { + anime.description = description.first.replaceAll("Synopsis:", ""); + } + final status = xpath(res, + '//*[@class="list-unstyled"]/li[contains(text(),"Statut")]/text()'); + if (status.isNotEmpty) { + anime.status = + parseStatus(status.first.replaceAll("Statut: ", ""), statusList); + } + + anime.genre = xpath(res, + '//*[@class="list-unstyled"]/li[contains(text(),"Genre")]/ul/li/a/text()'); + + final epUrls = xpath(res, '//*[@class="list-episodes list-group"]/a/@href'); + final dates = + xpath(res, '//*[@class="list-episodes list-group"]/a/span/text()'); + final names = xpath(res, '//*[@class="list-episodes list-group"]/a/text()'); + List episodes = []; + + for (var i = 0; i < names.length; i++) { + final date = dates[i]; + final name = names[i]; + episodes.add( + "Episode ${regExp(name.replaceAll(date, ""), r".* (\d*) [VvfF]{1,1}", '', 1, 1)}"); + } + final dateUploads = parseDates(dates, "dd MMMM yyyy", "fr"); + + List? episodesList = []; + for (var i = 0; i < episodes.length; i++) { + MChapter episode = MChapter(); + episode.name = episodes[i]; + episode.url = epUrls[i]; + episode.dateUpload = dateUploads[i]; + episodesList.add(episode); + } + + anime.chapters = episodesList; + return anime; + } + + @override + Future> getVideoList(MSource source, String url) async { + final res = await http('GET', json.encode({"url": url})); + + final servers = xpath(res, '//*[@id="nav-tabContent"]/div/iframe/@src'); + List videos = []; + final hosterSelection = preferenceHosterSelection(source.id); + for (var url in servers) { + final datasServer = { + "url": fixUrl(url), + "headers": {"X-Requested-With": "XMLHttpRequest"} + }; + + final resServer = await http('GET', json.encode(datasServer)); + final serverUrl = + fixUrl(regExp(resServer, r"data-url='([^']+)'", '', 1, 1)); + List a = []; + if (serverUrl.contains("https://streamwish") && + hosterSelection.contains("Streamwish")) { + a = await streamWishExtractor(serverUrl, "StreamWish"); + } else if (serverUrl.contains("sibnet") && + hosterSelection.contains("Sibnet")) { + a = await sibnetExtractor(serverUrl); + } else if (serverUrl.contains("https://doo") && + hosterSelection.contains("Doodstream")) { + a = await doodExtractor(serverUrl); + } else if (serverUrl.contains("https://voe.sx") && + hosterSelection.contains("Voe")) { + a = await voeExtractor(serverUrl, null); + } else if (serverUrl.contains("https://ok.ru") && + hosterSelection.contains("Okru")) { + a = await okruExtractor(serverUrl); + } else if (serverUrl.contains("vadbam") && + hosterSelection.contains("Vidbm")) { + a = await vidbmExtractor(serverUrl); + } else if (serverUrl.contains("upstream") && + hosterSelection.contains("Upstream")) { + a = await upstreamExtractor(serverUrl); + } else if (serverUrl.contains("sendvid") && + hosterSelection.contains("Sendvid")) { + a = await sendVidExtractor(serverUrl, null, ""); + } + videos.addAll(a); + } + + return videos; + } + + String fixUrl(String url) { + return regExp(url, r"^(?:(?:https?:)?//|www\.)", 'https://', 0, 0); + } + + @override + List getFilterList(MSource source) { + return [ + HeaderFilter("La recherche de texte ignore les filtres"), + SelectFilter("GenreFilter", "Genre", 0, [ + SelectFilterOption("", ""), + SelectFilterOption("Action", "/genre/action/"), + SelectFilterOption("Aventure", "/genre/aventure/"), + SelectFilterOption("Comedie", "/genre/comedie/"), + SelectFilterOption("Crime", "/genre/crime/"), + SelectFilterOption("Démons", "/genre/demons/"), + SelectFilterOption("Drame", "/genre/drame/"), + SelectFilterOption("Ecchi", "/genre/ecchi/"), + SelectFilterOption("Espace", "/genre/espace/"), + SelectFilterOption("Fantastique", "/genre/fantastique/"), + SelectFilterOption("Gore", "/genre/gore/"), + SelectFilterOption("Harem", "/genre/harem/"), + SelectFilterOption("Historique", "/genre/historique/"), + SelectFilterOption("Horreur", "/genre/horreur/"), + SelectFilterOption("Isekai", "/genre/isekai/"), + SelectFilterOption("Jeux", "/genre/jeu/"), + SelectFilterOption("L'école", "/genre/lecole/"), + SelectFilterOption("Magical girls", "/genre/magical-girls/"), + SelectFilterOption("Magie", "/genre/magie/"), + SelectFilterOption("Martial Arts", "/genre/martial-arts/"), + SelectFilterOption("Mecha", "/genre/mecha/"), + SelectFilterOption("Militaire", "/genre/militaire/"), + SelectFilterOption("Musique", "/genre/musique/"), + SelectFilterOption("Mysterieux", "/genre/mysterieux/"), + SelectFilterOption("Parodie", "/genre/Parodie/"), + SelectFilterOption("Police", "/genre/police/"), + SelectFilterOption("Psychologique", "/genre/psychologique/"), + SelectFilterOption("Romance", "/genre/romance/"), + SelectFilterOption("Samurai", "/genre/samurai/"), + SelectFilterOption("Sci-Fi", "/genre/sci-fi/"), + SelectFilterOption("Seinen", "/genre/seinen/"), + SelectFilterOption("Shoujo", "/genre/shoujo/"), + SelectFilterOption("Shoujo Ai", "/genre/shoujo-ai/"), + SelectFilterOption("Shounen", "/genre/shounen/"), + SelectFilterOption("Shounen Ai", "/genre/shounen-ai/"), + SelectFilterOption("Sport", "/genre/sport/"), + SelectFilterOption("Super Power", "/genre/super-power/"), + SelectFilterOption("Surnaturel", "/genre/surnaturel/"), + SelectFilterOption("Suspense", "/genre/suspense/"), + SelectFilterOption("Thriller", "/genre/thriller/"), + SelectFilterOption("Tranche de vie", "/genre/tranche-de-vie/"), + SelectFilterOption("Vampire", "/genre/vampire/") + ]), + SelectFilter("SubPageFilter", "Sous page", 0, [ + SelectFilterOption("", ""), + SelectFilterOption("Terminé", "/termine/"), + SelectFilterOption("Film", "/film/"), + ]) + ]; + } + + @override + List getSourcePreferences(MSource source) { + return [ + ListPreference( + key: "preferred_quality", + title: "Preferred Quality", + summary: "", + valueIndex: 1, + entries: ["1080p", "720p", "480p", "360p"], + entryValues: ["1080", "720", "480", "360"]), + MultiSelectListPreference( + key: "hoster_selection", + title: "Enable/Disable Hosts", + summary: "", + entries: [ + "Streamwish", + "Doodstream", + "Sendvid", + "Vidbm", + "Okru", + "Voe", + "Sibnet", + "Upstream" + ], + entryValues: [ + "Streamwish", + "Doodstream", + "Sendvid", + "Vidbm", + "Okru", + "Voe", + "Sibnet", + "Upstream" + ], + values: [ + "Streamwish", + "Doodstream", + "Sendvid", + "Vidbm", + "Okru", + "Voe", + "Sibnet", + "Upstream" + ]), + ]; + } + + 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; + } + + List preferenceHosterSelection(int sourceId) { + return getPreferenceValue(sourceId, "hoster_selection"); + } + + Future> upstreamExtractor(String url) async { + final res = await http('GET', json.encode({"url": url})); + final js = xpath(res, '//script[contains(text(), "m3u8")]/text()'); + if (js.isEmpty) { + return []; + } + final masterUrl = + substringBefore(substringAfter(evalJs(js.first), "{file:\""), "\"}"); + final masterPlaylistRes = + await http('GET', json.encode({"url": masterUrl})); + List videos = []; + 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 = "Upstream - $quality"; + videos.add(video); + } + return videos; + } + + Future> vidbmExtractor(String url) async { + final res = await http('GET', json.encode({"url": url})); + final js = xpath(res, + '//script[contains(text(), "m3u8") or contains(text(), "mp4")]/text()'); + if (js.isEmpty) { + return []; + } + final masterUrl = substringBefore(substringAfter(js.first, "source"), "\""); + final quality = substringBefore( + substringAfter( + substringBefore( + substringAfter(substringAfter(js.first, "source"), "file"), + "]"), + "label:\""), + "\""); + List videos = []; + if (masterUrl.contains("m3u8")) { + 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 = "Vidbm - $quality"; + videos.add(video); + } + return videos; + } else { + MVideo video = MVideo(); + video + ..url = masterUrl + ..originalUrl = masterUrl + ..quality = "Vidbm - $quality"; + videos.add(video); + } + return videos; + } +} + +OtakuFr main() { + return OtakuFr(); +} diff --git a/anime/src/fr/otakufr/source.dart b/anime/src/fr/otakufr/source.dart index c9495929..e54341c5 100644 --- a/anime/src/fr/otakufr/source.dart +++ b/anime/src/fr/otakufr/source.dart @@ -2,7 +2,7 @@ import '../../../../model/source.dart'; import '../../../../utils/utils.dart'; Source get otakufr => _otakufr; -const otakufrVersion = "0.0.4"; +const otakufrVersion = "0.0.45"; const otakufrCodeUrl = "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/anime/src/fr/otakufr/otakufr-v$otakufrVersion.dart"; Source _otakufr = Source( diff --git a/anime/src/id/oploverz/oploverz-v0.0.15.dart b/anime/src/id/oploverz/oploverz-v0.0.2.dart similarity index 95% rename from anime/src/id/oploverz/oploverz-v0.0.15.dart rename to anime/src/id/oploverz/oploverz-v0.0.2.dart index d2e00156..6ffe04d1 100644 --- a/anime/src/id/oploverz/oploverz-v0.0.15.dart +++ b/anime/src/id/oploverz/oploverz-v0.0.2.dart @@ -98,12 +98,11 @@ class OploVerz extends MProvider { xpath(ress, '//iframe[@class="playeriframe"]/@src').first; final resPlayer = await http('GET', json.encode({"url": playerLink})); var resJson = substringBefore(substringAfter(resPlayer, "= "), "<"); - var streams = - json.decode(getMapValue(resJson, "streams", encode: true)) as List; + var streams = json.decode(resJson)["streams"] as List>; List videos = []; for (var stream in streams) { - final videoUrl = getMapValue(stream, "play_url"); - final quality = getQuality(getMapValue(stream, "format_id")); + String videoUrl = stream["play_url"]; + final quality = getQuality(stream["format_id"]); MVideo video = MVideo(); video diff --git a/anime/src/id/oploverz/source.dart b/anime/src/id/oploverz/source.dart index 290be7f0..be58a1ea 100644 --- a/anime/src/id/oploverz/source.dart +++ b/anime/src/id/oploverz/source.dart @@ -2,7 +2,7 @@ import '../../../../model/source.dart'; import '../../../../utils/utils.dart'; Source get oploverz => _oploverz; -const oploverzVersion = "0.0.15"; +const oploverzVersion = "0.0.2"; const oploverzCodeUrl = "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/anime/src/id/oploverz/oploverz-v$oploverzVersion.dart"; Source _oploverz = Source( diff --git a/manga/multisrc/madara/madara-v0.0.4.dart b/manga/multisrc/madara/madara-v0.0.4.dart index 7393036b..c6c0c3e7 100644 --- a/manga/multisrc/madara/madara-v0.0.4.dart +++ b/manga/multisrc/madara/madara-v0.0.4.dart @@ -356,7 +356,7 @@ class Madara extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ TextFilter("AuthorFilter", "Author"), TextFilter("ArtistFilter", "Artist"), diff --git a/manga/multisrc/mangareader/mangareader-v0.0.5.dart b/manga/multisrc/mangareader/mangareader-v0.0.5.dart index 20d8bc0f..e1b392b6 100644 --- a/manga/multisrc/mangareader/mangareader-v0.0.5.dart +++ b/manga/multisrc/mangareader/mangareader-v0.0.5.dart @@ -236,7 +236,7 @@ class MangaReader extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ SeparatorFilter(), TextFilter("AuthorFilter", "Author"), diff --git a/manga/multisrc/mmrcms/mmrcms-v0.0.4.dart b/manga/multisrc/mmrcms/mmrcms-v0.0.4.dart index d0f522cf..d46a8f9c 100644 --- a/manga/multisrc/mmrcms/mmrcms-v0.0.4.dart +++ b/manga/multisrc/mmrcms/mmrcms-v0.0.4.dart @@ -232,7 +232,7 @@ class MMRCMS extends MProvider { return pagesUrl; } - List getFilterList() { + List getFilterList(MSource source) { return [ HeaderFilter("NOTE: Ignored if using text search!"), SeparatorFilter(), diff --git a/manga/multisrc/nepnep/nepnep-v0.0.4.dart b/manga/multisrc/nepnep/nepnep-v0.0.4.dart index 86f4a6cc..b7b014f9 100644 --- a/manga/multisrc/nepnep/nepnep-v0.0.4.dart +++ b/manga/multisrc/nepnep/nepnep-v0.0.4.dart @@ -10,9 +10,15 @@ class NepNep extends MProvider { final res = await http('GET', json.encode(data)); final directory = directoryFromDocument(res); - final resSort = sortMapList(json.decode(directory), "vm", 1); + final resSort = (json.decode(directory) as List>); + resSort.sort( + (Map a, Map b) => + (a["vm"] as String).compareTo(b["vm"] as String), + ); - return parseDirectory(resSort); + // sortMapList(json.decode(directory), "vm", 1); + + return parseDirectory(json.encode(resSort)); } @override @@ -308,7 +314,7 @@ class NepNep extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ TextFilter("YearFilter", "Years"), TextFilter("AuthorFilter", "Author"), diff --git a/manga/src/all/batoto/batoto-v0.0.4.dart b/manga/src/all/batoto/batoto-v0.0.45.dart similarity index 97% rename from manga/src/all/batoto/batoto-v0.0.4.dart rename to manga/src/all/batoto/batoto-v0.0.45.dart index 060f8176..4be7bdee 100644 --- a/manga/src/all/batoto/batoto-v0.0.4.dart +++ b/manga/src/all/batoto/batoto-v0.0.45.dart @@ -7,7 +7,7 @@ class Batoto extends MProvider { @override Future getPopular(MSource source, int page) async { final url = - "${source.baseUrl}/browse?${lang(source.lang)}&sort=views_a&page=$page"; + "${preferenceMirror(source.id)}/browse?${lang(source.lang)}&sort=views_a&page=$page"; final data = {"url": url}; final res = await http('GET', json.encode(data)); return mangaElementM(res, source); @@ -16,7 +16,7 @@ class Batoto extends MProvider { @override Future getLatestUpdates(MSource source, int page) async { final url = - "${source.baseUrl}/browse?${lang(source.lang)}&sort=update&page=$page"; + "${preferenceMirror(source.id)}/browse?${lang(source.lang)}&sort=update&page=$page"; final data = {"url": url}; final res = await http('GET', json.encode(data)); return mangaElementM(res, source); @@ -30,7 +30,7 @@ class Batoto extends MProvider { String min = ""; String max = ""; if (query.isNotEmpty) { - url = "${source.baseUrl}/search?word=$query&page=$page"; + url = "${preferenceMirror(source.id)}/search?word=$query&page=$page"; for (var filter in filters) { if (filter.type == "LetterFilter") { if (filter.state == 1) { @@ -39,7 +39,7 @@ class Batoto extends MProvider { } } } else { - url = "${source.baseUrl}/browse"; + url = "${preferenceMirror(source.id)}/browse"; for (var filter in filters) { if (filter.type == "LangGroupFilter") { final langs = (filter.state as List).where((e) => e.state).toList(); @@ -111,7 +111,7 @@ class Batoto extends MProvider { {"Ongoing": 0, "Completed": 1, "Cancelled": 3, "Hiatus": 2} ]; - final data = {"url": "${source.baseUrl}$url"}; + final data = {"url": "${preferenceMirror(source.id)}$url"}; final res = await http('GET', json.encode(data)); MManga manga = MManga(); final workStatus = xpath(res, @@ -171,7 +171,7 @@ class Batoto extends MProvider { @override Future> getPageList(MSource source, String url) async { - final datas = {"url": "${source.baseUrl}$url"}; + final datas = {"url": "${preferenceMirror(source.id)}$url"}; final res = await http('GET', json.encode(datas)); final script = xpath(res, @@ -273,7 +273,7 @@ class Batoto extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ SelectFilter("LetterFilter", "Letter matching mode (Slow)", 0, [ SelectFilterOption("Disabled", "disabled"), @@ -1663,36 +1663,51 @@ class Batoto extends MProvider { SeparatorFilter(), ]; } -} -Map getMirrorPref() { - return { - "bato.to": "https://bato.to", - "batocomic.com": "https://batocomic.com", - "batocomic.net": "https://batocomic.net", - "batocomic.org": "https://batocomic.org", - "batotoo.com": "https://batotoo.com", - "batotwo.com": "https://batotwo.com", - "battwo.com": "https://battwo.com", - "comiko.net": "https://comiko.net", - "comiko.org": "https://comiko.org", - "mangatoto.com": "https://mangatoto.com", - "mangatoto.net": "https://mangatoto.net", - "mangatoto.org": "https://mangatoto.org", - "readtoto.com": "https://readtoto.com", - "readtoto.net": "https://readtoto.net", - "readtoto.org": "https://readtoto.org", - "dto.to": "https://dto.to", - "hto.to": "https://hto.to", - "mto.to": "https://mto.to", - "wto.to": "https://wto.to", - "xbato.com": "https://xbato.com", - "xbato.net": "https://xbato.net", - "xbato.org": "https://xbato.org", - "zbato.com": "https://zbato.com", - "zbato.net": "https://zbato.net", - "zbato.org": "https://zbato.org", - }; + @override + List getSourcePreferences(MSource source) { + return [ + ListPreference( + key: "mirror", + title: "Mirror", + summary: "", + valueIndex: 0, + entries: mirrorEntries, + entryValues: mirrorEntries.map((e) => "https://$e").toList()), + ]; + } + + List mirrorEntries = [ + "bato.to", + "batocomic.com", + "batocomic.net", + "batocomic.org", + "batotoo.com", + "batotwo.com", + "battwo.com", + "comiko.net", + "comiko.org", + "mangatoto.com", + "mangatoto.net", + "mangatoto.org", + "readtoto.com", + "readtoto.net", + "readtoto.org", + "dto.to", + "hto.to", + "mto.to", + "wto.to", + "xbato.com", + "xbato.net", + "xbato.org", + "zbato.com", + "zbato.net", + "zbato.org", + ]; + + String preferenceMirror(int sourceId) { + return getPreferenceValue(sourceId, "mirror"); + } } Batoto main() { diff --git a/manga/src/all/batoto/sources.dart b/manga/src/all/batoto/sources.dart index e394fbf0..70f5efd8 100644 --- a/manga/src/all/batoto/sources.dart +++ b/manga/src/all/batoto/sources.dart @@ -1,7 +1,7 @@ import '../../../../model/source.dart'; import '../../../../utils/utils.dart'; -const batotoVersion = "0.0.4"; +const batotoVersion = "0.0.45"; const batotoSourceCodeUrl = "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/manga/src/all/batoto/batoto-v$batotoVersion.dart"; diff --git a/manga/src/all/comick/comick-v0.0.4.dart b/manga/src/all/comick/comick-v0.0.4.dart index 3afbaebc..69822d77 100644 --- a/manga/src/all/comick/comick-v0.0.4.dart +++ b/manga/src/all/comick/comick-v0.0.4.dart @@ -215,7 +215,7 @@ class ComickFun extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ HeaderFilter("The filter is ignored when using text search."), GroupFilter("GenreFilter", "Genre", [ diff --git a/manga/src/all/mangadex/mangadex-v0.0.45.dart b/manga/src/all/mangadex/mangadex-v0.0.5.dart similarity index 85% rename from manga/src/all/mangadex/mangadex-v0.0.45.dart rename to manga/src/all/mangadex/mangadex-v0.0.5.dart index 3660487d..77d681e6 100644 --- a/manga/src/all/mangadex/mangadex-v0.0.45.dart +++ b/manga/src/all/mangadex/mangadex-v0.0.5.dart @@ -8,7 +8,7 @@ class MangaDex extends MProvider { Future getPopular(MSource source, int page) async { page = (20 * (page - 1)); final url = - "https://api.mangadex.org/manga?limit=20&offset=$page&availableTranslatedLanguage[]=${source.lang}&includes[]=cover_art${getMDXContentRating()}&order[followedCount]=desc"; + "https://api.mangadex.org/manga?limit=20&offset=$page&availableTranslatedLanguage[]=${source.lang}&includes[]=cover_art${preferenceContentRating(source.id)}${preferenceOriginalLanguages(source.id)}&order[followedCount]=desc"; final datas = {"url": url}; final res = await http('GET', json.encode(datas)); return mangaRes(res, source); @@ -24,12 +24,12 @@ class MangaDex extends MProvider { final mangaIds = jsonPathToString(ress, r'$.data[*].relationships[*].id', '.--') .split('.--'); - String mangaIdss = "".toString(); + String mangaIdss = ""; for (var id in mangaIds) { mangaIdss += "&ids[]=$id"; } final newUrl = - "https://api.mangadex.org/manga?includes[]=cover_art&limit=${mangaIds.length}${getMDXContentRating()}$mangaIdss"; + "https://api.mangadex.org/manga?includes[]=cover_art&limit=${mangaIds.length}${preferenceContentRating(source.id)}${preferenceOriginalLanguages(source.id)}$mangaIdss"; final res = await http('GET', json.encode({"url": newUrl})); return mangaRes(res, source); } @@ -188,7 +188,7 @@ class MangaDex extends MProvider { final mangaId = url.split('/').last; final paginatedChapterList = - await paginatedChapterListRequest(mangaId, 0, source.lang); + await paginatedChapterListRequest(mangaId, 0, source.lang, source.id); final chapterList = jsonPathToString(paginatedChapterList, r'$.data[*]', '_.').split('_.'); int limit = @@ -206,8 +206,8 @@ class MangaDex extends MProvider { var hasMoreResults = (limit + offset) < total; while (hasMoreResults) { offset += limit; - var newRequest = - await paginatedChapterListRequest(mangaId, offset, source.lang); + var newRequest = await paginatedChapterListRequest( + mangaId, offset, source.lang, source.id); int total = int.parse(jsonPathToString(newRequest, r'$.total', '')); final chapterList = jsonPathToString(paginatedChapterList, r'$.data[*]', '_.') @@ -244,7 +244,7 @@ class MangaDex extends MProvider { for (var e in resJson) { MManga manga = MManga(); manga.name = findTitle(json.encode(e), source.lang); - manga.imageUrl = getCover(json.encode(e)); + manga.imageUrl = getCover(json.encode(e), source.id); manga.link = "/manga/${getMapValue(json.encode(e), "id")}"; mangaList.add(manga); } @@ -253,28 +253,28 @@ class MangaDex extends MProvider { List getChapters(int length, String paginatedChapterListA) { List chaptersList = []; - String paginatedChapterList = paginatedChapterListA.toString(); + String paginatedChapterList = paginatedChapterListA; final dataList = jsonPathToList(paginatedChapterList, r'$.data[*]', 0); for (var res in dataList) { - String scan = "".toString(); + String scan = ""; final groups = jsonPathToList(res, r'$.relationships[?@.id!="00e03853-1b96-4f41-9542-c71b8692033b"]', 0); - String chapName = "".toString(); + String chapName = ""; for (var element in groups) { final data = getMapValue(element, "attributes", encode: true); if (data.isNotEmpty) { final name = getMapValue(data, "name"); - scan += "$name".toString(); + scan += "$name"; final username = getMapValue(data, "username"); if (username.isNotEmpty) { if (scan.isEmpty) { - scan += "Uploaded by $username".toString(); + scan += "Uploaded by $username"; } } } } if (scan.isEmpty) { - scan = "No Group".toString(); + scan = "No Group"; } final dataRes = getMapValue(res, "attributes", encode: true); if (dataRes.isNotEmpty) { @@ -282,26 +282,26 @@ class MangaDex extends MProvider { final volume = getMapValue(data, "volume"); if (volume.isNotEmpty) { if (volume != "null") { - chapName = "Vol.$volume ".toString(); + chapName = "Vol.$volume "; } } final chapter = getMapValue(data, "chapter"); if (chapter.isNotEmpty) { if (chapter != "null") { - chapName += "Ch.$chapter ".toString(); + chapName += "Ch.$chapter "; } } final title = getMapValue(data, "title"); if (title.isNotEmpty) { if (title != "null") { if (chapName.isNotEmpty) { - chapName += "- ".toString(); + chapName += "- "; } - chapName += "$title".toString(); + chapName += "$title"; } } if (chapName.isEmpty) { - chapName += "Oneshot".toString(); + chapName += "Oneshot"; } final date = getMapValue(data, "publishAt"); final id = getMapValue(res, "id"); @@ -319,19 +319,13 @@ class MangaDex extends MProvider { } Future paginatedChapterListRequest( - String mangaId, int offset, String lang) async { + String mangaId, int offset, String lang, int sourceId) async { final url = - 'https://api.mangadex.org/manga/$mangaId/feed?limit=500&offset=$offset&includes[]=user&includes[]=scanlation_group&order[volume]=desc&order[chapter]=desc&translatedLanguage[]=$lang&includeFuturePublishAt=0&includeEmptyPages=0${getMDXContentRating()}'; + 'https://api.mangadex.org/manga/$mangaId/feed?limit=500&offset=$offset&includes[]=user&includes[]=scanlation_group&order[volume]=desc&order[chapter]=desc&translatedLanguage[]=$lang&includeFuturePublishAt=0&includeEmptyPages=0${preferenceContentRating(sourceId)}'; final res = await http('GET', json.encode({"url": url})); return res; } - String getMDXContentRating() { - String ctnRating = - "&contentRating[]=suggestive&contentRating[]=safe&contentRating[]=erotica&contentRating[]=pornographic"; - return ctnRating; - } - String findTitle(String dataRes, String lang) { final attributes = getMapValue(dataRes, "attributes", encode: true); final altTitlesJ = @@ -349,7 +343,8 @@ class MangaDex extends MProvider { return title; } - String getCover(String dataRes) { + String getCover(String dataRes, int sourceId) { + final coverQuality = getPreferenceValue(sourceId, "cover_quality"); final relationships = json .decode(getMapValue(dataRes, "relationships", encode: true)) as List; String coverFileName = "".toString(); @@ -360,7 +355,7 @@ class MangaDex extends MProvider { final attributes = getMapValue(json.encode(a), "attributes", encode: true); coverFileName = - "https://uploads.mangadex.org/covers/${getMapValue(dataRes, "id")}/${getMapValue(attributes, "fileName")}"; + "https://uploads.mangadex.org/covers/${getMapValue(dataRes, "id")}/${getMapValue(attributes, "fileName")}$coverQuality"; } } } @@ -368,7 +363,7 @@ class MangaDex extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ CheckBoxFilter( "Has available chapters", "", "HasAvailableChaptersFilter"), @@ -510,6 +505,83 @@ class MangaDex extends MProvider { ]; } + @override + List getSourcePreferences(MSource source) { + return [ + ListPreference( + key: "cover_quality", + title: "Cover quality", + summary: "", + valueIndex: 0, + entries: ["Original", "Medium", "Low"], + entryValues: ["", ".512.jpg", ".256.jpg"]), + MultiSelectListPreference( + key: "content_rating", + title: "Default content rating", + summary: "Show content with the selected rating by default", + valueIndex: 0, + entries: [ + "safe", + "suggestive", + "erotica", + "pornographic" + ], + entryValues: [ + "contentRating[]=safe", + "contentRating[]=suggestive", + "contentRating[]=erotica", + "contentRating[]=pornographic" + ], + values: [ + "contentRating[]=safe", + "contentRating[]=suggestive" + ]), + MultiSelectListPreference( + key: "original_languages", + title: "Filter original languages", + summary: + "Only show content that was originaly published in the selected languages in both latest and browse", + valueIndex: 0, + entries: [ + "Japanese", + "Chinese", + "Korean" + ], + entryValues: [ + "originalLanguage[]=ja", + "originalLanguage[]=zh&originalLanguage[]=zh-hk", + "originalLanguage[]=ko" + ], + values: []), + ]; + } + + String preferenceContentRating(int sourceId) { + final contentRating = + getPreferenceValue(sourceId, "content_rating") as List; + String contentRatingStr = ""; + if (contentRating.isNotEmpty) { + contentRatingStr = "&"; + for (var ctn in contentRating) { + contentRatingStr += "&$ctn"; + } + } + return contentRatingStr; + } + + String preferenceOriginalLanguages(int sourceId) { + final originalLanguages = + getPreferenceValue(sourceId, "original_languages") as List; + String originalLanguagesStr = ""; + if (originalLanguages.isNotEmpty) { + originalLanguagesStr = "&"; + for (var language in originalLanguages) { + originalLanguagesStr += "&$language"; + } + } + return originalLanguagesStr; + } + String ll(String url) { if (url.contains("?")) { return "&"; diff --git a/manga/src/all/mangadex/sources.dart b/manga/src/all/mangadex/sources.dart index 917c2fec..778ecfe9 100644 --- a/manga/src/all/mangadex/sources.dart +++ b/manga/src/all/mangadex/sources.dart @@ -4,7 +4,7 @@ import '../../../../utils/utils.dart'; const apiUrl = 'https://api.mangadex.org'; const baseUrl = 'https://mangadex.org'; const isNsfw = true; -const mangadexVersion = "0.0.45"; +const mangadexVersion = "0.0.5"; const mangadexSourceCodeUrl = "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/manga/src/all/mangadex/mangadex-v$mangadexVersion.dart"; String _iconUrl = getIconUrl("mangadex", "all"); diff --git a/manga/src/en/mangahere/mangahere-v0.0.4.dart b/manga/src/en/mangahere/mangahere-v0.0.4.dart index fb20f8da..91188591 100644 --- a/manga/src/en/mangahere/mangahere-v0.0.4.dart +++ b/manga/src/en/mangahere/mangahere-v0.0.4.dart @@ -259,7 +259,7 @@ class MangaHere extends MProvider { } @override - List getFilterList() { + List getFilterList(MSource source) { return [ SelectFilter("TypeList", "Type", 1, [ SelectFilterOption("American Manga", "5"), From db0b3c1b5b9afb0cc753bd759fd0a85735ab0677 Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:40:28 +0100 Subject: [PATCH 3/5] + --- manga/multisrc/nepnep/nepnep-v0.0.4.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/manga/multisrc/nepnep/nepnep-v0.0.4.dart b/manga/multisrc/nepnep/nepnep-v0.0.4.dart index b7b014f9..951985b5 100644 --- a/manga/multisrc/nepnep/nepnep-v0.0.4.dart +++ b/manga/multisrc/nepnep/nepnep-v0.0.4.dart @@ -10,13 +10,7 @@ class NepNep extends MProvider { final res = await http('GET', json.encode(data)); final directory = directoryFromDocument(res); - final resSort = (json.decode(directory) as List>); - resSort.sort( - (Map a, Map b) => - (a["vm"] as String).compareTo(b["vm"] as String), - ); - - // sortMapList(json.decode(directory), "vm", 1); + final resSort = sortMapList(json.decode(directory), "vm", 1); return parseDirectory(json.encode(resSort)); } From a04c2019ac6511786da97f855515fc7563aabae4 Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Tue, 5 Dec 2023 09:06:21 +0100 Subject: [PATCH 4/5] + --- manga/multisrc/nepnep/nepnep-v0.0.4.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manga/multisrc/nepnep/nepnep-v0.0.4.dart b/manga/multisrc/nepnep/nepnep-v0.0.4.dart index 951985b5..f84e959c 100644 --- a/manga/multisrc/nepnep/nepnep-v0.0.4.dart +++ b/manga/multisrc/nepnep/nepnep-v0.0.4.dart @@ -12,7 +12,7 @@ class NepNep extends MProvider { final directory = directoryFromDocument(res); final resSort = sortMapList(json.decode(directory), "vm", 1); - return parseDirectory(json.encode(resSort)); + return parseDirectory(resSort); } @override From 4d1596f59afa3e6f252597d5eee614707305be2c Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Tue, 5 Dec 2023 18:19:51 +0100 Subject: [PATCH 5/5] New source AnimeSaturn(IT) --- anime/source_generator.dart | 4 +- anime/src/it/animesaturn-v0.0.1.dart | 360 +++++++++++++++++++++++++++ anime/src/it/source.dart | 16 ++ icons/mangayomi-it-animesaturn.png | Bin 0 -> 6664 bytes 4 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 anime/src/it/animesaturn-v0.0.1.dart create mode 100644 anime/src/it/source.dart create mode 100644 icons/mangayomi-it-animesaturn.png diff --git a/anime/source_generator.dart b/anime/source_generator.dart index 9589261a..1c9e1b73 100644 --- a/anime/source_generator.dart +++ b/anime/source_generator.dart @@ -14,6 +14,7 @@ import 'src/fr/otakufr/source.dart'; import 'src/id/nimegami/source.dart'; import 'src/id/oploverz/source.dart'; import 'src/id/otakudesu/source.dart'; +import 'src/it/source.dart'; void main() { List _sourcesList = [ @@ -28,7 +29,8 @@ void main() { nimegami, oploverz, aniwave, - ...dopeflixSourcesList + ...dopeflixSourcesList, + animesaturn ]; final List> jsonList = _sourcesList.map((source) => source.toJson()).toList(); diff --git a/anime/src/it/animesaturn-v0.0.1.dart b/anime/src/it/animesaturn-v0.0.1.dart new file mode 100644 index 00000000..28fe45b4 --- /dev/null +++ b/anime/src/it/animesaturn-v0.0.1.dart @@ -0,0 +1,360 @@ +import 'package:mangayomi/bridge_lib.dart'; +import 'dart:convert'; + +class AnimeSaturn extends MProvider { + AnimeSaturn(); + + @override + Future getPopular(MSource source, int page) async { + final data = {"url": "${source.baseUrl}/animeincorso?page=$page"}; + final res = await http('GET', json.encode(data)); + + List animeList = []; + + final urls = xpath(res, + '//*[@class="sebox"]/div[@class="msebox"]/div[@class="headsebox"]/div[@class="tisebox"]/h2/a/@href'); + + final names = xpath(res, + '//*[@class="sebox"]/div[@class="msebox"]/div[@class="headsebox"]/div[@class="tisebox"]/h2/a/text()'); + + final images = xpath(res, + '//*[@class="sebox"]/div[@class="msebox"]/div[@class="bigsebox"]/div/img[@class="attachment-post-thumbnail size-post-thumbnail wp-post-image"]/@src'); + + for (var i = 0; i < names.length; i++) { + MManga anime = MManga(); + anime.name = formatTitle(names[i]); + anime.imageUrl = images[i]; + anime.link = urls[i]; + animeList.add(anime); + } + return MPages(animeList, true); + } + + @override + Future getLatestUpdates(MSource source, int page) async { + final data = {"url": "${source.baseUrl}/newest?page=$page"}; + final res = await http('GET', json.encode(data)); + + List animeList = []; + + final urls = xpath(res, '//*[@class="card mb-4 shadow-sm"]/a/@href'); + + final names = xpath(res, '///*[@class="card mb-4 shadow-sm"]/a/@title'); + + final images = xpath(res, + '//*[@class="card mb-4 shadow-sm"]/a/img[@class="new-anime"]/@src'); + + for (var i = 0; i < names.length; i++) { + MManga anime = MManga(); + anime.name = formatTitle(names[i]); + anime.imageUrl = images[i]; + anime.link = urls[i]; + animeList.add(anime); + } + return MPages(animeList, true); + } + + @override + Future search( + MSource source, String query, int page, FilterList filterList) async { + final filters = filterList.filters; + String url = ""; + + if (query.isNotEmpty) { + url = "${source.baseUrl}/animelist?search=$query"; + } else { + url = "${source.baseUrl}/filter?"; + int variantgenre = 0; + int variantstate = 0; + int variantyear = 0; + for (var filter in filters) { + if (filter.type == "GenreFilter") { + final genre = (filter.state as List).where((e) => e.state).toList(); + if (genre.isNotEmpty) { + for (var st in genre) { + url += "&categories%5B${variantgenre}%5D=${st.value}"; + variantgenre++; + } + } + } else if (filter.type == "YearList") { + final years = (filter.state as List).where((e) => e.state).toList(); + if (years.isNotEmpty) { + for (var st in years) { + url += "&years%5B${variantyear}%5D=${st.value}"; + variantyear++; + } + } + } else if (filter.type == "StateList") { + final states = (filter.state as List).where((e) => e.state).toList(); + if (states.isNotEmpty) { + for (var st in states) { + url += "&states%5B${variantstate}%5D=${st.value}"; + variantstate++; + } + } + } else if (filter.type == "LangList") { + final lang = filter.values[filter.state].value; + if (lang.isNotEmpty) { + url += "&language%5B0%5D=$lang"; + } + } + } + url += "&page=$page"; + } + final data = {"url": url}; + final res = await http('GET', json.encode(data)); + + List animeList = []; + List urls = []; + List names = []; + List images = []; + if (query.isNotEmpty) { + urls = xpath(res, + '//*[@class="list-group"]/li[@class="list-group-item bg-dark-as-box-shadow"]/div[@class="item-archivio"]/div[@class="info-archivio"]/h3/a[@class="badge badge-archivio badge-light"]/@href'); + + names = xpath(res, + '//*[@class="list-group"]/li[@class="list-group-item bg-dark-as-box-shadow"]/div[@class="item-archivio"]/div[@class="info-archivio"]/h3/a[@class="badge badge-archivio badge-light"]/text()'); + + images = xpath(res, + '//*[@class="list-group"]/li[@class="list-group-item bg-dark-as-box-shadow"]/div[@class="item-archivio"]/a/img/@src'); + } else { + urls = xpath(res, '//*[@class="card mb-4 shadow-sm"]/a/@href'); + + names = xpath(res, '//*[@class="card mb-4 shadow-sm"]/a/text()'); + + images = xpath(res, + '//*[@class="card mb-4 shadow-sm"]/a/img[@class="new-anime"]/@src'); + } + + for (var i = 0; i < names.length; i++) { + MManga anime = MManga(); + anime.name = formatTitle(names[i]); + anime.imageUrl = images[i]; + anime.link = urls[i]; + animeList.add(anime); + } + return MPages(animeList, query.isEmpty); + } + + @override + Future getDetail(MSource source, String url) async { + final statusList = [ + {"In corso": 0, "Finito": 1} + ]; + final data = {"url": url}; + final res = await http('GET', json.encode(data)); + MManga anime = MManga(); + final details = xpath(res, + '//div[@class="container shadow rounded bg-dark-as-box mb-3 p-3 w-100 text-white"]/text()') + .first; + + anime.status = parseStatus( + details.substring( + details.indexOf("Stato:") + 6, details.indexOf("Data di uscita:")), + statusList); + anime.author = details.substring(7, details.indexOf("Stato:")); + final description = xpath(res, '//*[@id="shown-trama"]/text()'); + final descriptionFull = xpath(res, '//*[@id="full-trama"]/text()'); + if (description.isNotEmpty) { + anime.description = description.first; + } else { + anime.description = ""; + } + if (descriptionFull.isNotEmpty) { + if (descriptionFull.first.length > anime.description.length) { + anime.description = descriptionFull.first; + } + } + + anime.genre = xpath(res, + '//*[@class="container shadow rounded bg-dark-as-box mb-3 p-3 w-100"]/a/text()'); + + final epUrls = xpath(res, + '//*[@class="btn-group episodes-button episodi-link-button"]/a/@href'); + + final titles = xpath(res, + '//*[@class="btn-group episodes-button episodi-link-button"]/a/text()'); + + List? episodesList = []; + for (var i = 0; i < epUrls.length; i++) { + MChapter episode = MChapter(); + episode.name = titles[i]; + episode.url = epUrls[i]; + episodesList.add(episode); + } + + anime.chapters = episodesList.reversed.toList(); + return anime; + } + + @override + Future> getVideoList(MSource source, String url) async { + final res = await http('GET', json.encode({"url": url})); + + final urlVid = xpath(res, '//a[contains(@href,"/watch")]/@href').first; + final resVid = await http('GET', json.encode({"url": urlVid})); + String masterUrl = ""; + if (resVid.contains("jwplayer(")) { + masterUrl = substringBefore(substringAfter(resVid, "file: \""), "\""); + } else { + masterUrl = querySelectorAll(resVid, + selector: "source", + typeElement: 3, + attributes: "src", + typeRegExp: 0) + .first; + } + + List videos = []; + if (masterUrl.endsWith("playlist.m3u8")) { + 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 = quality; + videos.add(video); + } + } else { + MVideo video = MVideo(); + video + ..url = masterUrl + ..originalUrl = masterUrl + ..quality = "Qualità predefinita"; + videos.add(video); + } + return sortVideos(videos, source.id); + } + + String formatTitle(String titlestring) { + return titlestring + .replaceAll("(ITA) ITA", "Dub ITA") + .replaceAll("(ITA)", "Dub ITA") + .replaceAll("Sub ITA", ""); + } + + @override + List getFilterList(MSource source) { + return [ + HeaderFilter("Ricerca per titolo ignora i filtri e viceversa"), + GroupFilter("GenreFilter", "Generi", [ + CheckBoxFilter("Arti Marziali", "Arti Marziali"), + CheckBoxFilter("Avventura", "Avventura"), + CheckBoxFilter("Azione", "Azione"), + CheckBoxFilter("Bambini", "Bambini"), + CheckBoxFilter("Commedia", "Commedia"), + CheckBoxFilter("Demenziale", "Demenziale"), + CheckBoxFilter("Demoni", "Demoni"), + CheckBoxFilter("Drammatico", "Drammatico"), + CheckBoxFilter("Ecchi", "Ecchi"), + CheckBoxFilter("Fantasy", "Fantasy"), + CheckBoxFilter("Gioco", "Gioco"), + CheckBoxFilter("Harem", "Harem"), + CheckBoxFilter("Hentai", "Hentai"), + CheckBoxFilter("Horror", "Horror"), + CheckBoxFilter("Josei", "Josei"), + CheckBoxFilter("Magia", "Magia"), + CheckBoxFilter("Mecha", "Mecha"), + CheckBoxFilter("Militari", "Militari"), + CheckBoxFilter("Mistero", "Mistero"), + CheckBoxFilter("Musicale", "Musicale"), + CheckBoxFilter("Parodia", "Parodia"), + CheckBoxFilter("Polizia", "Polizia"), + CheckBoxFilter("Psicologico", "Psicologico"), + CheckBoxFilter("Romantico", "Romantico"), + CheckBoxFilter("Samurai", "Samurai"), + CheckBoxFilter("Sci-Fi", "Sci-Fi"), + CheckBoxFilter("Scolastico", "Scolastico"), + CheckBoxFilter("Seinen", "Seinen"), + CheckBoxFilter("Sentimentale", "Sentimentale"), + CheckBoxFilter("Shoujo Ai", "Shoujo Ai"), + CheckBoxFilter("Shoujo", "Shoujo"), + CheckBoxFilter("Shounen Ai", "Shounen Ai"), + CheckBoxFilter("Shounen", "Shounen"), + CheckBoxFilter("Slice of Life", "Slice of Life"), + CheckBoxFilter("Soprannaturale", "Soprannaturale"), + CheckBoxFilter("Spazio", "Spazio"), + CheckBoxFilter("Sport", "Sport"), + CheckBoxFilter("Storico", "Storico"), + CheckBoxFilter("Superpoteri", "Superpoteri"), + CheckBoxFilter("Thriller", "Thriller"), + CheckBoxFilter("Vampiri", "Vampiri"), + CheckBoxFilter("Veicoli", "Veicoli"), + CheckBoxFilter("Yaoi", "Yaoi"), + CheckBoxFilter("Yuri", "Yuri"), + ]), + GroupFilter("YearList", "Anno di Uscita", [ + for (var i = 1969; i < 2022; i++) + CheckBoxFilter(i.toString(), i.toString()), + ]), + GroupFilter("StateList", "Stato", [ + CheckBoxFilter("In corso", "0"), + CheckBoxFilter("Finito", "1"), + CheckBoxFilter("Non rilasciato", "2"), + CheckBoxFilter("Droppato", "3"), + ]), + SelectFilter("LangList", "Lingua", 0, [ + SelectFilterOption("", ""), + SelectFilterOption("Subbato", "0"), + SelectFilterOption("Doppiato", "1"), + ]), + ]; + } + + @override + List getSourcePreferences(MSource source) { + return [ + ListPreference( + key: "preferred_quality", + title: "Qualità preferita", + summary: "", + valueIndex: 0, + entries: ["1080p", "720p", "480p", "360p", "240p", "144p"], + entryValues: ["1080", "720", "480", "360", "240", "144"]), + ]; + } + + 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; + } +} + +AnimeSaturn main() { + return AnimeSaturn(); +} diff --git a/anime/src/it/source.dart b/anime/src/it/source.dart new file mode 100644 index 00000000..dc47e3bf --- /dev/null +++ b/anime/src/it/source.dart @@ -0,0 +1,16 @@ +import '../../../model/source.dart'; +import '../../../utils/utils.dart'; + +Source get animesaturn => _animesaturn; +const animesaturnVersion = "0.0.1"; +const animesaturnCodeUrl = + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/anime/src/it/animesaturn/animesaturn-v$animesaturnVersion.dart"; +Source _animesaturn = Source( + name: "AnimeSaturn", + baseUrl: "https://www.animesaturn.tv", + lang: "it", + typeSource: "single", + iconUrl: getIconUrl("animesaturn", "it"), + sourceCodeUrl: animesaturnCodeUrl, + version: animesaturnVersion, + isManga: false); diff --git a/icons/mangayomi-it-animesaturn.png b/icons/mangayomi-it-animesaturn.png new file mode 100644 index 0000000000000000000000000000000000000000..39ee2d6209d7688441f4a79141df16a1a8e29df0 GIT binary patch literal 6664 zcmai3<6v|XB|r9tVXYe{Jqq&pS_>F!)Yr9rv{l#uS0?)Z_?3rNFK(j^T`J>Ng@ z-iI4s?%bI(bMBopCt6EG5g&&d2LJ%zD=W$A{Hx~w8`zlt@>aU92mnAer7R}{{kVMc z!#7oLZfPi}bXDqhG8^%FHi;lq1xx1GEcCyP z%)(@Vxckx-JL_8D4)X%Oq(vVo10Mqh-YXMkF9&lzxbF$eQ7m2RtqRB&l=^8A`J4NpXi?NxGV9N^gV^C$z*gWND1)6B_6;j|%pl zAB+5-#PG2ru5;-_xq?=J%BU&I^czPd)_XyzybF@|_KLp4+rb`!MD!>W*%C+N!-0V( z)$fK9Gvb^VwI1^@Wo4yFF&I_oGh~`WM{w#zK@9!ivhvEUutWrb)b+7dEf#8bWC&(2 zxAImMgQ(q)b=Q8>I}-@qt+rp7^%JW%CcCK*mGP5=vv zGBMnnN#Y~ks4q8|oU8rbGYEWF%cKqoG}YL|ym#vj1wQ5&B3eD?6dS1IT@tU3o}Q_<6F)^ z5}ubJ>N0O z1mj7ZuZWwrZg=X|l5^W0Z8Y$P&XKjNaK?N>p0C5AC9K~+umrHi+=l? zy4uIh`~{vIzVk*m@}~bH^22IzQ3H}vgyUFq0%PoGyS`UR-d_QUWAbN+j=>eE+|QEF zB_*Q}m`m5p^lFXY=gNBDVCAS_k0=H_2`))OWUPC+kdFH zF=>UO+D`Q1e7rpjE5QWg^^2zq^uD3Uz8Zq{1lXN=Se4M}`P43t$HUv( zo{NT&Z9BRqo|c*rQuoVBCtA%PQVx?37Do;1e=$&VpXzqE!?R7Y^TOq5k|qFN;ZI*` zr>FJRT(N~~+LFYowV{VrmA8AeJ+*LAn19|3GE z0$t><0YdABL4qnV>uv;WqhoIlaRbo{Sm^v_INwin%`qzTzu-|4$6Hm(}BizA0{nCS2H z^jwVVzBH1m29!}=IQhypvFnkFzPo3lCMMgXK-DKNmT~T!ozi0M#ak1#TSM6O&BN~!Pwdg^Yz*H1Ca0W4 zcc_^~netQ*+fIlj2}Y*kN!W76ZyG2nU6(np-#BWzY>yuGGhBr*_quvW3EsvXIfpqM z@fuz;R0?y+zV$K+P{hA*M(u}1rd(Xg)Ldj&+45% zE}i|w@aOt)!^2}H$>#~k#j)HKEXi>{B&0^au}{{JF~6y!SV0&*U%5o&uX}*oC!?ue z{@4I@XT6k5L@J4T*y`Bd&|uiRE)G=a-Aa={)7}!04pL%EoR4tI-;&``iMA@lS-_X4 zzm$|W`U-DzeooOa(t`nE*K;7Nbi4=gNVuo~cns zS~RQa${Bs0Gtgn5Ei2Y=n>R;+P5$46;gp@(DAVWN>)Wo$Rue6{s5=KBGx}>#^)^zZ`C^ZJwE%WIudkz zZCGT$hN8Axz*m3f{M+}bp}O)IQ`UwbPZ%Y~Q(IkOWm=_c?mH7*W{R9Zw8XV* z+17URpx;8L|5@{=kmg2MP#$xvk0!|b+&~Yq= z-krG3_@EbztT`DwU(Uh=(S*L$khdQ1*7(>Emt_6(Ro1qncq*O|ItMXUOmG6WnB1?~ zFBjD&xBD(K@)^SnwvB{uQK+P?&4S|p40xTkowMuLzQkqKd<0zHmT|vN&HCTAww9t{ zBxb~#Hg(7NEp*-`AXmUwSew$9%LtZ`KeKDjw87WA=VEmJ`=GrU%eT#6jGOJqwK%faP@(;K zg)S6k)t~%byr1e-@%qc2>&`@t1$r$TT4_~P#5P$PoS$Xfnp&N4$&4tzx*ag`jQ)4p zOmJLSYyAffR1E(+c5|G!Ik(Wp4vuL)mWUx1SmeQmqE(b=nfwE^VK=$w<|Fml!I`DF zIXZS|Qg)`u=5^h+TMjYyO+fAU@&oEzp1^pA1k6bZ9ujT!aPy>S3=D}LB$@(gf+A6Z z@r;6YD3{SS1C!M1#10<`J3Chq*XV?8-U=YMHv7bA&)b}KBXVAWdRxgd1 zJeeNWF-xDSNC!3JqT)lqi;D6iSU-|-hKgos2S8cp7|1f`Mbm)R2S&QkzYUkLNu>Qu zYNUcf+srSt2rA6}tO_NUs%=~|v|Ifb*p4!O4aq+mgvV8HI)?lgQR75^)Sb#q>zsF0ulu^4Nj-RYWo*8x8CUbk?uXQ-e?*b-M=d%~-kpKTRjxW@EbkRPj_l?y zi}$?g?vFIeEHKh=2(1=eN7WOCcH&5QtN#RB`QDXAak7mc*!@@;zBa2VB7{_eNtwQBjO{emzE# z+H;Mu7ef&d{Tt>dOO&?1(Op)x)ZqwTS|oknW}h-@3l{VV#X$)27@(25Z$ixE+!h6k#wFp z{Bf~XlleTnv1zC1Dj3O<5XiaK`-ZQe%r?iZd(e74vIq01Qhl%m1m*xRc{rta zJ9GV0I2ZH~h33phgN^zny)4d-RrBc@*P2BFOxxRYNMl6Ryck*R9>?Ii3J@*Kgz)OmG* zH2P;6lQe#`RM8kwUm?)YIB|Id=MJ{y1V{Jj6K>1=@twiZd5e^Q7TUfzdb;)fg>njA zm#{DcREIwkj+kDZ|9osp2*6!_%eEqMO?crlvW*g!GlyT?V|X{%F&4b~(^QZK`_qEz&TCWHu=@$^*Akzye)-x!1}Alz?dBC$ODiT0;FtA#kySqmAw|jUFhCA zyAno76UCl&qhotau9hk$bc^Ye+Q^yPiwJ(3LqwHDHdFjY|FA@CZvdYnppwi<=-BPL zjx;1(zcG$mauzL0D6f#!GogCj%r)HZB?%j_8247|69*+{ei5vo;o-mt;h@j^=-|8< zyx0|PXz`!XF^h3DzqZGW@7JUC4DAXTm?4q+(hJN78aR?=CUK5sz5uDQ@LWXXikvPQ z@w$uX0+i5x1eYBRv^0H5PeMl)0V4j(A9z=>Zus|Bd^kS+D)CaI>k#W*c6-e<+_*pC ztNL&Mx+ueH;3{dFs@M2ZBMYdGSvccEY`luB-mp{<1^%_!CLYxNaKGV~h7?+Cznofo z+!}X)+ef2?ykf4Xtu?ch=37BNxBqJ2q9zhHzG6TdXuCL@9L>0nAH4AXuts%dHBSas zr3cEr);Jb+l4&V;Gb(QSaMIACJ~jEP$8j}E`JoaZq!gel)G^kg-Gqd(%q@$IB`ap` zfI6wB1&2z1PLL^PgS3HO=J16TrGOKTdS?_K%1>hD=M_O zHC-kAPi#84v%<~2wXmQlrY6M=U0-&W$>OHQEsUSlEKNqzX&9$R_!B6jpYH?jLjFp!w= zkLlMpHchqnPIEJR%VJ^(e!Rlavs0Os*W~otq=S8u*B$dxSBq&Ww#9M85ki@i4r70; zortGr%FCi{#Vog$4iZX7HVc%A%7G70fkCY5@mYNBEqpGETsVq|mucH!))q}?poeZ;ZgZbk1ewY*Fe*uz6bFm4EOqO-l}DDo$-OUO zHl6%AkYeq*om7R5jn1VnwpQ0DH$3g=JIxUz2=InLboYuyg?WdIOTZp%HL|^a*}JPKeRjAQ$~c)WB&^M!li`mS4^p= zT(WRbFiQ5$2r8IOE>05)ff?)v_UE;<wAg@%uxKMu+;&;PA9rMv<)5_*Z zRjQ42@gu09Ig}o|t@OuTM|L5ru@mr#sGZnbBm$mc5lXqtXUQfuZ%)*)5GhIonejr$ z6V#Rk(yeU0%>XK~K(#@TGU1Le_`T?wgu%V1arNpQDXa>Oz-2|C1ioDXq^_o&!a_AS*JY@QQG=IFZK*|hg@bDU@*advQgD>~X(Nqd)$_j}4tpPP zBjS2NbRY2Tdkbf^3y-bVUwhp&>p<%=|FH>b3~kzC-{9_EdpA4{0R)>jrE9*HdMxOO z{F5WNL}Nm~a;*HTekW^%l-d8795W)nutMtbna96d82S12_37zh&HQ{pHvYBhTO=C7 zyP~9;wNckh{EL(g%6mj(e*#SQ9|#0lmw8m2gKqG-GWbGC2ufzn3}Q{m zdzMG|JNKDU=ks-VK*-H^Ak7C)D2>ynpKUod!WpFs9XbjiP{N3pn1H4H^cGz3-S5WW zsXdoVFYh$3t1IYRbTZBA0*@6u`kBq7?pBRq=ZsZ=DIbmE=nYC1apna2qU z{jRaHq|6dD)Gkc{SD!i#_Fecgmv66Jd5lc;5v}*C{qXOoWA`C0vWiS7icBC`#Wouf zv5^6q=Lvc*b0^Kkf@(ruWz7H2LHXyey!4z0ktxM_rSmab{Bw8!%JLd=HL~WR{{!sy B%4+}s literal 0 HcmV?d00001