import 'package:mangayomi/bridge_lib.dart'; import 'dart:convert'; class DopeFlix extends MProvider { DopeFlix({required this.source}); MSource source; final Client client = Client(source); @override String get baseUrl => getPreferenceValue(source.id, "preferred_domain"); @override Future getPopular(int page) async { final res = (await client.get(Uri.parse( "$baseUrl/${getPreferenceValue(source.id, "preferred_popular_page")}?page=$page"))) .body; return parseAnimeList(res); } @override Future getLatestUpdates(int page) async { final res = (await client.get(Uri.parse("$baseUrl/home"))).body; List animeList = []; final path = '//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'); 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); } return MPages(animeList, false); } @override Future search(String query, int page, FilterList filterList) async { final filters = filterList.filters; String url = "$baseUrl"; if (query.isNotEmpty) { url += "/search/${query.replaceAll(" ", "-")}?page=$page"; } else { url += "/filter/?page=$page"; for (var filter in filters) { if (filter.type == "TypeFilter") { final type = filter.values[filter.state].value; url += "${ll(url)}type=$type"; } else if (filter.type == "QualityFilter") { final quality = filter.values[filter.state].value; url += "${ll(url)}quality=$quality"; } else if (filter.type == "ReleaseYearFilter") { final year = filter.values[filter.state].value; url += "${ll(url)}release_year=$year"; } else if (filter.type == "GenresFilter") { final genre = (filter.state as List).where((e) => e.state).toList(); if (genre.isNotEmpty) { url += "${ll(url)}genre="; for (var st in genre) { url += "${st.value}-"; } } } else if (filter.type == "CountriesFilter") { final country = (filter.state as List).where((e) => e.state).toList(); if (country.isNotEmpty) { url += "${ll(url)}country="; for (var st in country) { url += "${st.value}-"; } } } } } final res = (await client.get(Uri.parse(url))).body; return parseAnimeList(res); } @override Future getDetail(String url) async { url = getUrlWithoutDomain(url); final res = (await client.get(Uri.parse("$baseUrl$url"))).body; MManga anime = MManga(); final description = xpath(res, '//div[@class="description"]/text()'); if (description.isNotEmpty) { anime.description = description.first.replaceAll("Overview:", ""); } final author = xpath(res, '//div[contains(text(),"Production")]/a/text()'); if (author.isNotEmpty) { anime.author = author.first; } anime.genre = xpath(res, '//div[contains(text(),"Genre")]/a/text()'); List episodesList = []; final id = xpath(res, '//div[@class="detail_page-watch"]/@data-id').first; final dataType = xpath(res, '//div[@class="detail_page-watch"]/@data-type').first; if (dataType == "1") { MChapter episode = MChapter(); episode.name = "Movie"; episode.url = "$baseUrl/ajax/movie/episodes/$id"; episodesList.add(episode); } else { final resS = (await client.get(Uri.parse("$baseUrl/ajax/v2/tv/seasons/$id"))).body; final seasonIds = xpath(resS, '//a[@class="dropdown-item ss-item"]/@data-id'); final seasonNames = xpath(resS, '//a[@class="dropdown-item ss-item"]/text()'); for (int i = 0; i < seasonIds.length; i++) { final seasonId = seasonIds[i]; final seasonName = seasonNames[i]; final html = (await client .get(Uri.parse("$baseUrl/ajax/v2/season/episodes/$seasonId"))) .body; final epsHtmls = parseHtml(html).select("div.eps-item"); for (var epH in epsHtmls) { final epHtml = epH.outerHtml; final episodeId = xpath(epHtml, '//div[contains(@class,"eps-item")]/@data-id') .first; final epNum = xpath(epHtml, '//div[@class="episode-number"]/text()').first; final epName = xpath(epHtml, '//h3[@class="film-name"]/text()').first; MChapter episode = MChapter(); episode.name = "$seasonName $epNum $epName"; episode.url = "$baseUrl/ajax/v2/episode/servers/$episodeId"; episodesList.add(episode); } } } anime.chapters = episodesList.reversed.toList(); return anime; } @override Future> getVideoList(String url) async { url = getUrlWithoutDomain(url); final res = (await client.get(Uri.parse("$baseUrl/$url"))).body; final vidsHtmls = parseHtml(res).select("ul.fss-list a.btn-play"); List videos = []; for (var vidH in vidsHtmls) { final vidHtml = vidH.outerHtml; final id = xpath(vidHtml, '//a/@data-id').first; final name = xpath(vidHtml, '//span/text()').first; final resSource = (await client.get(Uri.parse("$baseUrl/ajax/sources/$id"))).body; final vidUrl = substringBefore(substringAfter(resSource, "\"link\":\""), "\""); List a = []; if (name.contains("DoodStream")) { a = await doodExtractor(vidUrl, "DoodStream"); } else if (["Vidcloud", "UpCloud"].contains(name)) { final id = substringBefore(substringAfter(vidUrl, "/embed-4/"), "?"); final serverUrl = substringBefore(vidUrl, "/embed"); final resServer = (await client.get( Uri.parse("$serverUrl/ajax/embed-4/getSources?id=$id"), headers: {"X-Requested-With": "XMLHttpRequest"})) .body; final encrypted = getMapValue(resServer, "encrypted"); String videoResJson = ""; if (encrypted == "true") { final ciphered = getMapValue(resServer, "sources"); List> indexPairs = await generateIndexPairs(); var password = ''; String ciphertext = ciphered; int index = 0; for (List item in json.decode(json.encode(indexPairs))) { int start = item.first + index; int end = start + item.last; String passSubstr = ciphered.substring(start, end); password += passSubstr; ciphertext = ciphertext.replaceFirst(passSubstr, ""); index += item.last; } videoResJson = decryptAESCryptoJS(ciphertext, password); } else { videoResJson = resServer; } String masterUrl = ((json.decode(videoResJson) as List>) .first)['file']; String type = ((json.decode(videoResJson) as List>) .first)['type']; final tracks = (json.decode(resServer)['tracks'] as List) .where((e) => e['kind'] == 'captions' ? true : false) .toList(); List subtitles = []; for (var sub in tracks) { try { MTrack subtitle = MTrack(); subtitle ..label = sub["label"] ..file = sub["file"]; subtitles.add(subtitle); } catch (_) {} } subtitles = sortSubs(subtitles, source.id); if (type == "hls") { final masterPlaylistRes = (await client.get(Uri.parse(masterUrl))).body; for (var it in substringAfter(masterPlaylistRes, "#EXT-X-STREAM-INF:") .split("#EXT-X-STREAM-INF:")) { final quality = "${substringBefore(substringBefore(substringAfter(substringAfter(it, "RESOLUTION="), "x"), ","), "\n")}p"; String videoUrl = substringBefore(substringAfter(it, "\n"), "\n"); if (!videoUrl.startsWith("http")) { videoUrl = "${masterUrl.split("/").sublist(0, masterUrl.split("/").length - 1).join("/")}/$videoUrl"; } MVideo video = MVideo(); video ..url = videoUrl ..originalUrl = videoUrl ..quality = "$name - $quality" ..subtitles = subtitles; a.add(video); } } else { MVideo video = MVideo(); video ..url = masterUrl ..originalUrl = masterUrl ..quality = "$name - Default" ..subtitles = subtitles; a.add(video); } } videos.addAll(a); } return sortVideos(videos, source.id); } Future>> generateIndexPairs() async { final res = (await client.get(Uri.parse( "https://rabbitstream.net/js/player/prod/e4-player.min.js"))) .body; String script = substringBefore(substringAfter(res, "const "), "()"); script = script.substring(0, script.lastIndexOf(',')); final list = script .split(",") .map((String e) { String value = substringAfter(e, "="); if (value.contains("0x")) { return int.parse(substringAfter(value, "0x"), radix: 16); } else { return int.parse(value); } }) .toList() .skip(1) .toList(); return chunked(list, 2) .map((List list) => list.reversed.toList()) .toList(); } List> chunked(List list, int size) { List> chunks = []; for (int i = 0; i < list.length; i += size) { int end = list.length; if (i + size < list.length) { end = i + size; } chunks.add(list.sublist(i, end)); } return chunks; } MPages parseAnimeList(String res) { List animeList = []; final path = '//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'); 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 pages = xpath( res, '//ul[contains(@class,"pagination")]/li/a[@title="Next"]/@title'); return MPages(animeList, pages.isNotEmpty); } @override List getFilterList() { return [ SelectFilter("TypeFilter", "Type", 0, [ SelectFilterOption("All", "all"), SelectFilterOption("Movies", "movies"), SelectFilterOption("TV Shows", "tv") ]), SelectFilter("QualityFilter", "Quality", 0, [ SelectFilterOption("All", "all"), SelectFilterOption("HD", "HD"), SelectFilterOption("SD", "SD"), SelectFilterOption("CAM", "CAM") ]), SelectFilter("ReleaseYearFilter", "Released at", 0, [ SelectFilterOption("All", "all"), SelectFilterOption("2023", "2023"), SelectFilterOption("2022", "2022"), SelectFilterOption("2021", "2021"), SelectFilterOption("2020", "2020"), SelectFilterOption("2019", "2019"), SelectFilterOption("2018", "2018"), SelectFilterOption("Older", "older-2018") ]), SeparatorFilter(), GroupFilter("GenresFilter", "Genre", [ CheckBoxFilter("Action", "10"), CheckBoxFilter("Action & Adventure", "24"), CheckBoxFilter("Adventure", "18"), CheckBoxFilter("Animation", "3"), CheckBoxFilter("Biography", "37"), CheckBoxFilter("Comedy", "7"), CheckBoxFilter("Crime", "2"), CheckBoxFilter("Documentary", "11"), CheckBoxFilter("Drama", "4"), CheckBoxFilter("Family", "9"), CheckBoxFilter("Fantasy", "13"), CheckBoxFilter("History", "19"), CheckBoxFilter("Horror", "14"), CheckBoxFilter("Kids", "27"), CheckBoxFilter("Music", "15"), CheckBoxFilter("Mystery", "1"), CheckBoxFilter("News", "34"), CheckBoxFilter("Reality", "22"), CheckBoxFilter("Romance", "12"), CheckBoxFilter("Sci-Fi & Fantasy", "31"), CheckBoxFilter("Science Fiction", "5"), CheckBoxFilter("Soap", "35"), CheckBoxFilter("Talk", "29"), CheckBoxFilter("Thriller", "16"), CheckBoxFilter("TV Movie", "8"), CheckBoxFilter("War", "17"), CheckBoxFilter("War & Politics", "28"), CheckBoxFilter("Western", "6") ]), GroupFilter("CountriesFilter", "Countries", [ CheckBoxFilter("Argentina", "11"), CheckBoxFilter("Australia", "151"), CheckBoxFilter("Austria", "4"), CheckBoxFilter("Belgium", "44"), CheckBoxFilter("Brazil", "190"), CheckBoxFilter("Canada", "147"), CheckBoxFilter("China", "101"), CheckBoxFilter("Czech Republic", "231"), CheckBoxFilter("Denmark", "222"), CheckBoxFilter("Finland", "158"), CheckBoxFilter("France", "3"), CheckBoxFilter("Germany", "96"), CheckBoxFilter("Hong Kong", "93"), CheckBoxFilter("Hungary", "72"), CheckBoxFilter("India", "105"), CheckBoxFilter("Ireland", "196"), CheckBoxFilter("Israel", "24"), CheckBoxFilter("Italy", "205"), CheckBoxFilter("Japan", "173"), CheckBoxFilter("Luxembourg", "91"), CheckBoxFilter("Mexico", "40"), CheckBoxFilter("Netherlands", "172"), CheckBoxFilter("New Zealand", "122"), CheckBoxFilter("Norway", "219"), CheckBoxFilter("Poland", "23"), CheckBoxFilter("Romania", "170"), CheckBoxFilter("Russia", "109"), CheckBoxFilter("South Africa", "200"), CheckBoxFilter("South Korea", "135"), CheckBoxFilter("Spain", "62"), CheckBoxFilter("Sweden", "114"), CheckBoxFilter("Switzerland", "41"), CheckBoxFilter("Taiwan", "119"), CheckBoxFilter("Thailand", "57"), CheckBoxFilter("United Kingdom", "180"), CheckBoxFilter("United States of America", "129") ]), ]; } @override List getSourcePreferences() { return [ 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", 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 ll(String url) { if (url.contains("?")) { return "&"; } return "?"; } } DopeFlix main(MSource source) { return DopeFlix(source: source); }