From 8d388d8c9838617b9d7848e02dbe227f5ffdfb91 Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Sat, 3 Feb 2024 16:59:24 +0100 Subject: [PATCH] New source: Yomiroll (ALL) --- anime/source_generator.dart | 4 +- anime/src/all/yomiroll/icon.png | Bin 0 -> 4859 bytes anime/src/all/yomiroll/source.dart | 19 + anime/src/all/yomiroll/yomiroll.dart | 1153 ++++++++++++++++++++++++++ 4 files changed, 1175 insertions(+), 1 deletion(-) create mode 100644 anime/src/all/yomiroll/icon.png create mode 100644 anime/src/all/yomiroll/source.dart create mode 100644 anime/src/all/yomiroll/yomiroll.dart diff --git a/anime/source_generator.dart b/anime/source_generator.dart index 54e0c764..159feed5 100644 --- a/anime/source_generator.dart +++ b/anime/source_generator.dart @@ -7,6 +7,7 @@ import 'multisrc/dopeflix/sources.dart'; import 'multisrc/zorotheme/sources.dart'; import 'src/all/animeworldindia/sources.dart'; import 'src/all/nyaa/source.dart'; +import 'src/all/yomiroll/source.dart'; import 'src/ar/okanime/source.dart'; import 'src/de/aniflix/source.dart'; import 'src/en/aniwave/source.dart'; @@ -50,7 +51,8 @@ void main() { nineanimetv, aniflix, ...animeworldindiaSourcesList, - nyaaSource + nyaaSource, + yomirollSource ]; final List> jsonList = _sourcesList.map((source) => source.toJson()).toList(); diff --git a/anime/src/all/yomiroll/icon.png b/anime/src/all/yomiroll/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c0adfb06ce95bd8c11bffe33e8170c8011b9805f GIT binary patch literal 4859 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 literal 0 HcmV?d00001 diff --git a/anime/src/all/yomiroll/source.dart b/anime/src/all/yomiroll/source.dart new file mode 100644 index 00000000..3a078373 --- /dev/null +++ b/anime/src/all/yomiroll/source.dart @@ -0,0 +1,19 @@ +import '../../../../model/source.dart'; + +const _yomirollVersion = "0.0.1"; +const _yomirollSourceCodeUrl = + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/anime/src/all/yomiroll/yomiroll.dart"; + +String _iconUrl = + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/anime/src/all/yomiroll/icon.png"; + +Source get yomirollSource => _yomirollSource; +Source _yomirollSource = Source( + name: 'Yomiroll', + baseUrl: "https://crunchyroll.com", + lang: "all", + typeSource: "multiple", + iconUrl: _iconUrl, + version: _yomirollVersion, + isManga: false, + sourceCodeUrl: _yomirollSourceCodeUrl); diff --git a/anime/src/all/yomiroll/yomiroll.dart b/anime/src/all/yomiroll/yomiroll.dart new file mode 100644 index 00000000..7ac455a6 --- /dev/null +++ b/anime/src/all/yomiroll/yomiroll.dart @@ -0,0 +1,1153 @@ +import 'package:mangayomi/bridge_lib.dart'; +import 'dart:convert'; + +class YomiRoll extends MProvider { + YomiRoll({required this.source}); + + MSource source; + + final Client client = Client(source); + + String crUrl = "https://beta-api.crunchyroll.com"; + String crApiUrl = "https://beta-api.crunchyroll.com/content/v2"; + + @override + Future getPopular(int page) async { + final start = page != 1 ? "start=${(page - 1) * 36}&" : ""; + final res = await interceptAccesTokenAndGetResponse( + "$crApiUrl/discover/browse?${start}n=36&sort_by=popularity&locale=en-US"); + return await animeFromRes(res, start); + } + + @override + Future getLatestUpdates(int page) async { + final start = page != 1 ? "start=${(page - 1) * 36}&" : ""; + final res = await interceptAccesTokenAndGetResponse( + "$crApiUrl/discover/browse?${start}n=36&sort_by=newly_added&locale=en-US"); + return await animeFromRes(res, start); + } + + @override + Future search(String query, int page, FilterList filterList) async { + final filters = filterList.filters; + String url = ""; + final start = page != 1 ? "start=${(page - 1) * 36}&" : ""; + if (query.isNotEmpty) { + final typeFilter = filters + .where((e) => e.type == "TypeFilter" ? true : false) + .toList() + .first; + final type = typeFilter.values[typeFilter.state].value; + url = + "$crApiUrl/discover/search?${start}n=36&q=${query.toLowerCase().replaceAll(" ", "+")}&type=$type"; + } else { + url = "$crApiUrl/discover/browse?${start}n=36"; + for (var filter in filters) { + if (filter.type == "MediaFilter") { + url += filter.values[filter.state].value; + } else if (filter.type == "CategoryFilter") { + url += filter.values[filter.state].value; + } else if (filter.type == "SortFilter") { + url += "&sort_by=${filter.values[filter.state].value}"; + } else if (filter.type == "LanguageFilter") { + final categories = + (filter.state as List).where((e) => e.state).toList(); + if (categories.isNotEmpty) { + for (var st in categories) { + url += st.value; + } + } + } + } + } + String res = await interceptAccesTokenAndGetResponse(url); + if (query.isNotEmpty) { + final resJson = json.decode(res)["data"][0]; + res = json.encode({"total": resJson["count"], "data": resJson["items"]}); + } else {} + + return await animeFromRes(res, start); + } + + @override + Future getDetail(String url) async { + final media = json.decode(url); + final id = media["id"]; + final type = media["type"]; + bool isSerie = type == "series"; + String res = ""; + if (isSerie) { + res = await interceptAccesTokenAndGetResponse( + "$crApiUrl/cms/series/$id?locale=en-US"); + } else { + res = await interceptAccesTokenAndGetResponse( + "$crApiUrl/cms/movie_listings/$id/movies"); + } + Map data = + (json.decode(res)["data"] as List>).first; + MManga anime = MManga(); + anime.author = data["content_provider"]; + String description = data["description"]; + description += "\n\nLanguage:"; + if (data["is_subbed"]) { + description += " Sub"; + } + if (data["is_dubbed"]) { + description += " Dub"; + } + description += "\nMaturity Ratings: "; + description += (data["maturity_ratings"] as List).join(", "); + description += "\n\nAudio: "; + description += (data["audio_locales"] as List) + .map((e) => getLocale(e)) + .toList() + .toSet() + .toList() + .join(", "); + description += "\n\nSubs: "; + description += (data["subtitle_locales"] as List) + .map((e) => getLocale(e)) + .toList() + .toSet() + .toList() + .join(", "); + anime.description = description; + + String seasonsRes = ""; + if (isSerie) { + seasonsRes = await interceptAccesTokenAndGetResponse( + "$crApiUrl/cms/series/$id/seasons"); + } else { + seasonsRes = await interceptAccesTokenAndGetResponse( + "$crApiUrl/cms/movie_listings/$id/movies"); + } + + List> seasons = json.decode(seasonsRes)["data"]; + List? episodesList = []; + if (isSerie) { + seasons.sort((Map a, Map b) => + (a["season_number"] as int).compareTo((b["season_number"] as int))); + + for (var season in seasons) { + final episodesRes = await interceptAccesTokenAndGetResponse( + '$crApiUrl/cms/seasons/${season["id"]}/episodes'); + List> episodes = json.decode(episodesRes)["data"]; + episodes.sort((Map a, Map b) { + String aS = getMapValue(json.encode(a), "episode_number"); + if (aS.isEmpty) { + aS = "0"; + } + String bS = getMapValue(json.encode(b), "episode_number"); + if (bS.isEmpty) { + bS = "0"; + } + return int.parse(aS).compareTo(int.parse(bS)); + }); + for (var episode in episodes) { + MChapter ep = MChapter(); + List> urlMap = []; + List scanlator = []; + if (getMapValue(json.encode(episode), "versions").isNotEmpty) { + for (var version in episode["versions"]) { + urlMap.add({ + "media_id": version["media_guid"], + "audio": version["audio_locale"] + }); + scanlator.add(substringBefore(version["audio_locale"], "-")); + } + } else { + final mediaId = substringBefore( + substringAfter(episode["streams_link"], "videos/"), "/streams"); + scanlator.add(substringBefore(episode["audio_locale"], "-")); + urlMap.add({"media_id": mediaId, "audio": episode["audio_locale"]}); + } + + ep.url = json.encode(urlMap); + final epNumber = getMapValue(json.encode(episode), "episode_number"); + String name = ""; + if (epNumber.isNotEmpty) { + name = + "Season ${season["season_number"]} Ep $epNumber: ${episode["title"]}"; + } else { + name = episode["title"]; + } + ep.name = name; + ep.dateUpload = parseDates( + [episode["episode_air_date"]], "yyyy-MM-dd'T'HH:mm:ss", "en") + .first; + + ep.scanlator = scanlator.join(", "); + episodesList.add(ep); + } + } + } else { + for (var i = 0; i < seasons.length; i++) { + MChapter ep = MChapter(); + final movie = seasons[i]; + ep.name = "Movie ${i + 1}"; + ep.url = json.encode({"media_id": movie["id"], "audio": ""}); + ep.dateUpload = parseDates([movie["premium_available_date"]], + "yyyy-MM-dd'T'HH:mm:ss", "en") + .first; + episodesList.add(ep); + } + } + anime.chapters = episodesList.reversed.toList(); + return anime; + } + + @override + Future> getVideoList(String url) async { + List> jsonList = json.decode(url); + if (jsonList.isEmpty) throw "Episode List is empty"; + List videos = []; + List subtitles = []; + for (var v in jsonList) { + final mediaId = v["media_id"]; + String audio = v["audio"]; + + final res = await interceptAccesTokenAndGetResponse( + '$crUrl/cms/v2{0}/videos/$mediaId/streams?Policy={1}&Signature={2}&Key-Pair-Id={3}'); + + for (var ok + in (json.decode(res)["subtitles"] as Map).entries) { + try { + MTrack subtitle = MTrack(); + subtitle + ..label = getLocale(ok.value["locale"]) + ..file = ok.value["url"]; + subtitles.add(subtitle); + } catch (_) {} + } + if (audio.isEmpty) { + audio = getMapValue(res, "audio_locale"); + if (audio.isEmpty) { + audio = "ja-JP"; + } + } + audio = getLocale(audio); + for (var ok in (json.decode(res)["streams"]["adaptive_hls"] + as Map) + .entries) { + final url = ok.value["url"]; + + String hardsub = getMapValue(json.encode(ok.value), "hardsub_locale"); + if (hardsub.isNotEmpty) { + hardsub = " - HardSub: $hardsub"; + } + + final res = await client.get(Uri.parse(url)); + if (res.statusCode == 200) { + for (var it in substringAfter(res.body, "#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"); + + MVideo video = MVideo(); + video + ..url = videoUrl + ..originalUrl = videoUrl + ..quality = "$quality - Aud: $audio $hardsub" + ..subtitles = sortSubs(subtitles); + videos.add(video); + } + } + } + } + return sortVideos(videos); + } + + List sortSubs(List subs) { + String lang = getPreferenceValue(source.id, "preferred_subLang"); + + subs.sort((MTrack a, MTrack b) { + int langMatchA = 0; + if (a.label.contains(getLocale(lang))) { + langMatchA = 1; + } + int langMatchB = 0; + if (b.label.contains(getLocale(lang))) { + langMatchB = 1; + } + return langMatchB - langMatchA; + }); + return subs; + } + + Future animeFromRes(String res, String page) async { + int position = int.tryParse(page) ?? 0; + bool hasNextPage = position + 36 < json.decode(res)["total"]; + List> dataListJson = json.decode(res)["data"]; + List animeList = []; + for (var data in dataListJson) { + MManga anime = MManga(); + final type = data["type"]; + final title = data["title"]; + if (type == "series") { + final res = getMapValue( + json.encode(data["series_metadata"]), "tenant_categories", + encode: true); + if (res.isNotEmpty) { + anime.genre = json.decode(res); + } + } else { + final res = getMapValue( + json.encode(data["movie_metadata"]), "tenant_categories", + encode: true); + if (res.isNotEmpty) { + anime.genre = json.decode(res); + anime.status = MStatus.completed; + } + } + String description = data["description"]; + String metadata = type == "series" ? "series_metadata" : "movie_metadata"; + description += "\n\nLanguage:"; + if (data[metadata]["is_subbed"]) { + description += " Sub"; + } + if (data[metadata]["is_dubbed"]) { + description += " Dub"; + } + description += "\nMaturity Ratings: "; + description += (data[metadata]["maturity_ratings"] as List).join(", "); + description += "\n\nAudio: "; + description += (data[metadata]["audio_locales"] as List) + .map((e) => getLocale(e)) + .toList() + .toSet() + .toList() + .join(", "); + description += "\n\nSubs: "; + description += (data[metadata]["subtitle_locales"] as List) + .map((e) => getLocale(e)) + .toList() + .toSet() + .toList() + .join(", "); + anime.description = description; + anime.name = title; + anime.imageUrl = ((data["images"]["poster_tall"][0] as List).last + as Map)["source"]; + anime.link = json.encode({"id": data["id"], "type": type}); + animeList.add(anime); + } + return MPages(animeList, hasNextPage); + } + + Future interceptAccesTokenAndGetResponse(String url) async { + final accessToken = await getAccessToken(false); + final res = await checkUrlForNewRequest(url, accessToken); + Response response = + await client.get(Uri.parse(res["url"]), headers: res["headers"]); + if (response.statusCode == 401) { + Map res; + final newAccessToken = await getAccessToken(false); + if (accessToken != newAccessToken) { + res = await checkUrlForNewRequest(url, newAccessToken); + } + final refreshedToken = await getAccessToken(true); + + res = await checkUrlForNewRequest(url, refreshedToken); + Response response = + await client.get(Uri.parse(res["url"]), headers: res["headers"]); + return response.body; + } else { + return response.body; + } + } + + Future> getAccessToken(bool force) async { + String token = getPrefStringValue(source.id, "access_token", ""); + if (!force && token.isNotEmpty) { + return json.decode(token); + } else { + final token = await refreshAccessToken(); + setPrefStringValue(source.id, "access_token", token); + return json.decode(token); + } + } + + Future> checkUrlForNewRequest( + String url, Map tokenData) async { + if (url.contains("/cms/v2")) { + url = url + .replaceAll("{0}", tokenData["bucket"]) + .replaceAll("{1}", tokenData["policy"]) + .replaceAll("{2}", tokenData["signature"]) + .replaceAll("{3}", tokenData["key_pair_id"]); + } + return ({ + "url": url, + "headers": { + "authorization": + "${tokenData["token_type"]} ${tokenData["access_token"]}" + } + }); + } + + Future refreshAccessToken() async { + setPrefStringValue(source.id, "access_token", ""); + Response response = await client.get(Uri.parse( + "https://raw.githubusercontent.com/Samfun75/File-host/main/aniyomi/refreshToken.txt")); + final refreshToken = response.body.replaceAll(RegExp(r'[\n\r]'), ''); + Response tokenResponse = await client.post( + Uri.parse("$crUrl/auth/v1/token"), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': + 'Basic b2VkYXJteHN0bGgxanZhd2ltbnE6OWxFaHZIWkpEMzJqdVY1ZFc5Vk9TNTdkb3BkSnBnbzE=', + }, + body: { + 'grant_type': 'refresh_token', + 'refresh_token': refreshToken, + 'scope': 'offline_access', + }, + ); + final tokenJson = json.decode(tokenResponse.body); + + final res = await checkUrlForNewRequest("$crUrl/index/v2", { + "access_token": tokenJson["access_token"], + "token_type": tokenJson["token_type"] + }); + + final policyJson = json.decode( + (await client.get(Uri.parse(res["url"]), headers: res["headers"])) + .body); + + return json.encode({ + "access_token": tokenJson["access_token"], + "token_type": tokenJson["token_type"], + "policy": policyJson['cms']['policy'], + "signature": policyJson['cms']['signature'], + "key_pair_id": policyJson['cms']['key_pair_id'], + "bucket": policyJson['cms']['bucket'], + "expires": DateTime.parse("2024-02-02T14:06:52Z").microsecondsSinceEpoch, + }); + } + + List sortVideos(List videos) { + String quality = getPreferenceValue(source.id, "preferred_quality"); + String dub = getPreferenceValue(source.id, "preferred_audioLang"); + String sub = getPreferenceValue(source.id, "preferred_subLang"); + String subType = getPreferenceValue(source.id, "preferred_sub_type"); + videos.sort((MVideo a, MVideo b) { + int qualityMatchA = 0; + + if (a.quality.contains(quality) && + a.quality + .toLowerCase() + .contains("Aud: ${getLocale(dub)}".toLowerCase()) && + a.quality.toLowerCase().contains(subType.toLowerCase()) && + a.quality.toLowerCase().contains(sub.toLowerCase())) { + qualityMatchA = 1; + } + int qualityMatchB = 0; + if (b.quality.contains(quality) && + b.quality + .toLowerCase() + .contains("Aud: ${getLocale(dub)}".toLowerCase()) && + b.quality.toLowerCase().contains(subType.toLowerCase()) && + b.quality.toLowerCase().contains(sub.toLowerCase())) { + qualityMatchB = 1; + } + if (qualityMatchA != qualityMatchB) { + return qualityMatchB - qualityMatchA; + } + + final regex = RegExp(r'(\d+)p'); + final matchA = regex.firstMatch(a.quality); + final matchB = regex.firstMatch(b.quality); + final int qualityNumA = int.tryParse(matchA?.group(1) ?? '0') ?? 0; + final int qualityNumB = int.tryParse(matchB?.group(1) ?? '0') ?? 0; + return qualityNumB - qualityNumA; + }); + return videos; + } + + String getLocale(String key) { + return getMapValue(json.encode(locale), key); + } + + Map locale = { + "ar-ME": "Arabic", + "ar-SA": "Arabic (Saudi Arabia)", + "de-DE": "German", + "en-US": "English", + "en-IN": "English (India)", + "es-419": "Spanish (América Latina)", + "es-ES": "Spanish (España)", + "es-LA": "Spanish (América Latina)", + "fr-FR": "French", + "ja-JP": "Japanese", + "hi-IN": "Hindi", + "it-IT": "Italian", + "ko-KR": "Korean", + "pt-BR": "Português (Brasil)", + "pt-PT": "Português (Portugal)", + "pl-PL": "Polish", + "ru-RU": "Russian", + "tr-TR": "Turkish", + "uk-UK": "Ukrainian", + "he-IL": "Hebrew", + "ro-RO": "Romanian", + "sv-SE": "Swedish", + "zh-CN": "Chinese (PRC)", + "zh-HK": "Chinese (Hong Kong)", + "zh-TW": "Chinese (Taiwan)", + "ca-ES": "Català", + "id-ID": "Bahasa Indonesia", + "ms-MY": "Bahasa Melayu", + "ta-IN": "Tamil", + "te-IN": "Telugu", + "th-TH": "Thai", + "vi-VN": "Vietnamese" + }; + @override + List getFilterList() { + return [ + HeaderFilter("Search Filter (ignored if browsing)"), + SelectFilter("TypeFilter", "Type", 0, [ + SelectFilterOption("Top Results", "top_results"), + SelectFilterOption("Series", "series"), + SelectFilterOption("Movies", "movie_listing") + ]), + SeparatorFilter(), + SelectFilter("CategoryFilter", "Category", 0, [ + { + "type": "SelectOption", + "filter": {"name": "-", "value": ""} + }, + { + "type": "SelectOption", + "filter": {"name": "Action", "value": "&categories=action"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Action, Adventure", + "value": "&categories=action,adventure" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Action, Comedy", + "value": "&categories=action,comedy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Action, Drama", + "value": "&categories=action,drama" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Action, Fantasy", + "value": "&categories=action,fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Action, Historical", + "value": "&categories=action,historical" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Action, Post-Apocalyptic", + "value": "&categories=action,post-apocalyptic" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Action, Sci-Fi", + "value": "&categories=action,sci-fi" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Action, Supernatural", + "value": "&categories=action,supernatural" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Action, Thriller", + "value": "&categories=action,thriller" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Adventure", "value": "&categories=adventure"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Adventure, Fantasy", + "value": "&categories=adventure,fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Adventure, Isekai", + "value": "&categories=adventure,isekai" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Adventure, Romance", + "value": "&categories=adventure,romance" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Adventure, Sci-Fi", + "value": "&categories=adventure,sci-fi" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Adventure, Supernatural", + "value": "&categories=adventure,supernatural" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Comedy", "value": "&categories=comedy"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Comedy, Drama", + "value": "&categories=comedy,drama" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Comedy, Fantasy", + "value": "&categories=comedy,fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Comedy, Historical", + "value": "&categories=comedy,historical" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Comedy, Music", + "value": "&categories=comedy,music" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Comedy, Romance", + "value": "&categories=comedy,romance" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Comedy, Sci-Fi", + "value": "&categories=comedy,sci-fi" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Comedy, Slice of life", + "value": "&categories=comedy,slice+of+life" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Comedy, Supernatural", + "value": "&categories=comedy,supernatural" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Drama", "value": "&categories=drama"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Drama, Adventure", + "value": "&categories=drama,adventure" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Drama, Fantasy", + "value": "&categories=drama,fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Drama, Historical", + "value": "&categories=drama,historical" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Drama, Mecha", "value": "&categories=drama,mecha"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Drama, Mystery", + "value": "&categories=drama,mystery" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Drama, Romance", + "value": "&categories=drama,romance" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Drama, Sci-Fi", + "value": "&categories=drama,sci-fi" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Drama, Slice of life", + "value": "&categories=drama,slice+of+life" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Fantasy", "value": "&categories=fantasy"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Fantasy, Historical", + "value": "&categories=fantasy,historical" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Fantasy, Isekai", + "value": "&categories=fantasy,isekai" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Fantasy, Mystery", + "value": "&categories=fantasy,mystery" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Fantasy, Romance", + "value": "&categories=fantasy,romance" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Fantasy, Supernatural", + "value": "&categories=fantasy,supernatural" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Music", "value": "&categories=music"} + }, + { + "type": "SelectOption", + "filter": {"name": "Music, Drama", "value": "&categories=music,drama"} + }, + { + "type": "SelectOption", + "filter": {"name": "Music, Idols", "value": "&categories=music,idols"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Music, slice of life", + "value": "&categories=music,slice+of+life" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Romance", "value": "&categories=romance"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Romance, Harem", + "value": "&categories=romance,harem" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Romance, Historical", + "value": "&categories=romance,historical" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Sci-Fi", "value": "&categories=sci-fi"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Sci-Fi, Fantasy", + "value": "&categories=sci-fi,Fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Sci-Fi, Historical", + "value": "&categories=sci-fi,historical" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Sci-Fi, Mecha", + "value": "&categories=sci-fi,mecha" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Seinen", "value": "&categories=seinen"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Seinen, Action", + "value": "&categories=seinen,action" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Seinen, Drama", + "value": "&categories=seinen,drama" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Seinen, Fantasy", + "value": "&categories=seinen,fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Seinen, Historical", + "value": "&categories=seinen,historical" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Seinen, Supernatural", + "value": "&categories=seinen,supernatural" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Shojo", "value": "&categories=shojo"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Shojo, Fantasy", + "value": "&categories=shojo,Fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shojo, Magical Girls", + "value": "&categories=shojo,magical-girls" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shojo, Romance", + "value": "&categories=shojo,romance" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shojo, Slice of life", + "value": "&categories=shojo,slice+of+life" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Shonen", "value": "&categories=shonen"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Shonen, Action", + "value": "&categories=shonen,action" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shonen, Adventure", + "value": "&categories=shonen,adventure" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shonen, Comedy", + "value": "&categories=shonen,comedy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shonen, Drama", + "value": "&categories=shonen,drama" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shonen, Fantasy", + "value": "&categories=shonen,fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shonen, Mystery", + "value": "&categories=shonen,mystery" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shonen, Post-Apocalyptic", + "value": "&categories=shonen,post-apocalyptic" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Shonen, Supernatural", + "value": "&categories=shonen,supernatural" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Slice of life", + "value": "&categories=slice+of+life" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Slice of life, Fantasy", + "value": "&categories=slice+of+life,fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Slice of life, Romance", + "value": "&categories=slice+of+life,romance" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Slice of life, Sci-Fi", + "value": "&categories=slice+of+life,sci-fi" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Sports", "value": "&categories=sports"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Sports, Action", + "value": "&categories=sports,action" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Sports, Comedy", + "value": "&categories=sports,comedy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Sports, Drama", + "value": "&categories=sports,drama" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Supernatural", + "value": "&categories=supernatural" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Supernatural, Drama", + "value": "&categories=supernatural,drama" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Supernatural, Historical", + "value": "&categories=supernatural,historical" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Supernatural, Mystery", + "value": "&categories=supernatural,mystery" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Supernatural, Slice of life", + "value": "&categories=supernatural,slice+of+life" + } + }, + { + "type": "SelectOption", + "filter": {"name": "Thriller", "value": "&categories=thriller"} + }, + { + "type": "SelectOption", + "filter": { + "name": "Thriller, Drama", + "value": "&categories=thriller,drama" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Thriller, Fantasy", + "value": "&categories=thriller,fantasy" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Thriller, Sci-Fi", + "value": "&categories=thriller,sci-fi" + } + }, + { + "type": "SelectOption", + "filter": { + "name": "Thriller, Supernatural", + "value": "&categories=thriller,supernatural" + } + } + ]), + HeaderFilter("Browse Filters (ignored if searching)"), + SelectFilter("SortFilter", "Sort By", 0, [ + SelectFilterOption("Popular", "popularity"), + SelectFilterOption("New", "newly_added"), + SelectFilterOption("Alphabetical", "alphabetical") + ]), + SelectFilter("MediaFilter", "Media", 0, [ + SelectFilterOption("All", ""), + SelectFilterOption("Series", "&type=series"), + SelectFilterOption("Movies", "&type=movie_listing"), + ]), + GroupFilter("LanguageFilter", "Language", [ + CheckBoxFilter("Sub", "&is_subbed=true"), + CheckBoxFilter("Dub", "&is_dubbed=true") + ]), + ]; + } + + @override + List getSourcePreferences() { + return [ + ListPreference( + key: "preferred_quality", + title: "Preferred Quality", + summary: "", + valueIndex: 0, + entries: ["1080p", "720p", "480p", "360p", "240p", "80p"], + entryValues: ["1080p", "720p", "480p", "360p", "240p", "80p"]), + ListPreference( + key: "preferred_audioLang", + title: "Preferred Audio Language", + summary: "", + valueIndex: 3, + entries: locale.entries.map((e) => e.value).toList(), + entryValues: locale.entries.map((e) => e.key).toList()), + ListPreference( + key: "preferred_subLang", + title: "Preferred Sub language", + summary: "", + valueIndex: 3, + entries: locale.entries.map((e) => e.value).toList(), + entryValues: locale.entries.map((e) => e.key).toList()), + ListPreference( + key: "preferred_sub_type", + title: "Preferred Sub Type", + summary: "", + valueIndex: 0, + entries: ["Softsub", "Hardsub"], + entryValues: ["", "Hardsub"]), + ]; + } +} + +YomiRoll main(MSource source) { + return YomiRoll(source: source); +}