diff --git a/anime/source_generator.dart b/anime/source_generator.dart index 7ba2e412..7caa06d6 100644 --- a/anime/source_generator.dart +++ b/anime/source_generator.dart @@ -5,6 +5,7 @@ import '../model/source.dart'; import 'multisrc/datalifeengine/sources.dart'; import 'multisrc/dopeflix/sources.dart'; import 'multisrc/zorotheme/sources.dart'; +import 'src/all/animeworldindia/sources.dart'; import 'src/ar/okanime/source.dart'; import 'src/de/aniflix/source.dart'; import 'src/en/aniwave/source.dart'; @@ -46,7 +47,8 @@ void main() { yomoviesSource, animesamaSource, nineanimetv, - aniflix + aniflix, + ...animeworldindiaSourcesList ]; final List> jsonList = _sourcesList.map((source) => source.toJson()).toList(); diff --git a/anime/src/all/animeworldindia/animeworldindia.dart b/anime/src/all/animeworldindia/animeworldindia.dart new file mode 100644 index 00000000..fd532d31 --- /dev/null +++ b/anime/src/all/animeworldindia/animeworldindia.dart @@ -0,0 +1,411 @@ +import 'package:mangayomi/bridge_lib.dart'; +import 'dart:convert'; + +class AnimeWorldIndia extends MProvider { + AnimeWorldIndia(); + + @override + Future getPopular(MSource source, int page) async { + final data = { + "url": + "${source.baseUrl}/advanced-search/page/$page/?s_lang=${source.lang}&s_orderby=viewed" + }; + + final res = await http('GET', json.encode(data)); + return parseAnimeList(res, source.baseUrl); + } + + @override + Future getLatestUpdates(MSource source, int page) async { + final data = { + "url": + "${source.baseUrl}/advanced-search/page/$page/?s_lang=${source.lang}&s_orderby=update" + }; + final res = await http('GET', json.encode(data)); + return parseAnimeList(res, source.baseUrl); + } + + @override + Future search( + MSource source, String query, int page, FilterList filterList) async { + final filters = filterList.filters; + String url = + "${source.baseUrl}/advanced-search/page/$page/?s_keyword=$query&s_lang=${source.lang}"; + for (var filter in filters) { + if (filter.type == "TypeFilter") { + final type = filter.values[filter.state].value; + url += "${ll(url)}s_type=$type"; + } else if (filter.type == "StatusFilter") { + final status = filter.values[filter.state].value; + url += "${ll(url)}s_status=$status"; + } else if (filter.type == "StyleFilter") { + final style = filter.values[filter.state].value; + url += "${ll(url)}s_sub_type=$style"; + } else if (filter.type == "YearFilter") { + final year = filter.values[filter.state].value; + url += "${ll(url)}s_year=$year"; + } else if (filter.type == "SortFilter") { + final sort = filter.values[filter.state].value; + url += "${ll(url)}s_orderby=$sort"; + } else if (filter.type == "GenresFilter") { + final genre = (filter.state as List).where((e) => e.state).toList(); + url += "${ll(url)}s_genre="; + if (genre.isNotEmpty) { + for (var st in genre) { + url += "${st.value}".toLowerCase().replaceAll(" ", "-"); + if (genre.length > 1) { + url += "%2C"; + } + } + if (genre.length > 1) { + url = substringBeforeLast(url, '%2C'); + } + } + } + } + final data = {"url": url}; + final res = await http('GET', json.encode(data)); + return parseAnimeList(res, source.baseUrl); + } + + @override + Future getDetail(MSource source, String url) async { + final data = {"url": url}; + final res = await http('GET', json.encode(data)); + MManga anime = MManga(); + final document = parseHtml(res); + final isMovie = + document.xpath('//li/a[contains(text(),"Movie")]/text()').isNotEmpty; + if (isMovie) { + anime.status = MStatus.completed; + } else { + final eps = xpath( + res, '//ul/li/a[contains(@href,"${source.baseUrl}/watch")]/text()'); + if (eps.isNotEmpty) { + final epParts = eps.first + .substring(3) + .replaceAll(" ", "") + .replaceAll("\n", "") + .split('/'); + if (epParts.length == 2) { + if (epParts[0].compareTo(epParts[1]) == 0) { + anime.status = MStatus.completed; + } else { + anime.status = MStatus.ongoing; + } + } + } + } + anime.description = document.selectFirst("div[data-synopsis]")?.text ?? ""; + anime.author = document + .xpath('//li[contains(text(),"Producers:")]/span/a/text()') + .join(', '); + anime.genre = document.xpath( + '//span[@class="leading-6"]/a[contains(@class,"border-opacity-30")]/text()'); + final seasonsJson = json.decode(substringBeforeLast( + substringBefore( + substringAfter(res, "var season_list = "), "var season_label ="), + ";")) as List>; + bool isSingleSeason = seasonsJson.length == 1; + List? episodesList = []; + for (var i = 0; i < seasonsJson.length; i++) { + final seasonJson = seasonsJson[i]; + final seasonName = isSingleSeason ? "" : "Season ${i + 1}"; + final episodesJson = + (seasonJson["episodes"]["all"] as List>) + .reversed + .toList(); + for (var j = 0; j < episodesJson.length; j++) { + final episodeJson = episodesJson[j]; + final episodeTitle = episodeJson["metadata"]["title"] ?? ""; + String episodeName = ""; + if (isMovie) { + episodeName = "Movie"; + } else { + if (seasonName.isNotEmpty) { + episodeName = "$seasonName - "; + } + episodeName += "Episode ${j + 1} "; + if (episodeTitle.isNotEmpty) { + episodeName += "- $episodeTitle"; + } + } + MChapter episode = MChapter(); + episode.name = episodeName; + + episode.dateUpload = + "${int.parse(episodeJson["metadata"]["released"] ?? "0") * 1000}"; + episode.url = "/wp-json/kiranime/v1/episode?id=${episodeJson["id"]}"; + 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": "${source.baseUrl}$url"})); + + var resJson = substringBefore( + substringAfterLast(res, "\"players\":"), ",\"noplayer\":"); + var streams = (json.decode(resJson) as List>) + .where((e) => e["type"] == "stream" + ? true + : false && (e["url"] as String).isNotEmpty) + .toList() + .where((e) => language(source.lang).isEmpty || + language(source.lang) == e["language"] + ? true + : false) + .toList(); + List videos = []; + for (var stream in streams) { + String videoUrl = stream["url"]; + final language = stream["language"]; + final video = await mystreamExtractor(videoUrl, language); + videos.addAll(video); + } + + return sortVideos(videos, source.id); + } + + String getUrlWithoutDomain(String orig) { + final uri = Uri.parse(orig.replaceAll(' ', '%20')); + String out = uri.path; + if (uri.query.isNotEmpty) { + out += '?${uri.query}'; + } + if (uri.fragment.isNotEmpty) { + out += '#${uri.fragment}'; + } + return out; + } + + MPages parseAnimeList(String res, String baseUrl) { + List animeList = []; + final document = parseHtml(res); + + for (var element in document.select("div.col-span-1")) { + MManga anime = MManga(); + anime.name = + element.selectFirst("div.font-medium.line-clamp-2.mb-3").text; + anime.link = element.selectFirst("a").getHref; + anime.imageUrl = + "$baseUrl${getUrlWithoutDomain(element.selectFirst("img").getSrc)}"; + animeList.add(anime); + } + final hasNextPage = xpath(res, + '//li/span[@class="page-numbers current"]/parent::li//following-sibling::li/a/@href') + .isNotEmpty; + return MPages(animeList, hasNextPage); + } + + String language(String lang) { + final languages = { + "all": "", + "bn": "bengali", + "en": "english", + "hi": "hindi", + "ja": "japanese", + "ml": "malayalam", + "mr": "marathi", + "ta": "tamil", + "te": "telugu" + }; + return languages[lang] ?? ""; + } + + Future> mystreamExtractor(String url, String language) async { + List videos = []; + + final res = await http('GET', json.encode({"url": url})); + final streamCode = substringBefore( + substringAfter(substringAfter(res, "sniff("), ", \""), '"'); + + final streamUrl = + "${substringBefore(url, "/watch")}/m3u8/$streamCode/master.txt?s=1&cache=1"; + final masterPlaylistRes = + await http('GET', json.encode({"url": streamUrl})); + List audios = []; + for (var it in substringAfter(masterPlaylistRes, "#EXT-X-MEDIA:TYPE=AUDIO") + .split("#EXT-X-MEDIA:TYPE=AUDIO")) { + final line = + substringBefore(substringAfter(it, "#EXT-X-MEDIA:TYPE=AUDIO"), "\n"); + final audioUrl = substringBefore(substringAfter(line, "URI=\""), "\""); + MTrack audio = MTrack(); + audio + ..label = substringBefore(substringAfter(line, "NAME=\""), "\"") + ..file = audioUrl; + audios.add(audio); + } + + 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"); + + MVideo video = MVideo(); + video + ..url = videoUrl + ..originalUrl = videoUrl + ..quality = "[$language] MyStream - $quality" + ..audios = audios; + videos.add(video); + } + return videos; + } + + @override + List getFilterList(MSource source) { + return [ + SelectFilter("TypeFilter", "Type", 0, [ + SelectFilterOption("Any", "all"), + SelectFilterOption("TV", "tv"), + SelectFilterOption("Movie", "movies"), + ]), + SelectFilter("StatusFilter", "Status", 0, [ + SelectFilterOption("Any", "all"), + SelectFilterOption("Currently Airing", "airing"), + SelectFilterOption("Finished Airing", "completed"), + ]), + SelectFilter("StyleFilter", "Style", 0, [ + SelectFilterOption("Any", "all"), + SelectFilterOption("Anime", "anime"), + SelectFilterOption("Cartoon", "cartoon"), + ]), + SelectFilter("YearFilter", "Year", 0, [ + SelectFilterOption("Any", "all"), + SelectFilterOption("2024", "2024"), + SelectFilterOption("2023", "2023"), + SelectFilterOption("2022", "2022"), + SelectFilterOption("2021", "2021"), + SelectFilterOption("2020", "2020"), + SelectFilterOption("2019", "2019"), + SelectFilterOption("2018", "2018"), + SelectFilterOption("2017", "2017"), + SelectFilterOption("2016", "2016"), + SelectFilterOption("2015", "2015"), + SelectFilterOption("2014", "2014"), + SelectFilterOption("2013", "2013"), + SelectFilterOption("2012", "2012"), + SelectFilterOption("2011", "2011"), + SelectFilterOption("2010", "2010"), + SelectFilterOption("2009", "2009"), + SelectFilterOption("2008", "2008"), + SelectFilterOption("2007", "2007"), + SelectFilterOption("2006", "2006"), + SelectFilterOption("2005", "2005"), + SelectFilterOption("2004", "2004"), + SelectFilterOption("2003", "2003"), + SelectFilterOption("2002", "2002"), + SelectFilterOption("2001", "2001"), + SelectFilterOption("2000", "2000"), + SelectFilterOption("1999", "1999"), + SelectFilterOption("1998", "1998"), + SelectFilterOption("1997", "1997"), + SelectFilterOption("1996", "1996"), + SelectFilterOption("1995", "1995"), + SelectFilterOption("1994", "1994"), + SelectFilterOption("1993", "1993"), + SelectFilterOption("1992", "1992"), + SelectFilterOption("1991", "1991"), + SelectFilterOption("1990", "1990") + ]), + SelectFilter("SortFilter", "Sort", 0, [ + SelectFilterOption("Default", "default"), + SelectFilterOption("Ascending", "title_a_z"), + SelectFilterOption("Descending", "title_z_a"), + SelectFilterOption("Updated", "update"), + SelectFilterOption("Published", "date"), + SelectFilterOption("Most Viewed", "viewed"), + SelectFilterOption("Favourite", "favorite"), + ]), + GroupFilter("GenresFilter", "Genres", [ + CheckBoxFilter("Action", "Action"), + CheckBoxFilter("Adult Cast", "Adult Cast"), + CheckBoxFilter("Adventure", "Adventure"), + CheckBoxFilter("Animation", "Animation"), + CheckBoxFilter("Comedy", "Comedy"), + CheckBoxFilter("Detective", "Detective"), + CheckBoxFilter("Drama", "Drama"), + CheckBoxFilter("Ecchi", "Ecchi"), + CheckBoxFilter("Family", "Family"), + CheckBoxFilter("Fantasy", "Fantasy"), + CheckBoxFilter("Isekai", "Isekai"), + CheckBoxFilter("Kids", "Kids"), + CheckBoxFilter("Martial Arts", "Martial Arts"), + CheckBoxFilter("Mecha", "Mecha"), + CheckBoxFilter("Military", "Military"), + CheckBoxFilter("Mystery", "Mystery"), + CheckBoxFilter("Otaku Culture", "Otaku Culture"), + CheckBoxFilter("Reality", "Reality"), + CheckBoxFilter("Romance", "Romance"), + CheckBoxFilter("School", "School"), + CheckBoxFilter("Sci-Fi", "Sci-Fi"), + CheckBoxFilter("Seinen", "Seinen"), + CheckBoxFilter("Shounen", "Shounen"), + CheckBoxFilter("Slice of Life", "Slice of Life"), + CheckBoxFilter("Sports", "Sports"), + CheckBoxFilter("Super Power", "Super Power"), + CheckBoxFilter("SuperHero", "SuperHero"), + CheckBoxFilter("Supernatural", "Supernatural"), + CheckBoxFilter("TV Movie", "TV Movie"), + ]), + ]; + } + + @override + List getSourcePreferences(MSource source) { + return [ + ListPreference( + key: "preferred_quality", + title: "Preferred Quality", + summary: "", + valueIndex: 0, + entries: ["1080p", "720p", "480p", "360p", "240p"], + entryValues: ["1080", "720", "480", "360", "240"]), + ]; + } + + 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; + } + + String ll(String url) { + if (url.contains("?")) { + return "&"; + } + return "?"; + } +} + +AnimeWorldIndia main() { + return AnimeWorldIndia(); +} diff --git a/anime/src/all/animeworldindia/icon.png b/anime/src/all/animeworldindia/icon.png new file mode 100644 index 00000000..0426e29d Binary files /dev/null and b/anime/src/all/animeworldindia/icon.png differ diff --git a/anime/src/all/animeworldindia/sources.dart b/anime/src/all/animeworldindia/sources.dart new file mode 100644 index 00000000..6890c3d6 --- /dev/null +++ b/anime/src/all/animeworldindia/sources.dart @@ -0,0 +1,32 @@ +import '../../../../model/source.dart'; + +const _animeworldindiaVersion = "0.0.1"; +const _animeworldindiaSourceCodeUrl = + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/anime/src/all/animeworldindia/animeworldindia.dart"; + +String _iconUrl = + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/anime/src/all/animeworldindia/icon.png"; + +List _languages = [ + "all", + "en", + "bn", + "hi", + "ja", + "ml", + "mr", + "ta", + "te", +]; + +List get animeworldindiaSourcesList => _animeworldindiaSourcesList; +List _animeworldindiaSourcesList = _languages + .map((e) => Source( + name: 'AnimeWorld India', + baseUrl: "https://anime-world.in", + lang: e, + typeSource: "multiple", + iconUrl: _iconUrl, + version: _animeworldindiaVersion, + sourceCodeUrl: _animeworldindiaSourceCodeUrl)) + .toList();