mirror of
https://github.com/kodjodevf/mangayomi-extensions.git
synced 2026-02-14 02:41:39 +00:00
548 lines
18 KiB
Dart
548 lines
18 KiB
Dart
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<MPages> 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<MPages> getLatestUpdates(int page) async {
|
|
final res = (await client.get(Uri.parse("$baseUrl/home"))).body;
|
|
List<MManga> 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<MPages> 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<MManga> 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<MChapter> 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<List<MVideo>> 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<MVideo> 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<MVideo> a = [];
|
|
String masterUrl = "";
|
|
String type = "";
|
|
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<List<int>> indexPairs = await generateIndexPairs();
|
|
|
|
var password = '';
|
|
String ciphertext = ciphered;
|
|
int index = 0;
|
|
for (List<int> 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);
|
|
masterUrl = ((json.decode(videoResJson) as List<Map<String, dynamic>>)
|
|
.first)['file'];
|
|
|
|
type = ((json.decode(videoResJson) as List<Map<String, dynamic>>)
|
|
.first)['type'];
|
|
} else {
|
|
masterUrl =
|
|
((json.decode(resServer)["sources"] as List<Map<String, dynamic>>)
|
|
.first)['file'];
|
|
|
|
type =
|
|
((json.decode(resServer)["sources"] as List<Map<String, dynamic>>)
|
|
.first)['type'];
|
|
}
|
|
|
|
final tracks = (json.decode(resServer)['tracks'] as List)
|
|
.where((e) => e['kind'] == 'captions' ? true : false)
|
|
.toList();
|
|
List<MTrack> 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 as String).split("/").sublist(0, (masterUrl as String).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<List<List<int>>> 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<int> list) => list.reversed.toList())
|
|
.toList();
|
|
}
|
|
|
|
List<List<int>> chunked(List<int> list, int size) {
|
|
List<List<int>> 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<MManga> 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<dynamic> 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<dynamic> 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<MVideo> sortVideos(List<MVideo> 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<MTrack> sortSubs(List<MTrack> 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);
|
|
}
|