This commit is contained in:
Moustapha Kodjo Amadou
2025-10-01 15:27:46 +01:00
parent a19781234a
commit 6004f1f8d1
129 changed files with 2 additions and 26321 deletions

View File

@@ -1,714 +0,0 @@
const mangayomiSources = [
{
"name": "Autoembed",
"lang": "all",
"baseUrl": "https://watch.autoembed.cc",
"apiUrl": "https://tom.autoembed.cc",
"iconUrl":
"https://www.google.com/s2/favicons?sz=64&domain=https://autoembed.cc/",
"typeSource": "multi",
"isManga": false,
"itemType": 1,
"version": "1.2.7",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/all/autoembed.js"
}
];
class DefaultExtension extends MProvider {
decodeBase64 = function (f) {
var g = {},
b = 65,
d = 0,
a,
c = 0,
h,
e = "",
k = String.fromCharCode,
l = f.length;
for (a = ""; 91 > b; ) a += k(b++);
a += a.toLowerCase() + "0123456789+/";
for (b = 0; 64 > b; b++) g[a.charAt(b)] = b;
for (a = 0; a < l; a++)
for (b = g[f.charAt(a)], d = (d << 6) + b, c += 6; 8 <= c; )
((h = (d >>> (c -= 8)) & 255) || a < l - 2) && (e += k(h));
return e;
};
getHeaders(url) {
return {
Referer: url,
Origin: url,
};
}
getPreference(key) {
const preferences = new SharedPreferences();
return preferences.get(key);
}
async tmdbRequest(slug) {
var api = `https://94c8cb9f702d-tmdb-addon.baby-beamup.club/${slug}`;
var response = await new Client().get(api);
var body = JSON.parse(response.body);
return body;
}
async getSearchItems(body) {
var items = [];
var results = body.metas;
for (let i in results) {
var result = results[i];
var id = result.id;
var media_type = result.type;
items.push({
name: result.name,
imageUrl: result.poster,
link: `${media_type}||${id}`,
description: result.description,
genre: result.genre,
});
}
return items;
}
async getSearchInfo(slug) {
var body = await this.tmdbRequest(`catalog/movie/${slug}`);
var popMovie = await this.getSearchItems(body);
body = await this.tmdbRequest(`catalog/series/${slug}`);
var popSeries = await this.getSearchItems(body);
var fullList = [];
var priority = this.getPreference("pref_content_priority");
if (priority === "series") {
fullList = [...popSeries, ...popMovie];
} else {
fullList = [...popMovie, ...popSeries];
}
var hasNextPage = slug.indexOf("search=") > -1 ? false : true;
return {
list: fullList,
hasNextPage,
};
}
async getPopular(page) {
var skip = (page - 1) * 20;
return await this.getSearchInfo(`tmdb.popular/skip=${skip}.json`);
}
get supportsLatest() {
throw new Error("supportsLatest not implemented");
}
async getLatestUpdates(page) {
var trend_window = this.getPreference("pref_latest_time_window");
var skip = (page - 1) * 20;
return await this.getSearchInfo(
`tmdb.trending/genre=${trend_window}&skip=${skip}.json`
);
}
async search(query, page, filters) {
return await this.getSearchInfo(`tmdb.popular/search=${query}.json`);
}
async getDetail(url) {
var baseUrl = this.source.baseUrl;
var linkSlug = `${baseUrl}/title/`;
if (url.includes(linkSlug)) {
url = url.replace(linkSlug, "");
var id = url.replace("t", "");
if (url.includes("t")) {
url = `series||tmdb:${id}`;
} else {
url = `movie||tmdb:${id}`;
}
}
var parts = url.split("||");
var media_type = parts[0];
var id = parts[1];
var body = await this.tmdbRequest(`meta/${media_type}/${id}.json`);
var result = body.meta;
var tmdb_id = id.substring(5);
media_type = media_type == "series" ? "tv" : media_type;
var dateNow = Date.now().valueOf();
var release = result.released
? new Date(result.released).valueOf()
: dateNow;
var chaps = [];
var item = {
name: result.name,
imageUrl: result.poster,
link: `${linkSlug}${linkCode}`,
description: result.description,
genre: result.genre,
};
var link = `${media_type}||${tmdb_id}`;
if (media_type == "tv") {
var videos = result.videos;
for (var i in videos) {
var video = videos[i];
var seasonNum = video.season;
if (!seasonNum) continue;
release = video.released ? new Date(video.released).valueOf() : dateNow;
if (release < dateNow) {
var episodeNum = video.episode;
var name = `S${seasonNum}:E${episodeNum} - ${video.name}`;
var eplink = `${link}||${seasonNum}||${episodeNum}`;
chaps.push({
name: name,
url: eplink,
dateUpload: release.toString(),
});
}
}
} else {
if (release < dateNow) {
chaps.push({
name: "Movie",
url: link,
dateUpload: release.toString(),
});
}
}
item.chapters = chaps;
chaps.reverse();
return item;
}
// Extracts the streams url for different resolutions from a hls stream.
async extractStreams(url, lang = "", hdr = {}, host = "") {
var streams = [
{
url: url,
originalUrl: url,
quality: `${lang} Auto`,
headers: hdr,
},
];
var pref = this.getPreference("autoembed_split_stream_quality");
if (!pref) return streams;
const response = await new Client().get(url, hdr);
const body = response.body;
const lines = body.split("\n");
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
resolution = `${lang} ${resolution}`;
var m3u8Url = lines[i + 1].trim();
m3u8Url = m3u8Url.replace("./", `${url}/`);
if (host.length > 0) {
m3u8Url = `${host}${m3u8Url}`;
}
streams.push({
url: m3u8Url,
originalUrl: m3u8Url,
quality: resolution,
headers: hdr,
});
}
}
return streams;
}
// For some streams, we can form stream url using a default template.
async splitStreams(url, lang = "", hdr = {}) {
var streams = [
{
url: url,
originalUrl: url,
quality: `${lang} - Auto`,
headers: hdr,
},
];
var pref = this.getPreference("autoembed_split_stream_quality");
if (!pref) return streams;
var quality = ["360", "480", "720", "1080"];
for (var q of quality) {
var link = url;
if (q != "auto") {
link = link.replace("index.m3u8", `${q}/index.m3u8`);
q = `${q}p`;
}
streams.push({
url: link,
originalUrl: link,
quality: `${lang} - ${q}`,
headers: hdr,
});
}
return streams;
}
// Sorts streams based on user preference.
async sortStreams(streams) {
var sortedStreams = [];
var copyStreams = streams.slice();
var pref = this.getPreference("pref_video_resolution");
for (var i in streams) {
var stream = streams[i];
if (stream.quality.indexOf(pref) > -1) {
sortedStreams.push(stream);
var index = copyStreams.indexOf(stream);
if (index > -1) {
copyStreams.splice(index, 1);
}
break;
}
}
return [...sortedStreams, ...copyStreams];
}
// Gets subtitles based on TMDB id.
async getSubtitleList(id, s, e) {
var subPref = parseInt(
this.getPreference("autoembed_pref_subtitle_source")
);
var api = `https://sub.wyzie.ru/search?id=${id}`;
var hdr = {};
if (subPref === 2) {
api = `https://sources.hexa.watch/subs/${id}`;
hdr = { "Origin": "https://api.hexa.watch" };
if (s != "0") api = `${api}/${s}/${e}`;
} else {
if (s != "0") api = `${api}&season=${s}&episode=${e}`;
}
var response = await new Client().get(api, hdr);
var body = JSON.parse(response.body);
var subs = [];
for (var sub of body) {
subs.push({
file: sub.url,
label: sub.display,
});
}
return subs;
}
// For anime episode video list
async getVideoList(url) {
var streamAPI = parseInt(this.getPreference("autoembed_stream_source_3"));
var nativeSubs = this.getPreference("autoembed_pref_navtive_subtitle");
var parts = url.split("||");
var media_type = parts[0];
var id = parts[1];
var s = "0";
var e = "0";
if (media_type == "tv") {
s = parts[2];
e = parts[3];
}
var tmdb = id;
var streams = [];
var subtitles = [];
switch (streamAPI) {
case 2: {
if (media_type == "tv") {
id = `${id}/${s}/${e}`;
}
var api = `https://play2.123embed.net/server/3?path=/${media_type}/${id}`;
var response = await new Client().get(api);
if (response.statusCode != 200) {
throw new Error(
"play2.123embed.net unavailable\nPlease choose a different server"
);
}
var body = JSON.parse(response.body);
var link = body.playlist[0].file;
streams.push({
url: link,
originalUrl: link,
quality: "auto",
headers: { "Origin": "https://play2.123embed.net" },
});
break;
}
case 3: {
if (media_type == "tv") {
id = `${id}&s=${s}&e=${e}`;
}
var api = `https://autoembed.cc/embed/player.php?id=${id}`;
var response = await new Client().get(api);
if (response.statusCode != 200) {
throw new Error(
"autoembed.cc unavailable\nPlease choose a different server"
);
}
var body = response.body;
var sKey = '"file": ';
var eKey = "]});";
var start = body.indexOf(sKey);
if (start < 0) {
throw new Error(
"autoembed.cc videos unavailable\nPlease choose a different server"
);
}
start += sKey.length;
var end = body.substring(start).indexOf(eKey) + start - 1;
var strms = JSON.parse(body.substring(start, end) + "]");
for (var strm of strms) {
var link = strm.file;
var lang = strm.title;
var streamSplit = await this.splitStreams(link, lang);
streams = [...streams, ...streamSplit];
}
break;
}
case 4: {
if (media_type == "tv") {
id = `${id}&season=${s}&episode=${e}`;
}
var api = `https://flicky.host/player/desi.php?id=${id}`;
var response = await new Client().get(api, {
"Referer": "https://flicky.host/",
"sec-fetch-dest": "iframe",
});
if (response.statusCode != 200) {
throw new Error(
"flicky.host unavailable\nPlease choose a different server"
);
}
var body = response.body;
var sKey = "streams = ";
var eKey = "];";
var start = body.indexOf(sKey);
if (start < 0) {
throw new Error(
"flicky.host videos unavailable\nPlease choose a different server"
);
}
start += sKey.length;
var end = body.substring(start).indexOf(eKey) + start + 1;
var strms = JSON.parse(body.substring(start, end));
for (var strm of strms) {
var link = strm.url;
var lang = strm.language;
var streamSplit = await this.splitStreams(link, lang);
streams = [...streams, ...streamSplit];
}
break;
}
case 5: {
if (media_type == "tv") {
id = `${id}/${s}/${e}`;
}
var api = `https://vidapi.click/api/video/${media_type}/${id}`;
var response = await new Client().get(api);
if (response.statusCode != 200) {
throw new Error(
"vidapi.click unavailable\nPlease choose a different server"
);
}
var body = JSON.parse(response.body);
var link = body.sources[0].file;
if (nativeSubs) subtitles = body.tracks;
streams = await this.extractStreams(link);
break;
}
case 6: {
if (media_type == "tv") {
id = `${id}/${s}/${e}`;
}
var api = `https://sources.hexa.watch/plsdontscrapemeuwu/${id}`;
var hdr = { "Origin": "https://api.hexa.watch" };
var response = await new Client().get(api, hdr);
if (response.statusCode != 200) {
throw new Error(
"hexa.watch unavailable\nPlease choose a different server"
);
}
var body = JSON.parse(response.body);
var strms = body.streams;
for (var strm of strms) {
var streamLink = strm.url;
if (streamLink.length > 0) {
streams.push({
url: strm.url,
originalUrl: strm.url,
quality: `${strm.label} - Auto`,
headers: strm.headers,
});
}
}
break;
}
case 7: {
if (media_type == "tv") {
id = `${id}/${s}/${e}`;
}
var api = `https://vidsrc.su/embed/${media_type}/${id}`;
var response = await new Client().get(api);
if (response.statusCode != 200) {
throw new Error(
"vidsrc.su unavailable\nPlease choose a different server"
);
}
var body = response.body;
var sKey = "fixedServers = ";
var eKey = "];";
var start = body.indexOf(sKey);
if (start < 0) {
throw new Error(
"vidsrc.su videos unavailable\nPlease choose a different server"
);
}
start += sKey.length;
var end = body.substring(start).indexOf(eKey) + start + 1;
var strms = body.substring(start, end);
// Split the data into lines
var lines = strms.split("\n");
// Regex to match URLs in quotes that start with https://
var regex = /url:\s*'(https:\/\/[^']+)'/;
var availableStreams = [];
// Process each line
lines.forEach((line) => {
var match = line.match(regex);
if (match && match[1]) {
// Extract the label from the line
var labelMatch = line.match(/label:\s*'([^']+)'/);
var label = labelMatch ? labelMatch[1] : "Unknown";
// Add to our results
availableStreams.push({
url: match[1],
label: label,
});
}
});
for (var stream of availableStreams) {
var streamSplit = await this.extractStreams(stream.url, stream.label);
streams = [...streams, ...streamSplit];
}
if (nativeSubs) {
// subtitles
sKey = "const subtitles = ";
eKey = "];";
start = body.indexOf(sKey);
if (start < 0) {
break; // no need for native subtitle if not found.
}
start += sKey.length;
end = body.substring(start).indexOf(eKey) + start + 1;
var natSubs = JSON.parse(body.substring(start, end));
natSubs.forEach((sub) => {
subtitles.push({
file: sub.url,
label: sub.display,
});
});
}
break;
}
case 8: {
function reverse(str) {
return str.split("").reverse().join("");
}
if (media_type == "tv") {
id = `${id}/${s}/${e}`;
}
var baseUrl = "https://embed.su";
var embedUrl = `${baseUrl}/embed/${media_type}/${id}`;
var response = await new Client().get(
embedUrl,
this.getHeaders(baseUrl)
);
var body = response.body;
var sKey = "JSON.parse(atob(`";
var start = body.indexOf(sKey) + sKey.length;
var end = body.substring(start).indexOf("`") + start;
var configHash = body.substring(start, end);
var config = JSON.parse(this.decodeBase64(configHash));
var encodedHash = this.decodeBase64(config.hash);
var decodeHash = reverse(
encodedHash
.split(".")
.map((item) => reverse(item))
.join("")
);
encodedHash = JSON.parse(this.decodeBase64(decodeHash));
var serverHash = encodedHash[0].hash;
var api = `${baseUrl}/api/e/${serverHash}`;
response = await new Client().get(api, this.getHeaders(baseUrl));
var jsonRes = JSON.parse(response.body);
streams = await this.extractStreams(
jsonRes.source,
"",
this.getHeaders(baseUrl),
baseUrl
);
if (nativeSubs) subtitles = jsonRes.subtitles;
break;
}
default: {
if (media_type == "tv") {
id = `${id}/${s}/${e}`;
}
var api = `${this.source.apiUrl}/api/getVideoSource?type=${media_type}&id=${id}`;
var response = await new Client().get(
api,
this.getHeaders(this.source.apiUrl)
);
if (response.statusCode != 200) {
throw new Error(
"tom.autoembed.cc unavailable\nPlease choose a different server"
);
}
var body = JSON.parse(response.body);
var link = body.videoSource;
if (nativeSubs) subtitles = body.subtitles;
streams = await this.extractStreams(link);
break;
}
}
if (streams.length < 1) {
throw new Error(
"No streams unavailable\nPlease choose a different server"
);
}
var apiSubs = await this.getSubtitleList(tmdb, s, e);
streams[0].subtitles = [...subtitles, ...apiSubs];
return await this.sortStreams(streams);
}
// For manga chapter pages
async getPageList() {
throw new Error("getPageList not implemented");
}
getFilterList() {
throw new Error("getFilterList not implemented");
}
getSourcePreferences() {
return [
{
key: "pref_latest_time_window",
listPreference: {
title: "Preferred latest trend time window",
summary: "",
valueIndex: 0,
entries: ["Day", "Week"],
entryValues: ["day", "week"],
},
},
{
key: "pref_video_resolution",
listPreference: {
title: "Preferred video resolution",
summary: "",
valueIndex: 0,
entries: ["Auto", "1080p", "720p", "360p"],
entryValues: ["auto", "1080", "720", "360"],
},
},
{
key: "pref_content_priority",
listPreference: {
title: "Preferred content priority",
summary: "Choose which type of content to show first",
valueIndex: 0,
entries: ["Movies", "Series"],
entryValues: ["movies", "series"],
},
},
{
key: "autoembed_split_stream_quality",
"switchPreferenceCompat": {
"title": "Split stream into different quality streams",
"summary": "Split stream Auto into 360p/720p/1080p",
"value": true,
},
},
{
key: "autoembed_stream_source_3",
listPreference: {
title: "Preferred stream source",
summary: "",
valueIndex: 0,
entries: [
"tom.autoembed.cc",
"123embed.net",
"autoembed.cc - Indian languages",
"flicky.host - Indian languages",
"vidapi.click",
"hexa.watch",
"vidsrc.su",
"embed.su",
],
entryValues: ["1", "2", "3", "4", "5", "6", "7", "8"],
},
},
{
key: "autoembed_pref_navtive_subtitle",
"switchPreferenceCompat": {
"title": "Use native subtitles as well",
"summary":
"Use subtitles provided by the source along with subtitle API",
"value": true,
},
},
{
key: "autoembed_pref_subtitle_source",
listPreference: {
title: "Preferred subtitle source",
summary: "",
valueIndex: 0,
entries: ["sub.wyzie.ru", "hexa.watch"],
entryValues: ["1", "2"],
},
},
];
}
}

View File

@@ -1,353 +0,0 @@
const mangayomiSources = [{
"name": "Dramacool",
"lang": "all",
"baseUrl": "https://dramacool.com.tr",
"apiUrl": "",
"iconUrl": "https://www.google.com/s2/favicons?sz=128&domain=https://dramacool.com.tr",
"typeSource": "multi",
"itemType": 1,
"version": "1.0.0",
"pkgPath": "anime/src/all/dramacool.js"
}];
class DefaultExtension extends MProvider {
getHeaders(url) {
return {
'Referer': url,
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6788.76 Safari/537.36"
}
}
getPreference(key) {
return new SharedPreferences().get(key);
}
getBaseUrl() {
return this.source.baseUrl;
}
async request(slug) {
const baseUrl = this.getBaseUrl()
var url = `${baseUrl}${slug}`
var res = await new Client().get(url, this.getHeaders(baseUrl));
var doc = new Document(res.body);
return doc
}
async getList(slug) {
var body = await this.request(slug);
var list = []
var hasNextPage = body.selectFirst("a.next.page-numbers").text.length > 0 ? true : false;
var items = body.select(".switch-block.list-episode-item > li")
items.forEach(item => {
var a = item.selectFirst("a")
var link = a.getHref.replace(this.getBaseUrl(), "")
var imageUrl = a.selectFirst("img").getSrc
var name = a.selectFirst("h3").text
list.push({ name, link, imageUrl })
})
return { list, hasNextPage };
}
async getPopular(page) {
var slug = "/most-popular-drama"
return await this.getList(`${slug}/page/${page}/`)
}
get supportsLatest() {
throw new Error("supportsLatest not implemented");
}
async getLatestUpdates(page) {
var slug = this.getPreference("dramacool_latest_list")
return await this.getList(`/${slug}/page/${page}/`)
}
statusFromString(status) {
return {
"Ongoing": 0,
"Completed": 1,
}[status] ?? 5;
}
async search(query, page, filters) {
var slug = `/page/${page}/?type=movies&s=${query}`
return await this.getList(slug)
}
formatReleaseDate(str) {
var timeSplit = str.split(" ")
var t = parseInt(timeSplit[0])
var unit = timeSplit[1]
var mins = 0
var mons = 0
if (unit.includes('minute')) {
mins = t;
} else if (unit.includes('hour')) {
mins = t * 60;
} else if (unit.includes('day')) {
mins = t * 60 * 24;
} else if (unit.includes('week')) {
mins = t * 60 * 24 * 7;
} else if (unit.includes('month')) {
mons = t;
}
var now = new Date();
now.setMinutes(now.getMinutes() - mins)
now.setMinutes(now.getMonth() - mons)
var pastDate = new Date(now);
return "" + pastDate.valueOf();
}
async getDetail(url) {
if (url.includes("-episode")) {
url = '/series' + url.split("-episode")[0] + "/"
} else if (url.includes("-full-movie")) {
url = '/series' + url.split("-full-movie")[0] + "/"
}
var body = await this.request(url);
var infos = body.select(".info > p")
var name = body.selectFirst("h1").text.trim()
var imageUrl = body.selectFirst(".img").selectFirst("img").getSrc
var isDescription = infos[1].text.includes("Description")
var description = isDescription ? infos[2].text.trim() : ""
var link = `${this.getBaseUrl()}${url}`
var statusIndex = infos.at(-3).text.includes("Status:") ? -3 : -2
var status = this.statusFromString(infos.at(statusIndex).selectFirst("a").text)
var genre = []
infos.at(-1).select("a").forEach(a => genre.push(a.text.trim()))
var chapters = []
var epLists = body.select("ul.list-episode-item-2.all-episode > li")
for (var ep of epLists) {
var a = ep.selectFirst('a')
var epLink = a.getHref.replace(this.getBaseUrl(), "")
var epName = a.selectFirst("h3").text.replace(name + " ", "")
var scanlator = a.selectFirst("span.type").text
var dateUpload = this.formatReleaseDate(a.selectFirst("span.time").text)
chapters.push({
name: epName,
url: epLink,
scanlator,
dateUpload
})
}
return { name, imageUrl, description, link, status, genre, chapters }
}
async splitStreams(streams, server) {
var pref = this.getPreference("dramacool_split_stream_quality");
if (!pref) return streams
var autoStream = streams[0]
var autoStreamUrl = autoStream.url
var hdr = autoStream.headers
var hostUrl = ""
if (server == "Asianload") {
hostUrl = autoStreamUrl.substring(0, autoStreamUrl.indexOf("/media"))
} else {
hostUrl = autoStreamUrl.substring(0, autoStreamUrl.indexOf("master.m3u8"))
}
var response = await new Client().get(autoStreamUrl, hdr)
var body = response.body;
var lines = body.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-STREAM-INF:')) {
var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
resolution = `${server} - ${resolution}`
var m3u8Url = lines[i + 1].trim();
m3u8Url = hostUrl + m3u8Url
streams.push({
url: m3u8Url,
originalUrl: m3u8Url,
quality: resolution,
headers: hdr
});
}
}
return streams
}
decodeBase64(f) {
var g = {},
b = 65,
d = 0,
a, c = 0,
h, e = "",
k = String.fromCharCode,
l = f.length;
for (a = ""; 91 > b;) a += k(b++);
a += a.toLowerCase() + "0123456789+/";
for (b = 0; 64 > b; b++) g[a.charAt(b)] = b;
for (a = 0; a < l; a++)
for (b = g[f.charAt(a)], d = (d << 6) + b, c += 6; 8 <= c;)((h = d >>> (c -= 8) & 255) || a < l - 2) && (e += k(h));
return e
};
async extractDramacoolEmbed(doc) {
var streams = []
var script = doc.select('script').at(-2)
var unpack = unpackJs(script.text)
var skey = 'hls2":"'
var eKey = '"};jwplayer'
var start = unpack.indexOf(skey) + skey.length
var end = unpack.indexOf(eKey, start)
var track = unpack.substring(start, end)
streams.push({
url: track,
originalUrl: track,
quality: "Dramacool - Auto",
headers: this.getHeaders("https://dramacool.men/")
});
streams = await this.splitStreams(streams, "Dramacool")
return streams
}
async extractAsianLoadEmbed(doc) {
var streams = []
var script = doc.select('script').at(-2)
var unpack = script.text
// tracks
var skey = '|image|'
var eKey = '|'
var start = unpack.indexOf(skey) + skey.length
var end = unpack.indexOf(eKey, start)
var track = unpack.substring(start, end)
var streamUrl = this.decodeBase64(track)
// subs
eKey = "|default|"
var end = unpack.indexOf(eKey)
var subs = []
if (end != -1) {
skey = "|type|"
var start = unpack.indexOf(skey) + skey.length
var subTracks = unpack.substring(start, end).split("|")
subs.push({
file: this.decodeBase64(subTracks[1]),
label: subTracks[0]
})
}
streams.push({
url: streamUrl,
originalUrl: streamUrl,
quality: "Asianload - Auto",
subtitles: subs,
headers: this.getHeaders("https://asianload.cfd/")
});
// Download url
skey = '|_blank|'
eKey = '|'
start = unpack.indexOf(skey) + skey.length
end = unpack.indexOf(eKey, start)
track = unpack.substring(start, end)
var downUrl = this.decodeBase64(track)
streams.push({
url: downUrl,
originalUrl: downUrl,
quality: "Asianload - Direct download",
headers: this.getHeaders("https://asianload.cfd/")
});
streams = await this.splitStreams(streams, "Asianload")
return streams
}
// Sorts streams based on user preference.
async sortStreams(streams) {
var sortedStreams = [];
var copyStreams = streams.slice()
var pref = this.getPreference("dramacool_video_resolution");
for (var i in streams) {
var stream = streams[i];
if (stream.quality.indexOf(pref) > -1) {
sortedStreams.push(stream);
var index = copyStreams.indexOf(stream);
if (index > -1) {
copyStreams.splice(index, 1);
}
break;
}
}
return [...sortedStreams, ...copyStreams]
}
// For anime episode video list
async getVideoList(url) {
var res = await this.request(url)
var iframe = res.selectFirst("iframe").attr("src").trim()
if (iframe == "") {
throw new Error("No iframe found")
}
var streams = []
res = await new Client().get(iframe)
var doc = new Document(res.body);
if (iframe.includes("//dramacool")) {
streams = await this.extractDramacoolEmbed(doc)
} else if (iframe.includes("//asianload")) {
streams = await this.extractAsianLoadEmbed(doc)
}
return this.sortStreams(streams)
}
getSourcePreferences() {
return [
{
key: 'dramacool_latest_list',
listPreference: {
title: 'Preferred latest list',
summary: 'Choose which type of content to be shown "Lastest"',
valueIndex: 0,
entries: ["Drama", "Movie", "KShow"],
entryValues: ["recently-added-drama", "recently-added-movie", "recently-added-kshow"]
}
},
{
key: 'dramacool_split_stream_quality',
switchPreferenceCompat: {
title: 'Split stream into different quality streams',
summary: "Split stream Auto into 360p/720p/1080p",
value: true
}
}, {
key: 'dramacool_video_resolution',
listPreference: {
title: 'Preferred video resolution',
summary: '',
valueIndex: 0,
entries: ["Auto", "Direct download", "720p", "480", "360p"],
entryValues: ["Auto", "download", "720", "480", "360"]
}
},
]
}
}

View File

@@ -1,333 +0,0 @@
const mangayomiSources = [{
"name": "NetMirror",
"id": 446414301,
"lang": "all",
"baseUrl": "https://netfree2.cc",
"apiUrl": "https://netfree2.cc",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.netflixmirror.png",
"typeSource": "single",
"itemType": 1,
"version": "0.3.4",
"pkgPath": "anime/src/all/netflixmirror.js"
}];
class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
getPreference(key) {
const preferences = new SharedPreferences();
return preferences.get(key);
}
getTVBaseUrl() {
return this.getPreference("netmirror_override_tv_base_url");
}
getServiceDetails() {
return this.getPreference("netmirror_pref_service");
}
getPoster(id, service) {
if (service === "nf")
return `https://imgcdn.media/poster/v/${id}.jpg`
if (service === "pv")
return `https://imgcdn.media/pv/480/${id}.jpg`
}
async getCookie(service) {
const preferences = new SharedPreferences();
let cookie = preferences.getString("cookie", "");
var cookie_ts = parseInt(preferences.getString("cookie_ts", "0"));
var now_ts = parseInt(new Date().getTime() / 1000);
// Cookie lasts for 24hrs but still checking for 12hrs
if (now_ts - cookie_ts > 60 * 60 * 12) {
var baseUrl = this.getTVBaseUrl()
const check = await this.client.get(baseUrl + `/mobile/home`, { "cookie": cookie });
const hDocBody = new Document(check.body).selectFirst("body")
const addhash = hDocBody.attr("data-addhash");
const data_time = hDocBody.attr("data-time");
var res = await this.client.post(`${baseUrl}/tv/p.php`, { "cookie": "" }, { "hash": addhash });
cookie = res.headers["set-cookie"];
preferences.setString("cookie", cookie);
preferences.setString("cookie_ts", data_time);
}
service = service ?? this.getServiceDetails();
return `ott=${service}; ${cookie}`;
}
async request(slug, service = null, cookie = null) {
var service = service ?? this.getServiceDetails();
var cookie = cookie ?? await this.getCookie();
var srv = ""
if (service === "pv") srv = "/" + service
var url = this.getTVBaseUrl() + "/tv" + srv + slug
return (await this.client.get(url, { "cookie": cookie })).body;
}
async getHome(body) {
var service = this.getServiceDetails();
var list = []
if (service === "nf") {
var body = await this.request("/home", service)
var elements = new Document(body).select("a.slider-item.boxart-container.open-modal.focusme");
elements.forEach(item => {
var id = item.attr("data-post")
if (id.length > 0) {
var imageUrl = this.getPoster(id, service)
// Having no name breaks the script so having "id" as name
var name = `\n${id}`
list.push({ name, imageUrl, link: id })
}
})
} else {
var body = await this.request("/homepage.php", service)
var elements = JSON.parse(body).post
elements.forEach(item => {
var ids = item.ids
ids.split(",").forEach(id => {
var imageUrl = this.getPoster(id, service)
// Having no name breaks the script so having "id" as name
var name = `\n${id}`
list.push({ name, imageUrl, link: id })
})
})
}
return {
list: list,
hasNextPage: false
}
}
async getPopular(page) {
return await this.getHome()
}
async getLatestUpdates(page) {
return await this.getHome()
}
async search(query, page, filters) {
var service = this.getServiceDetails();
const data = JSON.parse(await this.request(`/search.php?s=${query}`, service));
const list = [];
data.searchResult.map(async (res) => {
const id = res.id;
list.push({ name: res.t, imageUrl: this.getPoster(id, service), link: id });
})
return {
list: list,
hasNextPage: false
}
}
async getDetail(url) {
var service = this.getServiceDetails();
var cookie = await this.getCookie(service);
var linkSlug = "https://netflix.com/title/"
if (service === "pv") linkSlug = `https://www.primevideo.com/detail/`
// Check needed while refreshing existing data
var vidId = url
if (url.includes(linkSlug)) vidId = url.replaceAll(linkSlug, '')
const data = JSON.parse(await this.request(`/post.php?id=${vidId}`));
const name = data.title;
const genre = [data.ua, ...(data.genre || '').split(',').map(g => g.trim())];
const description = data.desc;
let episodes = [];
var seasons = data.season
if (seasons) {
let newEpisodes = [];
await Promise.all(seasons.map(async (season) => {
const eps = await this.getEpisodes(name, vidId, season.id, 1, service, cookie);
newEpisodes.push(...eps);
}));
episodes.push(...newEpisodes);
} else {
// For movies aka if there are no seasons and episodes
episodes.push({
name: `Movie`,
url: vidId
});
}
var link = `${linkSlug}${vidId}`
return {
name, imageUrl: this.getPoster(vidId, service), link, description, status: 1, genre, episodes
};
}
async getEpisodes(name, eid, sid, page, service, cookie) {
const episodes = [];
let pg = page;
while (true) {
try {
const data = JSON.parse(await this.request(`/episodes.php?s=${sid}&series=${eid}&page=${pg}`, service, cookie));
data.episodes?.forEach(ep => {
var season = ep.s.replace('S', 'Season ')
var epNum = ep.ep.replace("E", "")
var epText = `Episode ${epNum}`
var title = ep.t
title = title == epText ? title : `${epText}: ${title}`
episodes.push({
name: `${season} ${title}`,
url: ep.id
});
});
if (data.nextPageShow === 0) break;
pg++;
} catch (_) {
break;
}
}
return episodes.reverse();
}
// Sorts streams based on user preference.
async sortStreams(streams) {
var sortedStreams = [];
var copyStreams = streams.slice()
var pref = this.getPreference("netmirror_pref_video_resolution");
for (var i in streams) {
var stream = streams[i];
if (stream.quality.indexOf(pref) > -1) {
sortedStreams.push(stream);
var index = copyStreams.indexOf(stream);
if (index > -1) {
copyStreams.splice(index, 1);
}
break;
}
}
return [...sortedStreams, ...copyStreams]
}
async getVideoList(url) {
var baseUrl = this.getTVBaseUrl()
var url = `/playlist.php?id=${url}`
const data = JSON.parse(await this.request(url));
let videoList = [];
let subtitles = [];
let audios = [];
var playlist = data[0]
var source = playlist.sources[0]
var link = baseUrl + source.file;
var headers =
{
'Origin': baseUrl,
'Referer': `${baseUrl}/`
};
// Auto
videoList.push({ url: link, quality: "Auto", "originalUrl": link, headers });
var resp = await this.client.get(link, headers);
if (resp.statusCode === 200) {
const masterPlaylist = resp.body;
if (masterPlaylist.indexOf("#EXT-X-STREAM-INF:") > 1) {
masterPlaylist.substringAfter('#EXT-X-MEDIA:').split('#EXT-X-MEDIA:').forEach(it => {
if (it.includes('TYPE=AUDIO')) {
const audioInfo = it.substringAfter('TYPE=AUDIO').substringBefore('\n');
const language = audioInfo.substringAfter('NAME="').substringBefore('"');
const url = audioInfo.substringAfter('URI="').substringBefore('"');
audios.push({ file: url, label: language });
}
});
masterPlaylist.substringAfter('#EXT-X-STREAM-INF:').split('#EXT-X-STREAM-INF:').forEach(it => {
var quality = `${it.substringAfter('RESOLUTION=').substringAfter('x').substringBefore(',')}p`;
let videoUrl = it.substringAfter('\n').substringBefore('\n');
if (!videoUrl.startsWith('http')) {
videoUrl = resp.request.url.substringBeforeLast('/') + `/${videoUrl}`;
}
headers['Host'] = videoUrl.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/)[1]
videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, headers });
});
}
if ("tracks" in playlist) {
await Promise.all(playlist.tracks.map(async (track) => {
if (track.kind == 'captions') {
var subUrl = track.file
subUrl = subUrl.startsWith("//") ? `https:${subUrl}` : subUrl;
var subText = await this.client.get(subUrl)
subtitles.push({
label: track.label,
file: subText.body
});
}
}));
}
}
videoList[0].audios = audios;
videoList[0].subtitles = subtitles;
return this.sortStreams(videoList);
}
getSourcePreferences() {
return [{
key: "netmirror_override_tv_base_url",
editTextPreference: {
title: "Override tv base url",
summary: "",
value: "https://netfree2.cc",
dialogTitle: "Override base url",
dialogMessage: "",
}
}, {
key: 'netmirror_pref_service',
listPreference: {
title: 'Preferred OTT service',
summary: '',
valueIndex: 0,
entries: ["Net mirror", "Prime mirror"],
entryValues: ["nf", "pv",]
}
}, {
key: 'netmirror_pref_video_resolution',
listPreference: {
title: 'Preferred video resolution',
summary: '',
valueIndex: 0,
entries: ["1080p", "720p", "480p"],
entryValues: ["1080", "720", "480"]
}
}
];
}
}

View File

@@ -1,279 +0,0 @@
const mangayomiSources = [
{
"name": "Soaper",
"id": 764093578,
"lang": "all",
"baseUrl": "https://soaper.cc",
"apiUrl": "",
"iconUrl":
"https://www.google.com/s2/favicons?sz=128&domain=https://soaper.cc/",
"typeSource": "multi",
"version": "1.0.5",
"itemType": 1,
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/all/soaper.js"
}
];
// Authors: - Swakshan, kodjodevf
class DefaultExtension extends MProvider {
getHeaders(url) {
return {
Referer: url,
Origin: url,
};
}
getPreference(key) {
return new SharedPreferences().get(key);
}
getBasueUrl() {
return this.getPreference("soaper_override_base_url");
}
async request(slug) {
const baseUrl = this.getBasueUrl();
var url = `${baseUrl}/${slug}`;
var res = await new Client().get(url, this.getHeaders(baseUrl));
var doc = new Document(res.body);
return doc;
}
async requestJSON(slug, data) {
const baseUrl = this.getBasueUrl();
var url = `${baseUrl}/${slug}`;
var res = await new Client().post(url, this.getHeaders(baseUrl), data);
return JSON.parse(res.body);
}
async formatList(slug, page) {
const baseUrl = this.getPreference("soaper_override_base_url");
slug = parseInt(page) > 1 ? `${slug}?page=${page}` : slug;
var doc = await this.request(slug);
var list = [];
var movies = doc.select(".thumbnail.text-center");
for (var movie of movies) {
var linkSection = movie.selectFirst("div.img-group > a");
var link = linkSection.getHref.substring(1);
var poster = linkSection.selectFirst("img").getSrc;
var imageUrl = `${baseUrl}${poster}`;
var name = movie.selectFirst("h5").selectFirst("a").text;
list.push({ name, imageUrl, link });
}
var hasNextPage = false;
if (slug.indexOf("search.html?") == -1) {
var pagination = doc.select("ul.pagination > li");
var last_page_num = parseInt(pagination[pagination.length - 2].text);
hasNextPage = page < last_page_num ? true : false;
}
return { list, hasNextPage };
}
async filterList(year = "all", genre = "all", sort = "new", page = 1) {
year = year == "all" ? "" : `/year/${year}`;
genre = genre == "all" ? "" : `/cat/${genre}`;
sort = sort == "new" ? "" : `/sort/${sort}`;
var slug = `${sort}${year}${genre}`;
var movieList = await this.formatList(`movielist${slug}`, page);
var seriesList = await this.formatList(`tvlist${slug}`, page);
var list = [];
var priority = this.getPreference("soaper_content_priority");
if (priority === "series") {
list = [...seriesList.list, ...movieList.list];
} else {
list = [...movieList.list, ...seriesList.list];
}
var hasNextPage = seriesList.hasNextPage || movieList.hasNextPage;
return { list, hasNextPage };
}
async getPopular(page) {
return await this.filterList("all", "all", "hot", page);
}
get supportsLatest() {
throw new Error("supportsLatest not implemented");
}
async getLatestUpdates(page) {
return await this.filterList("all", "all", "new", page);
}
async search(query, page, filters) {
var seriesList = [];
var movieList = [];
var list = [];
var res = await this.formatList(`search.html?keyword=${query}`, 1);
var movies = res["list"];
for (var movie of movies) {
var link = movie.link;
if (link.indexOf("tv_") != -1) {
seriesList.push(movie);
} else {
movieList.push(movie);
}
}
var priority = this.getPreference("soaper_content_priority");
if (priority === "series") {
list = [...seriesList, ...movieList];
} else {
list = [...movieList, ...seriesList];
}
return { list, hasNextPage: false };
}
async getDetail(url) {
const baseUrl = this.getPreference("soaper_override_base_url");
var slug = url.replace(`${baseUrl}/`,'')
var doc = await this.request(slug);
var name = doc
.selectFirst(".col-sm-12.col-lg-12.text-center")
.selectFirst("h4")
.text.trim();
var poster = doc
.selectFirst(".thumbnail.text-center")
.selectFirst("img").getSrc;
var imageUrl = `${baseUrl}${poster}`;
var description = doc.selectFirst("p#wrap").text.trim();
var link = `${baseUrl}/${slug}`;
var chapters = [];
if (slug.indexOf("tv_") != -1) {
var seasonList = doc.select(".alert.alert-info-ex.col-sm-12");
var seasonCount = seasonList.length;
for (var season of seasonList) {
var eps = season.select(".col-sm-12.col-md-6.col-lg-4.myp1");
for (var ep of eps) {
var epLinkSection = ep.selectFirst("a");
var epLink = epLinkSection.getHref.substring(1);
var epName = epLinkSection.text;
chapters.push({
name: `S${seasonCount}E${epName}`,
url: epLink,
});
}
seasonCount--;
}
} else {
chapters.push({
name: "Movie",
url: slug,
});
}
return { name, imageUrl, description, link, chapters };
}
// For anime episode video list
async getVideoList(url) {
var body = await this.request(url);
var baseUrl = this.getBasueUrl();
var streams = [];
// Traditional servers
var eId = body.selectFirst("#hId").attr("value");
var hIsW = body.selectFirst("#hIsW").attr("value");
var apiType = url[0].toUpperCase();
var servers = [0, 1];
for (var serverNum of servers) {
var serverName = body.selectFirst(`#server_button_${serverNum}`).text;
if (serverName.length < 1) continue;
var data = {
pass: eId,
param: "",
extra: "1",
e2: hIsW,
server: "" + serverNum,
};
var res = await this.requestJSON(
`home/index/Get${apiType}InfoAjax`,
data
);
var streamUrl = baseUrl + res.val;
var subs = [];
var vidSubs = res.subs;
if (vidSubs != null && vidSubs.length > 0) {
for (var sub of vidSubs) {
subs.push({
file: baseUrl + sub.path,
label: sub.name,
});
}
}
streams.push({
url: streamUrl,
originalUrl: streamUrl,
quality: serverName,
subtitles: subs,
});
}
// Download servers
var modal_footer = body.select(".modal-footer > a");
if (modal_footer.length > 0) {
modal_footer.reverse();
for (var item of modal_footer) {
var dSlug = item.getHref;
var dBody = await this.request(dSlug);
var res = dBody.selectFirst("#res").attr("value");
var mb = dBody.selectFirst("#mb").attr("value");
var streamLink = dBody.selectFirst("#link").attr("value");
streams.push({
url: streamLink,
originalUrl: streamLink,
quality: `Download Server: ${res} [${mb}]`,
});
}
}
return streams;
}
// For manga chapter pages
async getPageList() {
throw new Error("getPageList not implemented");
}
getFilterList() {
throw new Error("getFilterList not implemented");
}
getSourcePreferences() {
return [
{
key: "soaper_override_base_url",
editTextPreference: {
title: "Override base url",
summary: "Default: https://soaper.cc",
value: "https://soaper.cc",
dialogTitle: "Override base url",
dialogMessage: "",
},
},
{
key: "soaper_content_priority",
listPreference: {
title: "Preferred content priority",
summary: "Choose which type of content to show first",
valueIndex: 0,
entries: ["Movies", "Series"],
entryValues: ["movies", "series"],
},
},
];
}
}

View File

@@ -1,660 +0,0 @@
const mangayomiSources = [{
"name": "Torrentio (Torrent)",
"lang": "all",
"baseUrl": "https://torrentio.strem.fun",
"apiUrl": "",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.torrentio.png",
"typeSource": "torrent",
"isManga": false,
"itemType": 1,
"version": "0.0.25",
"pkgPath": "anime/src/all/torrentio.js"
}];
class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
justWatchQuery() {
return `
query GetPopularTitles(
$country: Country!,
$first: Int!,
$language: Language!,
$offset: Int,
$searchQuery: String,
$packages: [String!]!,
$objectTypes: [ObjectType!]!,
$popularTitlesSortBy: PopularTitlesSorting!,
$releaseYear: IntFilter
) {
popularTitles(
country: $country
first: $first
offset: $offset
sortBy: $popularTitlesSortBy
filter: {
objectTypes: $objectTypes,
searchQuery: $searchQuery,
packages: $packages,
genres: [],
excludeGenres: [],
releaseYear: $releaseYear
}
) {
edges {
node {
id
objectType
content(country: $country, language: $language) {
fullPath
title
shortDescription
externalIds {
imdbId
}
posterUrl
genres {
translation(language: $language)
}
credits {
name
role
}
}
}
}
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
`.trim();
}
async makeGraphQLRequest(query, variables) {
const res = await this.client.post("https://apis.justwatch.com/graphql", { "Content-Type": "application/json" },
{
query: query,
variables
});
return res;
}
async searchAnimeRequest(page, query) {
const preferences = new SharedPreferences();
const country = preferences.get("jw_region1");
const language = preferences.get("jw_lang");
const perPage = 40;
const year = 0;
const searchQueryRegex = /[^a-zA-Z0-9 ]/g;
const sanitizedQuery = query.replace(searchQueryRegex, "").trim();
const variables = {
first: perPage,
offset: (page - 1) * perPage,
platform: "WEB",
country: country,
language: language,
searchQuery: sanitizedQuery,
packages: [],
objectTypes: [],
popularTitlesSortBy: "TRENDING",
releaseYear: {
min: year,
max: year
}
};
return await this.makeGraphQLRequest(this.justWatchQuery(), variables);
}
parseSearchJson(jsonLine) {
const popularTitlesResponse = JSON.parse(jsonLine);
const edges = popularTitlesResponse?.data?.popularTitles?.edges || [];
const hasNextPage = popularTitlesResponse?.data?.popularTitles?.pageInfo?.hasNextPage || false;
const animeList = edges
.map(edge => {
const node = edge?.node;
const content = node?.content;
if (!node || !content) return null;
return {
link: `${content.externalIds?.imdbId || ""},${node.objectType || ""},${content.fullPath || ""}`,
name: content.title || "",
imageUrl: `https://images.justwatch.com${content.posterUrl?.replace("{profile}", "s276")?.replace("{format}", "webp")}`,
description: content.shortDescription || "",
genre: content.genres?.map(genre => genre.translation).filter(Boolean) || [],
author: (content.credits?.filter(credit => credit.role === "DIRECTOR").map(credit => credit.name) || []).join(", "),
artist: (content.credits?.filter(credit => credit.role === "ACTOR").slice(0, 4).map(credit => credit.name) || []).join(", "),
};
})
.filter(Boolean);
return { "list": animeList, hasNextPage };
}
get supportsLatest() {
return false;
}
async getPopular(page) {
return this.parseSearchJson((await this.searchAnimeRequest(page, "")).body);
}
async getLatestUpdates(page) {
}
async search(query, page, filters) {
return this.parseSearchJson((await this.searchAnimeRequest(page, query)).body);
}
async getDetail(url) {
const anime = {};
const parts = url.split(",");
const type = parts[1].toLowerCase();
const imdbId = parts[0];
const response = await this.client.get(`https://cinemeta-live.strem.io/meta/${type}/${imdbId}.json`);
const meta = JSON.parse(response.body).meta;
if (!meta) return anime;
anime.episodes = (() => {
switch (meta.type) {
case "show":
const videos = meta.videos || [];
return videos
.filter(video => (video.firstAired ? new Date(video.firstAired) : Date.now()) < Date.now())
.map(video => {
const firstAired = video.firstAired ? new Date(video.firstAired) : Date.now();
return {
url: `/stream/series/${video.id}.json`,
dateUpload: firstAired.valueOf().toString(),
name: `S${(video.season || "").toString().trim()}:E${(video.number || "").toString()} - ${video.name || ""}`,
};
})
.sort((a, b) => {
const seasonA = parseInt(a.name.substringAfter("S").substringBefore(":"), 10);
const seasonB = parseInt(b.name.substringAfter("S").substringBefore(":"), 10);
const episodeA = parseInt(a.name.substringAfter("E").substringBefore(" -"), 10);
const episodeB = parseInt(b.name.substringAfter("E").substringBefore(" -"), 10);
return seasonA - seasonB || episodeA - episodeB;
})
.reverse();
case "movie":
return [
{
url: `/stream/movie/${meta.id}.json`,
name: "Movie"
}
].reverse();
default:
return [];
}
})();
return anime;
}
appendQueryParam(key, values) {
let url = "";
if (values && values.length > 0) {
const filteredValues = Array.from(values).filter(value => value.trim() !== "").join(",");
url += `${key}=${filteredValues}|`;
}
return url;
};
async getVideoList(url) {
const preferences = new SharedPreferences();
let mainURL = `${this.source.baseUrl}/`;
mainURL += this.appendQueryParam("providers", preferences.get("provider_selection1"));
mainURL += this.appendQueryParam("language", preferences.get("lang_selection"));
mainURL += this.appendQueryParam("qualityfilter", preferences.get("quality_selection"));
mainURL += this.appendQueryParam("sort", new Set([preferences.get("sorting_link")]));
mainURL += url;
mainURL = mainURL.replace(/\|$/, "");
const responseEpisodes = await this.client.get(mainURL);
const streamList = JSON.parse(responseEpisodes.body);
const animeTrackers = `
http://nyaa.tracker.wf:7777/announce,
http://anidex.moe:6969/announce,http://tracker.anirena.com:80/announce,
udp://tracker.uw0.xyz:6969/announce,
http://share.camoe.cn:8080/announce,
http://t.nyaatracker.com:80/announce,
udp://47.ip-51-68-199.eu:6969/announce,
udp://9.rarbg.me:2940,
udp://9.rarbg.to:2820,
udp://exodus.desync.com:6969/announce,
udp://explodie.org:6969/announce,
udp://ipv4.tracker.harry.lu:80/announce,
udp://open.stealth.si:80/announce,
udp://opentor.org:2710/announce,
udp://opentracker.i2p.rocks:6969/announce,
udp://retracker.lanta-net.ru:2710/announce,
udp://tracker.cyberia.is:6969/announce,
udp://tracker.dler.org:6969/announce,
udp://tracker.ds.is:6969/announce,
udp://tracker.internetwarriors.net:1337,
udp://tracker.openbittorrent.com:6969/announce,
udp://tracker.opentrackr.org:1337/announce,
udp://tracker.tiny-vps.com:6969/announce,
udp://tracker.torrent.eu.org:451/announce,
udp://valakas.rollo.dnsabr.com:2710/announce,
udp://www.torrent.eu.org:451/announce
`.split(",").map(tracker => tracker.trim()).filter(tracker => tracker);
const videos = this.sortVideos((streamList.streams || []).map(stream => {
const hash = `magnet:?xt=urn:btih:${stream.infoHash}&dn=${stream.infoHash}&tr=${animeTrackers.join("&tr=")}&index=${stream.fileIdx}`;
const videoTitle = `${(stream.name || "").replace("Torrentio\n", "")}\n${stream.title || ""}`.trim();
return {
url: hash,
originalUrl: hash,
quality: videoTitle,
};
}));
const numberOfLinks = preferences.get("number_of_links");
if (numberOfLinks == "all") {
return videos;
}
return videos.slice(0, parseInt(numberOfLinks))
}
sortVideos(videos) {
const preferences = new SharedPreferences();
const isDub = preferences.get("dubbed");
const isEfficient = preferences.get("efficient");
return videos.sort((a, b) => {
const regexMatchA = /\[(.+?) download\]/.test(a.quality);
const regexMatchB = /\[(.+?) download\]/.test(b.quality);
const isDubA = isDub && !a.quality.toLowerCase().includes("dubbed");
const isDubB = isDub && !b.quality.toLowerCase().includes("dubbed");
const isEfficientA = isEfficient && !["hevc", "265", "av1"].some(q => a.quality.toLowerCase().includes(q));
const isEfficientB = isEfficient && !["hevc", "265", "av1"].some(q => b.quality.toLowerCase().includes(q));
return (
regexMatchA - regexMatchB ||
isDubA - isDubB ||
isEfficientA - isEfficientB
);
});
}
getSourcePreferences() {
return [
{
"key": "number_of_links",
"listPreference": {
"title": "Number of links to load for video list",
"summary": "⚠️ Increasing the number of links will increase the loading time of the video list",
"valueIndex": 1,
"entries": [
"2",
"4",
"8",
"12",
"all"],
"entryValues": [
"2",
"4",
"8",
"12",
"all"],
}
},
{
"key": "provider_selection1",
"multiSelectListPreference": {
"title": "Enable/Disable Providers",
"summary": "",
"entries": [
"YTS",
"EZTV",
"RARBG",
"1337x",
"ThePirateBay",
"KickassTorrents",
"TorrentGalaxy",
"MagnetDL",
"HorribleSubs",
"NyaaSi",
"TokyoTosho",
"AniDex",
"🇷🇺 Rutor",
"🇷🇺 Rutracker",
"🇵🇹 Comando",
"🇵🇹 BluDV",
"🇫🇷 Torrent9",
"🇪🇸 MejorTorrent",
"🇲🇽 Cinecalidad"],
"entryValues": [
"yts",
"eztv",
"rarbg",
"1337x",
"thepiratebay",
"kickasstorrents",
"torrentgalaxy",
"magnetdl",
"horriblesubs",
"nyaasi",
"tokyotosho",
"anidex",
"rutor",
"rutracker",
"comando",
"bludv",
"torrent9",
"mejortorrent",
"cinecalidad"],
"values": [
"yts",
"eztv",
"rarbg",
"1337x",
"thepiratebay",
"kickasstorrents",
"torrentgalaxy",
"magnetdl",
"horriblesubs",
"nyaasi",
"tokyotosho",
"anidex",
"rutor",
"rutracker",
"comando",
"bludv",
"torrent9",
"mejortorrent",
"cinecalidad"]
}
},
{
"key": "quality_selection",
"multiSelectListPreference": {
"title": "Exclude Qualities/Resolutions",
"summary": "",
"entries": [
"BluRay REMUX",
"HDR/HDR10+/Dolby Vision",
"Dolby Vision",
"4k",
"1080p",
"720p",
"480p",
"Other (DVDRip/HDRip/BDRip...)",
"Screener",
"Cam",
"Unknown"],
"entryValues": [
"brremux",
"hdrall",
"dolbyvision",
"4k",
"1080p",
"720p",
"480p",
"other",
"scr",
"cam",
"unknown"],
"values": [
"720p",
"480p",
"other",
"scr",
"cam",
"unknown"]
}
},
{
"key": "lang_selection",
"multiSelectListPreference": {
"title": "Priority foreign language",
"summary": "",
"entries": [
"🇯🇵 Japanese",
"🇷🇺 Russian",
"🇮🇹 Italian",
"🇵🇹 Portuguese",
"🇪🇸 Spanish",
"🇲🇽 Latino",
"🇰🇷 Korean",
"🇨🇳 Chinese",
"🇹🇼 Taiwanese",
"🇫🇷 French",
"🇩🇪 German",
"🇳🇱 Dutch",
"🇮🇳 Hindi",
"🇮🇳 Telugu",
"🇮🇳 Tamil",
"🇵🇱 Polish",
"🇱🇹 Lithuanian",
"🇱🇻 Latvian",
"🇪🇪 Estonian",
"🇨🇿 Czech",
"🇸🇰 Slovakian",
"🇸🇮 Slovenian",
"🇭🇺 Hungarian",
"🇷🇴 Romanian",
"🇧🇬 Bulgarian",
"🇷🇸 Serbian",
"🇭🇷 Croatian",
"🇺🇦 Ukrainian",
"🇬🇷 Greek",
"🇩🇰 Danish",
"🇫🇮 Finnish",
"🇸🇪 Swedish",
"🇳🇴 Norwegian",
"🇹🇷 Turkish",
"🇸🇦 Arabic",
"🇮🇷 Persian",
"🇮🇱 Hebrew",
"🇻🇳 Vietnamese",
"🇮🇩 Indonesian",
"🇲🇾 Malay",
"🇹🇭 Thai",],
"entryValues": [
"japanese",
"russian",
"italian",
"portuguese",
"spanish",
"latino",
"korean",
"chinese",
"taiwanese",
"french",
"german",
"dutch",
"hindi",
"telugu",
"tamil",
"polish",
"lithuanian",
"latvian",
"estonian",
"czech",
"slovakian",
"slovenian",
"hungarian",
"romanian",
"bulgarian",
"serbian",
"croatian",
"ukrainian",
"greek",
"danish",
"finnish",
"swedish",
"norwegian",
"turkish",
"arabic",
"persian",
"hebrew",
"vietnamese",
"indonesian",
"malay",
"thai"],
"values": []
}
},
{
"key": "sorting_link",
"listPreference": {
"title": "Sorting",
"summary": "",
"valueIndex": 0,
"entries": [
"By quality then seeders",
"By quality then size",
"By seeders",
"By size"],
"entryValues": [
"quality",
"qualitysize",
"seeders",
"size"],
}
},
{
"key": "dubbed",
"switchPreferenceCompat": {
"title": "Dubbed Video Priority",
"summary": "",
"value": false
}
},
{
"key": "efficient",
"switchPreferenceCompat": {
"title": "Efficient Video Priority",
"summary": "Codec: (HEVC / x265) & AV1. High-quality video with less data usage.",
"value": false
}
},
{
"key": "jw_region1",
"listPreference": {
"title": "Catalogue Region",
"summary": "Region based catalogue recommendation.",
"valueIndex": 132,
"entries": [
"Albania", "Algeria", "Androrra", "Angola", "Antigua and Barbuda", "Argentina", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Barbados", "Belarus", "Belgium", "Belize", "Bermuda", "Bolivia", "Bosnia and Herzegovina", "Brazil", "Bulgaria", "Burkina Faso", "Cameroon", "Canada", "Cape Verde", "Chad", "Chile", "Colombia", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic", "DR Congo", "Denmark", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Estonia", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "Germany", "Ghana", "Gibraltar", "Greece", "Guatemala", "Guernsey", "Guyana", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast", "Jamaica", "Japan", "Jordan", "Kenya", "Kosovo", "Kuwait", "Latvia", "Lebanon", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Macedonia", "Madagascar", "Malawi", "Malaysia", "Mali", "Malta", "Mauritius", "Mexico", "Moldova", "Monaco", "Montenegro", "Morocco", "Mozambique", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Norway", "Oman", "Pakistan", "Palestine", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Saint Lucia", "San Marino", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Singapore", "Slovakia", "Slovenia", "South Africa", "South Korea", "Spain", "Sweden", "Switzerland", "Taiwan", "Tanzania", "Thailand", "Trinidad and Tobago", "Tunisia", "Turkey", "Turks and Caicos Islands", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", "Vatican City", "Venezuela", "Yemen", "Zambia", "Zimbabwe"],
"entryValues": [
"AL", "DZ", "AD", "AO", "AG", "AR", "AU", "AT", "AZ", "BS", "BH", "BB", "BY", "BE", "BZ", "BM", "BO", "BA", "BR", "BG", "BF", "CM", "CA", "CV", "TD", "CL", "CO", "CR", "HR", "CU", "CY", "CZ", "CD", "DK", "DO", "EC", "EG", "SV", "GQ", "EE", "FJ", "FI", "FR", "GF", "PF", "DE", "GH", "GI", "GR", "GT", "GG", "GY", "HN", "HK", "HU", "IS", "IN", "ID", "IQ", "IE", "IL", "IT", "CI", "JM", "JP", "JO", "KE", "XK", "KW", "LV", "LB", "LY", "LI", "LT", "LU", "MK", "MG", "MW", "MY", "ML", "MT", "MU", "MX", "MD", "MC", "ME", "MA", "MZ", "NL", "NZ", "NI", "NE", "NG", "NO", "OM", "PK", "PS", "PA", "PG", "PY", "PE", "PH", "PL", "PT", "QA", "RO", "RU", "LC", "SM", "SA", "SN", "RS", "SC", "SG", "SK", "SI", "ZA", "KR", "ES", "SE", "CH", "TW", "TZ", "TH", "TT", "TN", "TR", "TC", "UG", "UA", "AE", "UK", "US", "UY", "VA", "VE", "YE", "ZM", "ZW"],
}
},
{
"key": "jw_lang",
"listPreference": {
"title": "Poster and Titles Language",
"summary": "",
"valueIndex": 9,
"entries": [
"Arabic",
"Azerbaijani",
"Belarusian",
"Bulgarian",
"Bosnian",
"Catalan",
"Czech",
"German",
"Greek",
"English",
"English (U.S.A.)",
"Spanish",
"Spanish (Spain)",
"Spanish (Latinamerican)",
"Estonian",
"Finnish",
"French",
"French (Canada)",
"Hebrew",
"Croatian",
"Hungarian",
"Icelandic",
"Italian",
"Japanese",
"Korean",
"Lithuanian",
"Latvian",
"Macedonian",
"Maltese",
"Polish",
"Portuguese",
"Portuguese (Portugal)",
"Portuguese (Brazil)",
"Romanian",
"Russian",
"Slovakian",
"Slovenian",
"Albanian",
"Serbian",
"Swedish",
"Swahili",
"Turkish",
"Ukrainian",
"Urdu",
"Chinese"],
"entryValues": [
"ar",
"az",
"be",
"bg",
"bs",
"ca",
"cs",
"de",
"el",
"en",
"en-US",
"es",
"es-ES",
"es-LA",
"et",
"fi",
"fr",
"fr-CA",
"he",
"hr",
"hu",
"is",
"it",
"ja",
"ko",
"lt",
"lv",
"mk",
"mt",
"pl",
"pt",
"pt-PT",
"pt-BR",
"ro",
"ru",
"sk",
"sl",
"sq",
"sr",
"sv",
"sw",
"tr",
"uk",
"ur",
"zh"],
}
},
];
}
}

View File

@@ -1,631 +0,0 @@
const mangayomiSources = [{
"name": "Torrentio Anime (Torrent)",
"lang": "all",
"baseUrl": "https://torrentio.strem.fun",
"apiUrl": "",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.torrentio.png",
"typeSource": "torrent",
"isManga": false,
"itemType": 1,
"version": "0.0.2",
"pkgPath": "anime/src/all/torrentioanime.js"
}];
class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
anilistQuery() {
return `
query ($page: Int, $perPage: Int, $sort: [MediaSort], $search: String) {
Page(page: $page, perPage: $perPage) {
pageInfo {
currentPage
hasNextPage
}
media(type: ANIME, sort: $sort, search: $search, status_in: [RELEASING, FINISHED, NOT_YET_RELEASED]) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
`.trim();
}
anilistLatestQuery() {
const currentTimeInSeconds = Math.floor(Date.now() / 1000);
return `
query ($page: Int, $perPage: Int, $sort: [AiringSort]) {
Page(page: $page, perPage: $perPage) {
pageInfo {
currentPage
hasNextPage
}
airingSchedules(
airingAt_greater: 0
airingAt_lesser: ${currentTimeInSeconds - 10000}
sort: $sort
) {
media {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
}
`.trim();
}
async makeGraphQLRequest(query, variables) {
const res = await this.client.post("https://graphql.anilist.co", {},
{
query, variables
});
return res;
}
parseSearchJson(jsonLine, isLatestQuery = false) {
const jsonData = JSON.parse(jsonLine);
jsonData.type = isLatestQuery ? "AnilistMetaLatest" : "AnilistMeta";
const metaData = jsonData;
const mediaList = metaData.type == "AnilistMeta"
? metaData.data?.Page?.media || []
: metaData.data?.Page?.airingSchedules.map(schedule => schedule.media) || [];
const hasNextPage = metaData.type == "AnilistMeta" || metaData.type == "AnilistMetaLatest"
? metaData.data?.Page?.pageInfo?.hasNextPage || false
: false;
const animeList = mediaList
.filter(media => !((media?.countryOfOrigin === "CN" || media?.isAdult) && isLatestQuery))
.map(media => {
const anime = {};
anime.link = media?.id?.toString() || "";
anime.name = (() => {
const preferenceTitle = new SharedPreferences().get("pref_title")
switch (preferenceTitle) {
case "romaji":
return media?.title?.romaji || "";
case "english":
return media?.title?.english?.trim() || media?.title?.romaji || "";
case "native":
return media?.title?.native || "";
default:
return "";
}
})();
anime.imageUrl = media?.coverImage?.extraLarge || "";
return anime;
});
return { "list": animeList, "hasNextPage": hasNextPage };
}
async getPopular(page) {
const variables = JSON.stringify({
page: page,
perPage: 30,
sort: "TRENDING_DESC"
});
const res = await this.makeGraphQLRequest(this.anilistQuery(), variables);
return this.parseSearchJson(res.body)
}
async getLatestUpdates(page) {
const variables = JSON.stringify({
page: page,
perPage: 30,
sort: "TIME_DESC"
});
const res = await this.makeGraphQLRequest(this.anilistLatestQuery(), variables);
return this.parseSearchJson(res.body, true)
}
async search(query, page, filters) {
const variables = JSON.stringify({
page: page,
perPage: 30,
sort: "POPULARITY_DESC",
search: query
});
const res = await this.makeGraphQLRequest(this.anilistQuery(), variables);
return this.parseSearchJson(res.body)
}
async getDetail(url) {
const query = `
query($id: Int){
Media(id: $id){
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
`.trim();
const variables = JSON.stringify({ id: url });
const res = await this.makeGraphQLRequest(query, variables);
const media = JSON.parse(res.body).data.Media;
const anime = {};
anime.imageUrl = media?.coverImage?.extraLarge || "";
anime.description = (media?.description || "No Description")
.replace(/<br><br>/g, "\n")
.replace(/<.*?>/g, "");
anime.status = (() => {
switch (media?.status) {
case "RELEASING":
return 0;
case "FINISHED":
return 1;
case "HIATUS":
return 2;
case "NOT_YET_RELEASED":
return 3;
default:
return 5;
}
})();
const tagsList = media?.tags?.map(tag => tag.name).filter(Boolean) || [];
const genresList = media?.genres || [];
anime.genre = [...new Set([...tagsList, ...genresList])].sort();
const studiosList = media?.studios?.nodes?.map(node => node.name).filter(Boolean) || [];
anime.author = studiosList.sort().join(", ");
const response = await this.client.get(`https://api.ani.zip/mappings?anilist_id=${url}`);
const kitsuId = JSON.parse(response.body).mappings.kitsu_id.toString();
const responseEpisodes = await this.client.get(`https://anime-kitsu.strem.fun/meta/series/kitsu%3A${kitsuId}.json`);
const episodeList = JSON.parse(responseEpisodes.body);
anime.episodes = (() => {
switch (episodeList.meta?.type) {
case "series": {
const videos = episodeList.meta.videos;
return videos
.filter(video => video.thumbnail !== null && ((video.released ? new Date(video.released) : Date.now()) < Date.now()))
.map(video => {
const releaseDate = video.released ? new Date(video.released) : Date.now();
return {
url: `/stream/series/${video.id}.json`,
dateUpload: releaseDate.valueOf().toString(),
name: `Episode ${video.episode} : ${video.title
?.replace(/^Episode /, "")
?.replace(/^\d+\s*/, "")
?.trim()}`,
};
})
.reverse();
}
case "movie": {
const kitsuId = episodeList.meta.kitsuId;
return [
{
url: `/stream/movie/${kitsuId}.json`,
name: "Movie",
},
].reverse();
}
default:
return [];
}
})()
return anime;
}
appendQueryParam(key, values) {
let url = "";
if (values && values.length > 0) {
const filteredValues = Array.from(values).filter(value => value.trim() !== "").join(",");
url += `${key}=${filteredValues}|`;
}
return url;
};
async getVideoList(url) {
const preferences = new SharedPreferences();
let mainURL = `${this.source.baseUrl}/`;
mainURL += this.appendQueryParam("providers", preferences.get("provider_selection"));
mainURL += this.appendQueryParam("language", preferences.get("lang_selection"));
mainURL += this.appendQueryParam("qualityfilter", preferences.get("quality_selection"));
mainURL += this.appendQueryParam("sort", new Set([preferences.get("sorting_link")]));
mainURL += url;
mainURL = mainURL.replace(/\|$/, "");
const responseEpisodes = await this.client.get(mainURL);
const streamList = JSON.parse(responseEpisodes.body);
const animeTrackers = `
http://nyaa.tracker.wf:7777/announce,
http://anidex.moe:6969/announce,http://tracker.anirena.com:80/announce,
udp://tracker.uw0.xyz:6969/announce,
http://share.camoe.cn:8080/announce,
http://t.nyaatracker.com:80/announce,
udp://47.ip-51-68-199.eu:6969/announce,
udp://9.rarbg.me:2940,
udp://9.rarbg.to:2820,
udp://exodus.desync.com:6969/announce,
udp://explodie.org:6969/announce,
udp://ipv4.tracker.harry.lu:80/announce,
udp://open.stealth.si:80/announce,
udp://opentor.org:2710/announce,
udp://opentracker.i2p.rocks:6969/announce,
udp://retracker.lanta-net.ru:2710/announce,
udp://tracker.cyberia.is:6969/announce,
udp://tracker.dler.org:6969/announce,
udp://tracker.ds.is:6969/announce,
udp://tracker.internetwarriors.net:1337,
udp://tracker.openbittorrent.com:6969/announce,
udp://tracker.opentrackr.org:1337/announce,
udp://tracker.tiny-vps.com:6969/announce,
udp://tracker.torrent.eu.org:451/announce,
udp://valakas.rollo.dnsabr.com:2710/announce,
udp://www.torrent.eu.org:451/announce
`.split(",").map(tracker => tracker.trim()).filter(tracker => tracker);
const videos = this.sortVideos((streamList.streams || []).map(stream => {
const hash = `magnet:?xt=urn:btih:${stream.infoHash}&dn=${stream.infoHash}&tr=${animeTrackers.join("&tr=")}&index=${stream.fileIdx}`;
const videoTitle = `${(stream.name || "").replace("Torrentio\n", "")}\n${stream.title || ""}`.trim();
return {
url: hash,
originalUrl: hash,
quality: videoTitle,
};
}));
const numberOfLinks = preferences.get("number_of_links");
if (numberOfLinks == "all") {
return videos;
}
return videos.slice(0, parseInt(numberOfLinks))
}
sortVideos(videos) {
const preferences = new SharedPreferences();
const isDub = preferences.get("dubbed");
const isEfficient = preferences.get("efficient");
return videos.sort((a, b) => {
const regexMatchA = /\[(.+?) download\]/.test(a.quality);
const regexMatchB = /\[(.+?) download\]/.test(b.quality);
const isDubA = isDub && !a.quality.toLowerCase().includes("dubbed");
const isDubB = isDub && !b.quality.toLowerCase().includes("dubbed");
const isEfficientA = isEfficient && !["hevc", "265", "av1"].some(q => a.quality.toLowerCase().includes(q));
const isEfficientB = isEfficient && !["hevc", "265", "av1"].some(q => b.quality.toLowerCase().includes(q));
return (
regexMatchA - regexMatchB ||
isDubA - isDubB ||
isEfficientA - isEfficientB
);
});
}
getSourcePreferences() {
return [
{
"key": "number_of_links",
"listPreference": {
"title": "Number of links to load for video list",
"summary": "⚠️ Increasing the number of links will increase the loading time of the video list",
"valueIndex": 1,
"entries": [
"2",
"4",
"8",
"12",
"all"],
"entryValues": [
"2",
"4",
"8",
"12",
"all"],
}
},
{
"key": "provider_selection",
"multiSelectListPreference": {
"title": "Enable/Disable Providers",
"summary": "",
"entries": [
"YTS",
"EZTV",
"RARBG",
"1337x",
"ThePirateBay",
"KickassTorrents",
"TorrentGalaxy",
"MagnetDL",
"HorribleSubs",
"NyaaSi",
"TokyoTosho",
"AniDex",
"🇷🇺 Rutor",
"🇷🇺 Rutracker",
"🇵🇹 Comando",
"🇵🇹 BluDV",
"🇫🇷 Torrent9",
"🇪🇸 MejorTorrent",
"🇲🇽 Cinecalidad"],
"entryValues": [
"yts",
"eztv",
"rarbg",
"1337x",
"thepiratebay",
"kickasstorrents",
"torrentgalaxy",
"magnetdl",
"horriblesubs",
"nyaasi",
"tokyotosho",
"anidex",
"rutor",
"rutracker",
"comando",
"bludv",
"torrent9",
"mejortorrent",
"cinecalidad"],
"values": [
"nyaasi",]
}
},
{
"key": "quality_selection",
"multiSelectListPreference": {
"title": "Exclude Qualities/Resolutions",
"summary": "",
"entries": [
"BluRay REMUX",
"HDR/HDR10+/Dolby Vision",
"Dolby Vision",
"4k",
"1080p",
"720p",
"480p",
"Other (DVDRip/HDRip/BDRip...)",
"Screener",
"Cam",
"Unknown"],
"entryValues": [
"brremux",
"hdrall",
"dolbyvision",
"4k",
"1080p",
"720p",
"480p",
"other",
"scr",
"cam",
"unknown"],
"values": [
"720p",
"480p",
"other",
"scr",
"cam",
"unknown"]
}
},
{
"key": "lang_selection",
"multiSelectListPreference": {
"title": "Priority foreign language",
"summary": "",
"entries": [
"🇯🇵 Japanese",
"🇷🇺 Russian",
"🇮🇹 Italian",
"🇵🇹 Portuguese",
"🇪🇸 Spanish",
"🇲🇽 Latino",
"🇰🇷 Korean",
"🇨🇳 Chinese",
"🇹🇼 Taiwanese",
"🇫🇷 French",
"🇩🇪 German",
"🇳🇱 Dutch",
"🇮🇳 Hindi",
"🇮🇳 Telugu",
"🇮🇳 Tamil",
"🇵🇱 Polish",
"🇱🇹 Lithuanian",
"🇱🇻 Latvian",
"🇪🇪 Estonian",
"🇨🇿 Czech",
"🇸🇰 Slovakian",
"🇸🇮 Slovenian",
"🇭🇺 Hungarian",
"🇷🇴 Romanian",
"🇧🇬 Bulgarian",
"🇷🇸 Serbian",
"🇭🇷 Croatian",
"🇺🇦 Ukrainian",
"🇬🇷 Greek",
"🇩🇰 Danish",
"🇫🇮 Finnish",
"🇸🇪 Swedish",
"🇳🇴 Norwegian",
"🇹🇷 Turkish",
"🇸🇦 Arabic",
"🇮🇷 Persian",
"🇮🇱 Hebrew",
"🇻🇳 Vietnamese",
"🇮🇩 Indonesian",
"🇲🇾 Malay",
"🇹🇭 Thai",],
"entryValues": [
"japanese",
"russian",
"italian",
"portuguese",
"spanish",
"latino",
"korean",
"chinese",
"taiwanese",
"french",
"german",
"dutch",
"hindi",
"telugu",
"tamil",
"polish",
"lithuanian",
"latvian",
"estonian",
"czech",
"slovakian",
"slovenian",
"hungarian",
"romanian",
"bulgarian",
"serbian",
"croatian",
"ukrainian",
"greek",
"danish",
"finnish",
"swedish",
"norwegian",
"turkish",
"arabic",
"persian",
"hebrew",
"vietnamese",
"indonesian",
"malay",
"thai"],
"values": []
}
},
{
"key": "sorting_link",
"listPreference": {
"title": "Sorting",
"summary": "",
"valueIndex": 0,
"entries": [
"By quality then seeders",
"By quality then size",
"By seeders",
"By size"],
"entryValues": [
"quality",
"qualitysize",
"seeders",
"size"],
}
},
{
"key": "pref_title",
"listPreference": {
"title": "Preferred Title",
"summary": "",
"valueIndex": 0,
"entries": [
"Romaji",
"English",
"Native"],
"entryValues": [
"romaji",
"english",
"native"],
}
},
{
"key": "dubbed",
"switchPreferenceCompat": {
"title": "Dubbed Video Priority",
"summary": "",
"value": false
}
},
{
"key": "efficient",
"switchPreferenceCompat": {
"title": "Efficient Video Priority",
"summary": "Codec: (HEVC / x265) & AV1. High-quality video with less data usage.",
"value": false
}
}
];
}
}

View File

@@ -1,933 +0,0 @@
const mangayomiSources = [{
"name": "AniWorld",
"lang": "de",
"baseUrl": "https://aniworld.to",
"apiUrl": "",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/de.aniworld.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.3.8",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/de/aniworld.js"
}];
class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
async getPopular(page) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(`${baseUrl}/beliebte-animes`);
const elements = new Document(res.body).select("div.seriesListContainer div");
const list = [];
for (const element of elements) {
const linkElement = element.selectFirst("a");
const name = element.selectFirst("h3").text;
const imageUrl = baseUrl + linkElement.selectFirst("img").attr("data-src");
const link = linkElement.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
async getLatestUpdates(page) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(`${baseUrl}/neu`);
const elements = new Document(res.body).select("div.seriesListContainer div");
const list = [];
for (const element of elements) {
const linkElement = element.selectFirst("a");
const name = element.selectFirst("h3").text;
const imageUrl = baseUrl + linkElement.selectFirst("img").attr("data-src");
const link = linkElement.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
async search(query, page, filters) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(`${baseUrl}/animes`);
const elements = new Document(res.body).select("#seriesContainer > div > ul > li > a").filter(e => e.attr("title").toLowerCase().includes(query.toLowerCase()));
const list = [];
for (const element of elements) {
const name = element.text;
const link = element.attr("href");
const img = new Document((await this.client.get(baseUrl + link)).body).selectFirst("div.seriesCoverBox img").attr("data-src");
const imageUrl = baseUrl + img;
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
cleanHtmlString(input) {
if (!input) return "";
return input
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/<br>/g, '\n')
.replace(/<br\s*\/?>/g, '\n')
.replace(/&#039;/g, "'")
.replace(/&quot;/g, '"')
.replace(/<a\s+href="([^"]*)".*?>.*?<\/a>/g, '$1');
}
/**
* Custom asyncPool implementation.
* @param {number} poolLimit - Maximum number of concurrent promises.
* @param {Array} array - Array of items to process.
* @param {function} iteratorFn - Function that returns a promise for each item.
* @returns {Promise<Array>} - Promise resolving to an array of results.
*/
async asyncPool(poolLimit, array, iteratorFn) {
const ret = []; // Array to store all promises
const executing = []; // Array to store currently executing promises
for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item));
ret.push(p);
// When poolLimit is reached, wait for the fastest promise to complete
if (poolLimit <= array.length) {
const e = p.then(() => {
// Remove the promise from executing array once it resolves
executing.splice(executing.indexOf(e), 1);
});
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}
return Promise.all(ret);
}
async getDetail(url) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(baseUrl + url);
const document = new Document(res.body);
const imageUrl = baseUrl +
document.selectFirst("div.seriesCoverBox img").attr("data-src");
const name = document.selectFirst("div.series-title h1 span").text;
const genre = document.select("div.genres ul li").map(e => e.text).filter(text => !/^\+\s\d+$/.test(text));
const description = this.cleanHtmlString(document.selectFirst("p.seri_des").attr("data-full-description"));
const produzent = document.select("div.cast li")
.filter(e => e.outerHtml.includes("Produzent:"));
let author = "";
if (produzent.length > 0) {
author = produzent[0]
.select("li")
.map((e) => e.text)
.filter((text) => !/^\s\&\s\d+\sweitere$/.test(text))
.join(", ");
}
const seasonsElements = document.select("#stream > ul:nth-child(1) > li > a");
// Use asyncPool to limit concurrency while processing seasons
const episodesArrays = await this.asyncPool(2, seasonsElements, element => this.parseEpisodesFromSeries(element));
// Flatten the resulting arrays and reverse the order
const episodes = episodesArrays.flat().reverse();
return { name, imageUrl, description, author, status: 5, genre, episodes };
}
async parseEpisodesFromSeries(element) {
const seasonId = element.getHref;
const res = await this.client.get(this.source.baseUrl + seasonId);
const episodeElements = new Document(res.body).select("table.seasonEpisodesList tbody tr");
// Use asyncPool to limit concurrency while processing episodes of a season
return await this.asyncPool(13, episodeElements, e => this.episodeFromElement(e));
}
async episodeFromElement(element) {
const titleAnchor = element.selectFirst("td.seasonEpisodeTitle a");
const episodeSpan = titleAnchor.selectFirst("span");
const url = titleAnchor.attr("href");
const episodeSeasonId = element.attr("data-episode-season-id");
let episode = this.cleanHtmlString(episodeSpan.text);
let name = "";
if (url.includes("/film")) {
name = `Film ${episodeSeasonId} : ${episode}`;
} else {
const seasonMatch = url.match(/staffel-(\d+)\/episode/);
name = `Staffel ${seasonMatch[1]} Folge ${episodeSeasonId} : ${episode}`;
}
return name && url ? { name, url } : {};
}
async getVideoList(url) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(baseUrl + url, {
'Accept': '*/*',
'Referer': baseUrl + url,
'Priority': 'u=0, i',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0'
});
const document = new Document(res.body);
let promises = [];
const videos = [];
const preferences = new SharedPreferences();
const hostFilter = preferences.get("host_filter");
const langFilter = preferences.get("lang_filter");
const redirectsElements = document.select("ul.row li");
const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false });
for (const element of redirectsElements) {
const langkey = element.attr("data-lang-key");
const lang = (langkey == 1 || langkey == 3) ? 'Deutscher' : 'Englischer';
const type = (langkey == 1) ? 'Dub' : 'Sub';
const host = element.selectFirst("a h4").text;
if (hostFilter.includes(host) && langFilter.includes(`${lang} ${type}`)) {
const redirect = baseUrl + element.selectFirst("a.watchEpisode").attr("href");
promises.push((async (redirect, lang, type, host) => {
const location = (await dartClient.get(redirect)).headers.location;
return await extractAny(location, host.toLowerCase(), lang, type, host, { 'Referer': this.source.baseUrl });
})(redirect, lang, type, host));
}
}
for (const p of (await Promise.allSettled(promises))) {
if (p.status == 'fulfilled') {
videos.push.apply(videos, p.value);
}
}
return sortVideos(videos);
}
getSourcePreferences() {
const languages = ['Deutsch', 'Englisch'];
const languageValues = ['Deutscher', 'Englischer'];
const types = ['Dub', 'Sub'];
const resolutions = ['1080p', '720p', '480p'];
const hosts = ['Doodstream', 'Filemoon', 'Luluvdo', 'SpeedFiles', 'Streamtape', 'Vidmoly', 'Vidoza', 'VOE'];
const languageFilters = [];
for (const lang of languageValues) {
for (const type of types) {
languageFilters.push(`${lang} ${type}`);
}
}
return [
{
key: 'lang',
listPreference: {
title: 'Bevorzugte Sprache',
summary: 'Wenn verfügbar, wird diese Sprache ausgewählt. Priority = 0 (lower is better)',
valueIndex: 0,
entries: languages,
entryValues: languageValues
}
},
{
key: 'type',
listPreference: {
title: 'Bevorzugter Typ',
summary: 'Wenn verfügbar, wird dieser Typ ausgewählt. Priority = 1 (lower is better)',
valueIndex: 0,
entries: types,
entryValues: types
}
},
{
key: 'res',
listPreference: {
title: 'Bevorzugte Auflösung',
summary: 'Wenn verfügbar, wird diese Auflösung ausgewählt. Priority = 2 (lower is better)',
valueIndex: 0,
entries: resolutions,
entryValues: resolutions
}
},
{
key: 'host',
listPreference: {
title: 'Bevorzugter Hoster',
summary: 'Wenn verfügbar, wird dieser Hoster ausgewählt. Priority = 3 (lower is better)',
valueIndex: 0,
entries: hosts,
entryValues: hosts
}
},
{
key: "lang_filter",
multiSelectListPreference: {
title: "Sprachen auswählen",
summary: "Wähle aus welche Sprachen dir angezeigt werden sollen. Weniger streams zu laden beschleunigt den Start der Videos.",
entries: languageFilters,
entryValues: languageFilters,
values: languageFilters
}
},
{
key: "host_filter",
multiSelectListPreference: {
title: "Hoster auswählen",
summary: "Wähle aus welche Hoster dir angezeigt werden sollen. Weniger streams zu laden beschleunigt den Start der Videos.",
entries: hosts,
entryValues: hosts,
values: hosts
}
}
];
}
}
/***************************************************************************************************
*
* mangayomi-js-helpers v1.2
*
* # Video Extractors
* - vidGuardExtractor
* - doodExtractor
* - vidozaExtractor
* - okruExtractor
* - amazonExtractor
* - vidHideExtractor
* - filemoonExtractor
* - mixdropExtractor
* - speedfilesExtractor
* - luluvdoExtractor
* - burstcloudExtractor (not working, see description)
*
* # Video Extractor Wrappers
* - streamWishExtractor
* - voeExtractor
* - mp4UploadExtractor
* - yourUploadExtractor
* - streamTapeExtractor
* - sendVidExtractor
*
* # Video Extractor helpers
* - extractAny
*
* # Playlist Extractors
* - m3u8Extractor
* - jwplayerExtractor
*
* # Extension Helpers
* - sortVideos()
*
* # Uint8Array
* - Uint8Array.fromBase64()
* - Uint8Array.prototype.toBase64()
* - Uint8Array.prototype.decode()
*
* # String
* - String.prototype.encode()
* - String.decode()
* - String.prototype.reverse()
* - String.prototype.swapcase()
* - getRandomString()
*
* # Encode/Decode Functions
* - decodeUTF8
* - encodeUTF8
*
* # Url
* - absUrl()
*
***************************************************************************************************/
//--------------------------------------------------------------------------------------------------
// Video Extractors
//--------------------------------------------------------------------------------------------------
async function vidGuardExtractor(url) {
// get html
const res = await new Client().get(url);
const doc = new Document(res.body);
const script = doc.selectFirst('script:contains(eval)');
// eval code
const code = script.text;
eval?.('var window = {};');
eval?.(code);
const playlistUrl = globalThis.window.svg.stream;
// decode sig
const encoded = playlistUrl.match(/sig=(.*?)&/)[1];
const charCodes = [];
for (let i = 0; i < encoded.length; i += 2) {
charCodes.push(parseInt(encoded.slice(i, i + 2), 16) ^ 2);
}
let decoded = Uint8Array.fromBase64(
String.fromCharCode(...charCodes))
.slice(5, -5)
.reverse();
for (let i = 0; i < decoded.length; i += 2) {
let tmp = decoded[i];
decoded[i] = decoded[i + 1];
decoded[i + 1] = tmp;
}
decoded = decoded.decode();
return await m3u8Extractor(playlistUrl.replace(encoded, decoded), null);
}
async function doodExtractor(url) {
const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false });
let response = await dartClient.get(url);
while ("location" in response.headers) {
response = await dartClient.get(response.headers.location);
}
const newUrl = response.request.url;
const doodhost = newUrl.match(/https:\/\/(.*?)\//, newUrl)[0].slice(8, -1);
const md5 = response.body.match(/'\/pass_md5\/(.*?)',/, newUrl)[0].slice(11, -2);
const token = md5.substring(md5.lastIndexOf("/") + 1);
const expiry = new Date().valueOf();
const randomString = getRandomString(10);
response = await new Client().get(`https://${doodhost}/pass_md5/${md5}`, { "Referer": newUrl });
const videoUrl = `${response.body}${randomString}?token=${token}&expiry=${expiry}`;
const headers = { "User-Agent": "Mangayomi", "Referer": doodhost };
return [{ url: videoUrl, originalUrl: videoUrl, headers: headers, quality: '' }];
}
async function vidmolyExtractor(url) {
const res = await new Client({ 'useDartHttpClient': true, "followRedirects": true }).get(url);
const playlistUrl = res.body.match(/https:\/\/\S*\.m3u8/)[0];
return await m3u8Extractor(playlistUrl, {
'Referer': 'https://vidmoly.to',
'Origin': 'https://vidmoly.to'
});
}
async function vidozaExtractor(url) {
let response = await new Client({ 'useDartHttpClient': true, "followRedirects": true }).get(url);
const videoUrl = response.body.match(/https:\/\/\S*\.mp4/)[0];
return [{ url: videoUrl, originalUrl: videoUrl, quality: '' }];
}
async function okruExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const tag = doc.selectFirst('div[data-options]');
const playlistUrl = tag.attr('data-options').match(/hlsManifestUrl.*?(h.*?id=\d+)/)[1].replaceAll('\\\\u0026', '&');
return await m3u8Extractor(playlistUrl, null);
}
async function amazonExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const videoUrl = doc.selectFirst('video').getSrc;
return videoUrl ? [{ url: videoUrl, originalUrl: videoUrl, headers: null, quality: '' }] : [];
}
async function vidHideExtractor(url) {
const res = await new Client().get(url);
return await jwplayerExtractor(res.body);
}
async function filemoonExtractor(url, headers) {
headers = headers ?? {};
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
delete headers['user-agent'];
let res = await new Client().get(url, headers);
const src = res.body.match(/iframe src="(.*?)"/)?.[1];
if (src) {
res = await new Client().get(src, {
'Referer': url,
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'User-Agent': headers['User-Agent']
});
}
return await jwplayerExtractor(res.body, headers);
}
async function mixdropExtractor(url) {
headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' };
let res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(url, headers);
while ("location" in res.headers) {
res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(res.headers.location, headers);
}
const newUrl = res.request.url;
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(MDCore):contains(eval)').text;
const unpacked = unpackJs(code);
let videoUrl = unpacked.match(/wurl="(.*?)"/)?.[1];
if (!videoUrl) return [];
videoUrl = 'https:' + videoUrl;
headers.referer = newUrl;
return [{ url: videoUrl, originalUrl: videoUrl, quality: '', headers: headers }];
}
async function speedfilesExtractor(url) {
let res = await new Client().get(url);
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(var)').text;
let b64;
// Get b64
for (const match of code.matchAll(/(?:var|let|const)\s*\w+\s*=\s*["']([^"']+)/g)) {
if (match[1].match(/[g-zG-Z]/)) {
b64 = match[1];
break;
}
}
// decode b64 => b64
const step1 = Uint8Array.fromBase64(b64).reverse().decode().swapcase();
// decode b64 => hex
const step2 = Uint8Array.fromBase64(step1).reverse().decode();
// decode hex => b64
let step3 = [];
for (let i = 0; i < step2.length; i += 2) {
step3.push(parseInt(step2.slice(i, i + 2), 16) - 3);
}
step3 = String.fromCharCode(...step3.reverse()).swapcase();
// decode b64 => url
const videoUrl = Uint8Array.fromBase64(step3).decode();
return [{ url: videoUrl, originalUrl: videoUrl, quality: '', headers: null }];
}
async function luluvdoExtractor(url) {
const client = new Client();
const match = url.match(/(.*?:\/\/.*?)\/.*\/(.*)/);
const headers = { 'user-agent': 'Mangayomi' };
const res = await client.get(`${match[1]}/dl?op=embed&file_code=${match[2]}`, headers);
return await jwplayerExtractor(res.body, headers);
}
/** Does not work: Client always sets 'charset=utf-8' in Content-Type. */
async function burstcloudExtractor(url) {
let client = new Client();
let res = await client.get(url);
const id = res.body.match(/data-file-id="(.*?)"/)[1];
const headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
'Referer': url,
};
const data = {
'fileId': id
};
res = await client.post(`https://www.burstcloud.co/file/play-request/`, headers, data);
const videoUrl = res.body.match(/cdnUrl":"(.*?)"/)[1];
return [{
url: videoUrl,
originalUrl: videoUrl,
headers: { 'Referer': url.match(/.*?:\/\/.*?\//) },
quality: ''
}];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Wrappers
//--------------------------------------------------------------------------------------------------
_streamWishExtractor = streamWishExtractor;
streamWishExtractor = async (url) => {
return (await _streamWishExtractor(url, '')).map(v => {
v.quality = v.quality.slice(3, -1);
return v;
});
}
_voeExtractor = voeExtractor;
voeExtractor = async (url) => {
return (await _voeExtractor(url, '')).map(v => {
v.quality = v.quality.replace(/Voe: (\d+p?)/i, '$1');
return v;
});
}
_mp4UploadExtractor = mp4UploadExtractor;
mp4UploadExtractor = async (url) => {
return (await _mp4UploadExtractor(url)).map(v => {
v.quality = v.quality.match(/\d+p/)?.[0] ?? '';
return v;
});
}
_yourUploadExtractor = yourUploadExtractor;
yourUploadExtractor = async (url) => {
return (await _yourUploadExtractor(url))
.filter(v => !v.url.includes('/novideo'))
.map(v => {
v.quality = '';
return v;
});
}
_streamTapeExtractor = streamTapeExtractor;
streamTapeExtractor = async (url) => {
return await _streamTapeExtractor(url, '');
}
_sendVidExtractor = sendVidExtractor;
sendVidExtractor = async (url) => {
let res = await new Client().get(url);
var videoUrl, quality;
try {
videoUrl = res.body.match(/og:video" content="(.*?\.mp4.*?)"/)[1];
quality = res.body.match(/og:video:height" content="(.*?)"/)?.[1];
quality = quality ? quality + 'p' : '';
} catch (error) {
}
if (!videoUrl) {
return _sendVidExtractor(url, null, '');
}
return [{ url: videoUrl, originalUrl: videoUrl, quality: quality, headers: null }];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Helpers
//--------------------------------------------------------------------------------------------------
async function extractAny(url, method, lang, type, host, headers = null) {
const m = extractAny.methods[method];
return (!m) ? [] : (await m(url, headers)).map(v => {
v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`;
return v;
});
};
extractAny.methods = {
'amazon': amazonExtractor,
'burstcloud': burstcloudExtractor,
'doodstream': doodExtractor,
'filemoon': filemoonExtractor,
'luluvdo': luluvdoExtractor,
'mixdrop': mixdropExtractor,
'mp4upload': mp4UploadExtractor,
'okru': okruExtractor,
'sendvid': sendVidExtractor,
'speedfiles': speedfilesExtractor,
'streamtape': streamTapeExtractor,
'streamwish': vidHideExtractor,
'vidguard': vidGuardExtractor,
'vidhide': vidHideExtractor,
'vidmoly': vidmolyExtractor,
'vidoza': vidozaExtractor,
'voe': voeExtractor,
'yourupload': yourUploadExtractor
};
//--------------------------------------------------------------------------------------------------
// Playlist Extractors
//--------------------------------------------------------------------------------------------------
async function m3u8Extractor(url, headers = null) {
// https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist
// https://developer.apple.com/documentation/http-live-streaming/adding-alternate-media-to-a-playlist
// define attribute lists
const streamAttributes = [
['avg_bandwidth', /AVERAGE-BANDWIDTH=(\d+)/],
['bandwidth', /\bBANDWIDTH=(\d+)/],
['resolution', /\bRESOLUTION=([\dx]+)/],
['framerate', /\bFRAME-RATE=([\d\.]+)/],
['codecs', /\bCODECS="(.*?)"/],
['video', /\bVIDEO="(.*?)"/],
['audio', /\bAUDIO="(.*?)"/],
['subtitles', /\bSUBTITLES="(.*?)"/],
['captions', /\bCLOSED-CAPTIONS="(.*?)"/]
];
const mediaAttributes = [
['type', /\bTYPE=([\w-]*)/],
['group', /\bGROUP-ID="(.*?)"/],
['lang', /\bLANGUAGE="(.*?)"/],
['name', /\bNAME="(.*?)"/],
['autoselect', /\bAUTOSELECT=(\w*)/],
['default', /\bDEFAULT=(\w*)/],
['instream-id', /\bINSTREAM-ID="(.*?)"/],
['assoc-lang', /\bASSOC-LANGUAGE="(.*?)"/],
['channels', /\bCHANNELS="(.*?)"/],
['uri', /\bURI="(.*?)"/]
];
const streams = [], videos = {}, audios = {}, subtitles = {}, captions = {};
const dict = { 'VIDEO': videos, 'AUDIO': audios, 'SUBTITLES': subtitles, 'CLOSED-CAPTIONS': captions };
const res = await new Client().get(url, headers);
const text = res.body;
if (res.statusCode != 200) {
return [];
}
// collect media
for (const match of text.matchAll(/#EXT-X-MEDIA:(.*)/g)) {
const info = match[1], medium = {};
for (const attr of mediaAttributes) {
const m = info.match(attr[1]);
medium[attr[0]] = m ? m[1] : null;
}
const type = medium.type;
delete medium.type;
const group = medium.group;
delete medium.group;
const typedict = dict[type];
if (typedict[group] == undefined)
typedict[group] = [];
typedict[group].push(medium);
}
// collect streams
for (const match of text.matchAll(/#EXT-X-STREAM-INF:(.*)\s*(.*)/g)) {
const info = match[1], stream = { 'url': absUrl(match[2], url) };
for (const attr of streamAttributes) {
const m = info.match(attr[1]);
stream[attr[0]] = m ? m[1] : null;
}
stream['video'] = videos[stream.video] ?? null;
stream['audio'] = audios[stream.audio] ?? null;
stream['subtitles'] = subtitles[stream.subtitles] ?? null;
stream['captions'] = captions[stream.captions] ?? null;
// format resolution or bandwidth
let quality;
if (stream.resolution) {
quality = stream.resolution.match(/x(\d+)/)[1] + 'p';
} else {
quality = (parseInt(stream.avg_bandwidth ?? stream.bandwidth) / 1000000) + 'Mb/s'
}
// add stream to list
const subs = stream.subtitles?.map((s) => {
return { file: s.uri, label: s.name };
});
const auds = stream.audio?.map((a) => {
return { file: a.uri, label: a.name };
});
streams.push({
url: stream.url,
quality: quality,
originalUrl: stream.url,
headers: headers,
subtitles: subs ?? null,
audios: auds ?? null
});
}
return streams.length ? streams : [{
url: url,
quality: '',
originalUrl: url,
headers: headers,
subtitles: null,
audios: null
}];
}
async function jwplayerExtractor(text, headers) {
// https://docs.jwplayer.com/players/reference/playlists
const getsetup = /setup\(({[\s\S]*?})\)/;
const getsources = /sources:\s*(\[[\s\S]*?\])/;
const gettracks = /tracks:\s*(\[[\s\S]*?\])/;
const unpacked = unpackJs(text);
const videos = [], subtitles = [];
const data = eval('(' + (getsetup.exec(text) || getsetup.exec(unpacked))?.[1] + ')');
if (data) {
var sources = data.sources;
var tracks = data.tracks;
} else {
var sources = eval('(' + (getsources.exec(text) || getsources.exec(unpacked))?.[1] + ')');
var tracks = eval('(' + (gettracks.exec(text) || gettracks.exec(unpacked))?.[1] + ')');
}
for (t of tracks) {
if (t.type == "captions") {
subtitles.push({ file: t.file, label: t.label });
}
}
for (s of sources) {
if (s.file.includes('master.m3u8')) {
videos.push(...(await m3u8Extractor(s.file, headers)));
} else if (s.file.includes('.mpd')) {
} else {
videos.push({ url: s.file, originalUrl: s.file, quality: '', headers: headers });
}
}
return videos.map(v => {
v.subtitles = subtitles;
return v;
});
}
//--------------------------------------------------------------------------------------------------
// Extension Helpers
//--------------------------------------------------------------------------------------------------
function sortVideos(videos) {
const pref = new SharedPreferences();
const getres = RegExp('(\\d+)p?', 'i');
const lang = RegExp(pref.get('lang'), 'i');
const type = RegExp(pref.get('type'), 'i');
const res = RegExp(getres.exec(pref.get('res'))[1], 'i');
const host = RegExp(pref.get('host'), 'i');
let getScore = (q, hasRes) => {
const bLang = lang.test(q), bType = type.test(q), bRes = res.test(q), bHost = host.test(q);
if (hasRes) {
return bLang * (8 + bType * (4 + bRes * (2 + bHost * 1)));
} else {
return bLang * (8 + bType * (4 + (bHost * 3)));
}
}
return videos.sort((a, b) => {
const resA = getres.exec(a.quality)?.[1];
const resB = getres.exec(b.quality)?.[1];
const score = getScore(b.quality, resB) - getScore(a.quality, resA);
if (score) return score;
const qA = resA ? a.quality.replace(resA, (9999 - parseInt(resA)).toString()) : a.quality;
const qB = resA ? b.quality.replace(resB, (9999 - parseInt(resB)).toString()) : b.quality;
return qA.localeCompare(qB);
});
}
//--------------------------------------------------------------------------------------------------
// Uint8Array
//--------------------------------------------------------------------------------------------------
Uint8Array.fromBase64 = function (b64) {
// [00,01,02,03,04,05,06,07,08,\t,\n,0b,0c,\r,0e,0f,10,11,12,13,14,15,16,17,18,19,1a,1b,1c,1d,1e,1f,' ', !, ", #, $, %, &, ', (, ), *, +,',', -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <,'=', >, ?, @,A,B,C,D,E,F,G,H,I,J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~,7f]
const m = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1]
let data = [], val = 0, bits = -8
for (const c of b64) {
let n = m[c.charCodeAt(0)];
if (n == -1) break;
val = (val << 6) + n;
bits += 6;
for (; bits >= 0; bits -= 8)
data.push((val >> bits) & 0xFF);
}
return new Uint8Array(data);
}
Uint8Array.prototype.toBase64 = function () {
const m = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
let b64 = '', val = 0, bits = -6;
for (const b of this) {
val = (val << 8) + b;
bits += 8;
while (bits >= 0) {
b64 += m[(val >> bits) & 0x3F];
bits -= 6;
}
}
if (bits > -6)
b64 += m[(val << -bits) & 0x3F];
return b64 + ['', '', '==', '='][b64.length % 4];
}
Uint8Array.prototype.decode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(this);
}
return null;
}
//--------------------------------------------------------------------------------------------------
// String
//--------------------------------------------------------------------------------------------------
String.prototype.encode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return encodeUTF8(this);
}
return null;
}
String.decode = function (data, encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(data);
}
return null;
}
String.prototype.reverse = function () {
return this.split('').reverse().join('');
}
String.prototype.swapcase = function () {
const isAsciiLetter = /[A-z]/;
const result = [];
for (const l of this)
result.push(isAsciiLetter.test(l) ? String.fromCharCode(l.charCodeAt() ^ 32) : l);
return result.join('');
}
function getRandomString(length) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
let result = "";
for (let i = 0; i < length; i++) {
const random = Math.floor(Math.random() * 61);
result += chars[random];
}
return result;
}
//--------------------------------------------------------------------------------------------------
// Encode/Decode Functions
//--------------------------------------------------------------------------------------------------
function decodeUTF8(data) {
const codes = [];
for (let i = 0; i < data.length;) {
const c = data[i++];
const len = (c > 0xBF) + (c > 0xDF) + (c > 0xEF);
let val = c & (0xFF >> (len + 1));
for (const end = i + len; i < end; i++) {
val = (val << 6) + (data[i] & 0x3F);
}
codes.push(val);
}
return String.fromCharCode(...codes);
}
function encodeUTF8(string) {
const data = [];
for (const c of string) {
const code = c.charCodeAt(0);
const len = (code > 0x7F) + (code > 0x7FF) + (code > 0xFFFF);
let bits = len * 6;
data.push((len ? ~(0xFF >> len + 1) : (0)) + (code >> bits));
while (bits > 0) {
data.push(0x80 + ((code >> (bits -= 6)) & 0x3F))
}
}
return new Uint8Array(data);
}
//--------------------------------------------------------------------------------------------------
// Url
//--------------------------------------------------------------------------------------------------
function absUrl(url, base) {
if (url.search(/^\w+:\/\//) == 0) {
return url;
} else if (url.startsWith('/')) {
return base.slice(0, base.lastIndexOf('/')) + url;
} else {
return base.slice(0, base.lastIndexOf('/') + 1) + url;
}
}

View File

@@ -1,964 +0,0 @@
const mangayomiSources = [{
"name": "SerienStream",
"lang": "de",
"baseUrl": "https://s.to",
"apiUrl": "",
"iconUrl": "https://s.to/favicon.ico",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.9",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/de/serienstream.js"
}];
class DefaultExtension extends MProvider {
constructor () {
super();
this.client = new Client();
}
async getPopular(page) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(`${baseUrl}/beliebte-serien`);
const elements = new Document(res.body).select("div.seriesListContainer div");
const list = [];
for (const element of elements) {
const linkElement = element.selectFirst("a");
const name = element.selectFirst("h3").text;
const imageUrl = baseUrl + linkElement.selectFirst("img").attr("data-src");
const link = linkElement.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
async getLatestUpdates(page) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(`${baseUrl}/neu`);
const elements = new Document(res.body).select("div.seriesListContainer div");
const list = [];
for (const element of elements) {
const linkElement = element.selectFirst("a");
const name = element.selectFirst("h3").text;
const imageUrl = baseUrl + linkElement.selectFirst("img").attr("data-src");
const link = linkElement.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
async search(query, page, filters) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(`${baseUrl}/serien`);
const elements = new Document(res.body).select("#seriesContainer > div > ul > li > a").filter(e => e.attr("title").toLowerCase().includes(query.toLowerCase()));
const list = [];
for (const element of elements) {
const name = element.text;
const link = element.attr("href");
const img = new Document((await this.client.get(baseUrl + link)).body).selectFirst("div.seriesCoverBox img").attr("data-src");
const imageUrl = baseUrl + img;
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
cleanHtmlString(input) {
if (!input) return "";
return input
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/<br>/g, '\n')
.replace(/<br\s*\/?>/g, '\n')
.replace(/&#039;/g, "'")
.replace(/&quot;/g, '"')
.replace(/<a\s+href="([^"]*)".*?>.*?<\/a>/g, '$1');
}
/**
* Custom asyncPool implementation.
* @param {number} poolLimit - Maximum number of concurrent promises.
* @param {Array} array - Array of items to process.
* @param {function} iteratorFn - Function that returns a promise for each item.
* @returns {Promise<Array>} - Promise resolving to an array of results.
*/
async asyncPool(poolLimit, array, iteratorFn) {
const ret = []; // Array to store all promises
const executing = []; // Array to store currently executing promises
for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item));
ret.push(p);
// When poolLimit is reached, wait for the fastest promise to complete
if (poolLimit <= array.length) {
const e = p.then(() => {
// Remove the promise from executing array once it resolves
executing.splice(executing.indexOf(e), 1);
});
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}
return Promise.all(ret);
}
async getDetail(url) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(baseUrl + url);
const document = new Document(res.body);
const imageUrl = baseUrl +
document.selectFirst("div.seriesCoverBox img").attr("data-src");
const name = document.selectFirst("div.series-title h1 span").text;
const genre = document.select("div.genres ul li").map(e => e.text).filter(text => !/^\+\s\d+$/.test(text));
const description = this.cleanHtmlString(document.selectFirst("p.seri_des").attr("data-full-description"));
const produzent = document.select("div.cast li")
.filter(e => e.outerHtml.includes("Produzent:"));
let author = "";
if (produzent.length > 0) {
author = produzent[0]
.select("li")
.map((e) => e.text)
.filter((text) => !/^\s\&\s\d+\sweitere$/.test(text))
.join(", ");
}
const seasonsElements = document.select("#stream > ul:nth-child(1) > li > a");
// Use asyncPool to limit concurrency while processing seasons
const episodesArrays = await this.asyncPool(2, seasonsElements, element => this.parseEpisodesFromSeries(element));
// Flatten the resulting arrays and reverse the order
const episodes = episodesArrays.flat().reverse();
return { name, imageUrl, description, author, status: 5, genre, episodes };
}
async parseEpisodesFromSeries(element) {
const seasonId = element.getHref;
const res = await this.client.get(this.source.baseUrl + seasonId);
const episodeElements = new Document(res.body).select("table.seasonEpisodesList tbody tr");
// Use asyncPool to limit concurrency while processing episodes of a season
return await this.asyncPool(13, episodeElements, e => this.episodeFromElement(e));
}
async episodeFromElement(element) {
const titleAnchor = element.selectFirst("td.seasonEpisodeTitle a");
const episodeSpan = titleAnchor.selectFirst("span");
const url = titleAnchor.attr("href");
const dateUpload = await this.getUploadDateFromEpisode(url);
const episodeSeasonId = element.attr("data-episode-season-id");
let episode = this.cleanHtmlString(episodeSpan.text);
let name = "";
if (url.includes("/film")) {
name = `Film ${episodeSeasonId} : ${episode}`;
} else {
const seasonMatch = url.match(/staffel-(\d+)\/episode/);
name = `Staffel ${seasonMatch[1]} Folge ${episodeSeasonId} : ${episode}`;
}
return name && url ? { name, url, dateUpload } : {};
}
async getUploadDateFromEpisode(url) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(baseUrl + url);
const getLastSundayOfMonth = (year, month) => {
const lastDay = this.createDate(year, month, 0);
const lastSunday = lastDay.getDate() - lastDay.getDay();
return this.createDate(year, month - 1, lastSunday);
};
const document = new Document(res.body);
const dateString = document.selectFirst('strong[style="color: white;"]').text;
const dateTimePart = dateString.split(", ")[1];
const [date, time] = dateTimePart.split(" ");
const [day, month, year] = date.split(".");
const [hours, minutes] = time.split(":");
const dayInt = parseInt(day);
const monthInt = parseInt(month);
const yearInt = parseInt(year);
const hoursInt = parseInt(hours);
const minutesInt = parseInt(minutes);
const lastSundayOfMarch = getLastSundayOfMonth(yearInt, 3);
const lastSundayOfOctober = getLastSundayOfMonth(yearInt, 10);
const jsDate = this.createDate(yearInt, monthInt - 1, dayInt, hoursInt, minutesInt);
// If Date between lastSundayOfMarch & lastSundayOfOctober -> CEST (MESZ)
const isInDST = jsDate >= lastSundayOfMarch && jsDate < lastSundayOfOctober;
let timeZoneOffset = isInDST ? 0 : 1;
// If it's in CEST, subtract 1 hour from UTC (to get local time in CEST)
const correctedTime = this.createDate(jsDate.getTime() + (timeZoneOffset - 1) * 60 * 60 * 1000);
return String(correctedTime.valueOf()); // dateUpload is a string containing date expressed in millisecondsSinceEpoch.
}
createDate(yearOrNum, month, day) {
if (yearOrNum && month && day) {
return day < 1 ? new Date(Math.max(new Date().getFullYear(), yearOrNum), Math.max(0, month)) :
new Date(Math.max(new Date().getFullYear(), yearOrNum), Math.max(0, month), day);
} else if (yearOrNum) {
return new Date(yearOrNum);
} else {
return new Date();
}
}
async getVideoList(url) {
const baseUrl = this.source.baseUrl;
const res = await this.client.get(baseUrl + url, {
'Accept': '*/*',
'Referer': baseUrl + url,
'Priority': 'u=0, i',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0'
});
const document = new Document(res.body);
let promises = [];
const videos = [];
const preferences = new SharedPreferences();
const hostFilter = preferences.get("host_filter");
const langFilter = preferences.get("lang_filter");
const redirectsElements = document.select("ul.row li");
const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false });
for (const element of redirectsElements) {
const langkey = element.attr("data-lang-key");
const lang = (langkey == 1 || langkey == 3) ? 'Deutscher' : 'Englischer';
const type = (langkey == 1) ? 'Dub' : 'Sub';
const host = element.selectFirst("a h4").text;
if (hostFilter.includes(host) && langFilter.includes(`${lang} ${type}`)) {
const redirect = baseUrl + element.selectFirst("a.watchEpisode").attr("href");
promises.push((async (redirect, lang, type, host) => {
const location = (await dartClient.get(redirect)).headers.location;
return await extractAny(location, host.toLowerCase(), lang, type, host, {'Referer': this.source.baseUrl});
})(redirect, lang, type, host));
}
}
for (const p of (await Promise.allSettled(promises))) {
if (p.status == 'fulfilled') {
videos.push.apply(videos, p.value);
}
}
return sortVideos(videos);
}
getSourcePreferences() {
const languages = ['Deutsch', 'Englisch'];
const languageValues = ['Deutscher', 'Englischer'];
const types = ['Dub', 'Sub'];
const resolutions = ['1080p', '720p', '480p'];
const hosts = ['Doodstream', 'Filemoon', 'Luluvdo', 'SpeedFiles', 'Streamtape', 'Vidoza', 'VOE'];
const languageFilters = [];
for (const lang of languageValues) {
for (const type of types) {
languageFilters.push(`${lang} ${type}`);
}
}
return [
{
key: 'lang',
listPreference: {
title: 'Bevorzugte Sprache',
summary: 'Wenn verfügbar, wird diese Sprache ausgewählt. Priority = 0 (lower is better)',
valueIndex: 0,
entries: languages,
entryValues: languageValues
}
},
{
key: 'type',
listPreference: {
title: 'Bevorzugter Typ',
summary: 'Wenn verfügbar, wird dieser Typ ausgewählt. Priority = 1 (lower is better)',
valueIndex: 0,
entries: types,
entryValues: types
}
},
{
key: 'res',
listPreference: {
title: 'Bevorzugte Auflösung',
summary: 'Wenn verfügbar, wird diese Auflösung ausgewählt. Priority = 2 (lower is better)',
valueIndex: 0,
entries: resolutions,
entryValues: resolutions
}
},
{
key: 'host',
listPreference: {
title: 'Bevorzugter Hoster',
summary: 'Wenn verfügbar, wird dieser Hoster ausgewählt. Priority = 3 (lower is better)',
valueIndex: 0,
entries: hosts,
entryValues: hosts
}
},
{
key: "lang_filter",
multiSelectListPreference: {
title: "Sprachen auswählen",
summary: "Wähle aus welche Sprachen dir angezeigt werden sollen. Weniger streams zu laden beschleunigt den Start der Videos.",
entries: languageFilters,
entryValues: languageFilters,
values: languageFilters
}
},
{
key: "host_filter",
multiSelectListPreference: {
title: "Hoster auswählen",
summary: "Wähle aus welche Hoster dir angezeigt werden sollen. Weniger streams zu laden beschleunigt den Start der Videos.",
entries: hosts,
entryValues: hosts,
values: hosts
}
}
];
}
}
/***************************************************************************************************
*
* mangayomi-js-helpers v1.2
*
* # Video Extractors
* - vidGuardExtractor
* - doodExtractor
* - vidozaExtractor
* - okruExtractor
* - amazonExtractor
* - vidHideExtractor
* - filemoonExtractor
* - mixdropExtractor
* - speedfilesExtractor
* - luluvdoExtractor
* - burstcloudExtractor (not working, see description)
*
* # Video Extractor Wrappers
* - streamWishExtractor
* - voeExtractor
* - mp4UploadExtractor
* - yourUploadExtractor
* - streamTapeExtractor
* - sendVidExtractor
*
* # Video Extractor helpers
* - extractAny
*
* # Playlist Extractors
* - m3u8Extractor
* - jwplayerExtractor
*
* # Extension Helpers
* - sortVideos()
*
* # Uint8Array
* - Uint8Array.fromBase64()
* - Uint8Array.prototype.toBase64()
* - Uint8Array.prototype.decode()
*
* # String
* - String.prototype.encode()
* - String.decode()
* - String.prototype.reverse()
* - String.prototype.swapcase()
* - getRandomString()
*
* # Encode/Decode Functions
* - decodeUTF8
* - encodeUTF8
*
* # Url
* - absUrl()
*
***************************************************************************************************/
//--------------------------------------------------------------------------------------------------
// Video Extractors
//--------------------------------------------------------------------------------------------------
async function vidGuardExtractor(url) {
// get html
const res = await new Client().get(url);
const doc = new Document(res.body);
const script = doc.selectFirst('script:contains(eval)');
// eval code
const code = script.text;
eval?.('var window = {};');
eval?.(code);
const playlistUrl = globalThis.window.svg.stream;
// decode sig
const encoded = playlistUrl.match(/sig=(.*?)&/)[1];
const charCodes = [];
for (let i = 0; i < encoded.length; i += 2) {
charCodes.push(parseInt(encoded.slice(i, i + 2), 16) ^ 2);
}
let decoded = Uint8Array.fromBase64(
String.fromCharCode(...charCodes))
.slice(5, -5)
.reverse();
for (let i = 0; i < decoded.length; i += 2) {
let tmp = decoded[i];
decoded[i] = decoded[i + 1];
decoded[i + 1] = tmp;
}
decoded = decoded.decode();
return await m3u8Extractor(playlistUrl.replace(encoded, decoded), null);
}
async function doodExtractor(url) {
const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false });
let response = await dartClient.get(url);
while ("location" in response.headers) {
response = await dartClient.get(response.headers.location);
}
const newUrl = response.request.url;
const doodhost = newUrl.match(/https:\/\/(.*?)\//, newUrl)[0].slice(8, -1);
const md5 = response.body.match(/'\/pass_md5\/(.*?)',/, newUrl)[0].slice(11, -2);
const token = md5.substring(md5.lastIndexOf("/") + 1);
const expiry = new Date().valueOf();
const randomString = getRandomString(10);
response = await new Client().get(`https://${doodhost}/pass_md5/${md5}`, { "Referer": newUrl });
const videoUrl = `${response.body}${randomString}?token=${token}&expiry=${expiry}`;
const headers = { "User-Agent": "Mangayomi", "Referer": doodhost };
return [{ url: videoUrl, originalUrl: videoUrl, headers: headers, quality: '' }];
}
async function vidozaExtractor(url) {
let response = await new Client({ 'useDartHttpClient': true, "followRedirects": true }).get(url);
const videoUrl = response.body.match(/https:\/\/\S*\.mp4/)[0];
return [{ url: videoUrl, originalUrl: videoUrl, quality: '' }];
}
async function okruExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const tag = doc.selectFirst('div[data-options]');
const playlistUrl = tag.attr('data-options').match(/hlsManifestUrl.*?(h.*?id=\d+)/)[1].replaceAll('\\\\u0026', '&');
return await m3u8Extractor(playlistUrl, null);
}
async function amazonExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const videoUrl = doc.selectFirst('video').getSrc;
return videoUrl ? [{ url: videoUrl, originalUrl: videoUrl, headers: null, quality: '' }] : [];
}
async function vidHideExtractor(url) {
const res = await new Client().get(url);
return await jwplayerExtractor(res.body);
}
async function filemoonExtractor(url, headers) {
headers = headers ?? {};
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
delete headers['user-agent'];
let res = await new Client().get(url, headers);
const src = res.body.match(/iframe src="(.*?)"/)?.[1];
if (src) {
res = await new Client().get(src, {
'Referer': url,
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'User-Agent': headers['User-Agent']
});
}
return await jwplayerExtractor(res.body, headers);
}
async function mixdropExtractor(url) {
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'};
let res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(url, headers);
while ("location" in res.headers) {
res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(res.headers.location, headers);
}
const newUrl = res.request.url;
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(MDCore):contains(eval)').text;
const unpacked = unpackJs(code);
let videoUrl = unpacked.match(/wurl="(.*?)"/)?.[1];
if (!videoUrl) return [];
videoUrl = 'https:' + videoUrl;
headers.referer = newUrl;
return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: headers}];
}
async function speedfilesExtractor(url) {
let res = await new Client().get(url);
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(var)').text;
let b64;
// Get b64
for (const match of code.matchAll(/(?:var|let|const)\s*\w+\s*=\s*["']([^"']+)/g)) {
if (match[1].match(/[g-zG-Z]/)) {
b64 = match[1];
break;
}
}
// decode b64 => b64
const step1 = Uint8Array.fromBase64(b64).reverse().decode().swapcase();
// decode b64 => hex
const step2 = Uint8Array.fromBase64(step1).reverse().decode();
// decode hex => b64
let step3 = [];
for (let i = 0; i < step2.length; i += 2) {
step3.push(parseInt(step2.slice(i, i + 2), 16) - 3);
}
step3 = String.fromCharCode(...step3.reverse()).swapcase();
// decode b64 => url
const videoUrl = Uint8Array.fromBase64(step3).decode();
return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: null}];
}
async function luluvdoExtractor(url) {
const client = new Client();
const match = url.match(/(.*?:\/\/.*?)\/.*\/(.*)/);
const headers = {'user-agent': 'Mangayomi'};
const res = await client.get(`${match[1]}/dl?op=embed&file_code=${match[2]}`, headers);
return await jwplayerExtractor(res.body, headers);
}
/** Does not work: Client always sets 'charset=utf-8' in Content-Type. */
async function burstcloudExtractor(url) {
let client = new Client();
let res = await client.get(url);
const id = res.body.match(/data-file-id="(.*?)"/)[1];
const headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
'Referer': url,
};
const data = {
'fileId': id
};
res = await client.post(`https://www.burstcloud.co/file/play-request/`, headers, data);
const videoUrl = res.body.match(/cdnUrl":"(.*?)"/)[1];
return [{
url: videoUrl,
originalUrl: videoUrl,
headers: { 'Referer': url.match(/.*?:\/\/.*?\//) },
quality: ''
}];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Wrappers
//--------------------------------------------------------------------------------------------------
_streamWishExtractor = streamWishExtractor;
streamWishExtractor = async (url) => {
return (await _streamWishExtractor(url, '')).map(v => {
v.quality = v.quality.slice(3, -1);
return v;
});
}
_voeExtractor = voeExtractor;
voeExtractor = async (url) => {
return (await _voeExtractor(url, '')).map(v => {
v.quality = v.quality.replace(/Voe: (\d+p?)/i, '$1');
return v;
});
}
_mp4UploadExtractor = mp4UploadExtractor;
mp4UploadExtractor = async (url) => {
return (await _mp4UploadExtractor(url)).map(v => {
v.quality = v.quality.match(/\d+p/)?.[0] ?? '';
return v;
});
}
_yourUploadExtractor = yourUploadExtractor;
yourUploadExtractor = async (url) => {
return (await _yourUploadExtractor(url))
.filter(v => !v.url.includes('/novideo'))
.map(v => {
v.quality = '';
return v;
});
}
_streamTapeExtractor = streamTapeExtractor;
streamTapeExtractor = async (url) => {
return await _streamTapeExtractor(url, '');
}
_sendVidExtractor = sendVidExtractor;
sendVidExtractor = async (url) => {
let res = await new Client().get(url);
var videoUrl, quality;
try {
videoUrl = res.body.match(/og:video" content="(.*?\.mp4.*?)"/)[1];
quality = res.body.match(/og:video:height" content="(.*?)"/)?.[1];
quality = quality ? quality + 'p' : '';
} catch (error) {
}
if (!videoUrl) {
return _sendVidExtractor(url, null, '');
}
return [{url: videoUrl, originalUrl: videoUrl, quality: quality, headers: null}];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Helpers
//--------------------------------------------------------------------------------------------------
async function extractAny(url, method, lang, type, host, headers = null) {
const m = extractAny.methods[method];
return (!m) ? [] : (await m(url, headers)).map(v => {
v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`;
return v;
});
};
extractAny.methods = {
'amazon': amazonExtractor,
'burstcloud': burstcloudExtractor,
'doodstream': doodExtractor,
'filemoon': filemoonExtractor,
'luluvdo': luluvdoExtractor,
'mixdrop': mixdropExtractor,
'mp4upload': mp4UploadExtractor,
'okru': okruExtractor,
'sendvid': sendVidExtractor,
'speedfiles': speedfilesExtractor,
'streamtape': streamTapeExtractor,
'streamwish': vidHideExtractor,
'vidguard': vidGuardExtractor,
'vidhide': vidHideExtractor,
'vidoza': vidozaExtractor,
'voe': voeExtractor,
'yourupload': yourUploadExtractor
};
//--------------------------------------------------------------------------------------------------
// Playlist Extractors
//--------------------------------------------------------------------------------------------------
async function m3u8Extractor(url, headers = null) {
// https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist
// https://developer.apple.com/documentation/http-live-streaming/adding-alternate-media-to-a-playlist
// define attribute lists
const streamAttributes = [
['avg_bandwidth', /AVERAGE-BANDWIDTH=(\d+)/],
['bandwidth', /\bBANDWIDTH=(\d+)/],
['resolution', /\bRESOLUTION=([\dx]+)/],
['framerate', /\bFRAME-RATE=([\d\.]+)/],
['codecs', /\bCODECS="(.*?)"/],
['video', /\bVIDEO="(.*?)"/],
['audio', /\bAUDIO="(.*?)"/],
['subtitles', /\bSUBTITLES="(.*?)"/],
['captions', /\bCLOSED-CAPTIONS="(.*?)"/]
];
const mediaAttributes = [
['type', /\bTYPE=([\w-]*)/],
['group', /\bGROUP-ID="(.*?)"/],
['lang', /\bLANGUAGE="(.*?)"/],
['name', /\bNAME="(.*?)"/],
['autoselect', /\bAUTOSELECT=(\w*)/],
['default', /\bDEFAULT=(\w*)/],
['instream-id', /\bINSTREAM-ID="(.*?)"/],
['assoc-lang', /\bASSOC-LANGUAGE="(.*?)"/],
['channels', /\bCHANNELS="(.*?)"/],
['uri', /\bURI="(.*?)"/]
];
const streams = [], videos = {}, audios = {}, subtitles = {}, captions = {};
const dict = { 'VIDEO': videos, 'AUDIO': audios, 'SUBTITLES': subtitles, 'CLOSED-CAPTIONS': captions };
const res = await new Client().get(url, headers);
const text = res.body;
if (res.statusCode != 200) {
return [];
}
// collect media
for (const match of text.matchAll(/#EXT-X-MEDIA:(.*)/g)) {
const info = match[1], medium = {};
for (const attr of mediaAttributes) {
const m = info.match(attr[1]);
medium[attr[0]] = m ? m[1] : null;
}
const type = medium.type;
delete medium.type;
const group = medium.group;
delete medium.group;
const typedict = dict[type];
if (typedict[group] == undefined)
typedict[group] = [];
typedict[group].push(medium);
}
// collect streams
for (const match of text.matchAll(/#EXT-X-STREAM-INF:(.*)\s*(.*)/g)) {
const info = match[1], stream = { 'url': absUrl(match[2], url) };
for (const attr of streamAttributes) {
const m = info.match(attr[1]);
stream[attr[0]] = m ? m[1] : null;
}
stream['video'] = videos[stream.video] ?? null;
stream['audio'] = audios[stream.audio] ?? null;
stream['subtitles'] = subtitles[stream.subtitles] ?? null;
stream['captions'] = captions[stream.captions] ?? null;
// format resolution or bandwidth
let quality;
if (stream.resolution) {
quality = stream.resolution.match(/x(\d+)/)[1] + 'p';
} else {
quality = (parseInt(stream.avg_bandwidth ?? stream.bandwidth) / 1000000) + 'Mb/s'
}
// add stream to list
const subs = stream.subtitles?.map((s) => {
return { file: s.uri, label: s.name };
});
const auds = stream.audio?.map((a) => {
return { file: a.uri, label: a.name };
});
streams.push({
url: stream.url,
quality: quality,
originalUrl: stream.url,
headers: headers,
subtitles: subs ?? null,
audios: auds ?? null
});
}
return streams.length ? streams : [{
url: url,
quality: '',
originalUrl: url,
headers: headers,
subtitles: null,
audios: null
}];
}
async function jwplayerExtractor(text, headers) {
// https://docs.jwplayer.com/players/reference/playlists
const getsetup = /setup\(({[\s\S]*?})\)/;
const getsources = /sources:\s*(\[[\s\S]*?\])/;
const gettracks = /tracks:\s*(\[[\s\S]*?\])/;
const unpacked = unpackJs(text);
const videos = [], subtitles = [];
const data = eval('(' + (getsetup.exec(text) || getsetup.exec(unpacked))?.[1] + ')');
if (data){
var sources = data.sources;
var tracks = data.tracks;
} else {
var sources = eval('(' + (getsources.exec(text) || getsources.exec(unpacked))?.[1] + ')');
var tracks = eval('(' + (gettracks.exec(text) || gettracks.exec(unpacked))?.[1] + ')');
}
for (t of tracks) {
if (t.type == "captions") {
subtitles.push({file: t.file, label: t.label});
}
}
for (s of sources) {
if (s.file.includes('master.m3u8')) {
videos.push(...(await m3u8Extractor(s.file, headers)));
} else if (s.file.includes('.mpd')) {
} else {
videos.push({url: s.file, originalUrl: s.file, quality: '', headers: headers});
}
}
return videos.map(v => {
v.subtitles = subtitles;
return v;
});
}
//--------------------------------------------------------------------------------------------------
// Extension Helpers
//--------------------------------------------------------------------------------------------------
function sortVideos(videos) {
const pref = new SharedPreferences();
const getres = RegExp('(\\d+)p?', 'i');
const lang = RegExp(pref.get('lang'), 'i');
const type = RegExp(pref.get('type'), 'i');
const res = RegExp(getres.exec(pref.get('res'))[1], 'i');
const host = RegExp(pref.get('host'), 'i');
let getScore = (q, hasRes) => {
const bLang = lang.test(q), bType = type.test(q), bRes = res.test(q), bHost = host.test(q);
if (hasRes) {
return bLang * (8 + bType * (4 + bRes * (2 + bHost * 1)));
} else {
return bLang * (8 + bType * (4 + (bHost * 3)));
}
}
return videos.sort((a, b) => {
const resA = getres.exec(a.quality)?.[1];
const resB = getres.exec(b.quality)?.[1];
const score = getScore(b.quality, resB) - getScore(a.quality, resA);
if (score) return score;
const qA = resA ? a.quality.replace(resA, (9999 - parseInt(resA)).toString()) : a.quality;
const qB = resA ? b.quality.replace(resB, (9999 - parseInt(resB)).toString()) : b.quality;
return qA.localeCompare(qB);
});
}
//--------------------------------------------------------------------------------------------------
// Uint8Array
//--------------------------------------------------------------------------------------------------
Uint8Array.fromBase64 = function (b64) {
// [00,01,02,03,04,05,06,07,08,\t,\n,0b,0c,\r,0e,0f,10,11,12,13,14,15,16,17,18,19,1a,1b,1c,1d,1e,1f,' ', !, ", #, $, %, &, ', (, ), *, +,',', -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <,'=', >, ?, @,A,B,C,D,E,F,G,H,I,J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~,7f]
const m = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1]
let data = [], val = 0, bits = -8
for (const c of b64) {
let n = m[c.charCodeAt(0)];
if (n == -1) break;
val = (val << 6) + n;
bits += 6;
for (; bits >= 0; bits -= 8)
data.push((val >> bits) & 0xFF);
}
return new Uint8Array(data);
}
Uint8Array.prototype.toBase64 = function () {
const m = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
let b64 = '', val = 0, bits = -6;
for (const b of this) {
val = (val << 8) + b;
bits += 8;
while (bits >= 0) {
b64 += m[(val >> bits) & 0x3F];
bits -= 6;
}
}
if (bits > -6)
b64 += m[(val << -bits) & 0x3F];
return b64 + ['', '', '==', '='][b64.length % 4];
}
Uint8Array.prototype.decode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(this);
}
return null;
}
//--------------------------------------------------------------------------------------------------
// String
//--------------------------------------------------------------------------------------------------
String.prototype.encode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return encodeUTF8(this);
}
return null;
}
String.decode = function (data, encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(data);
}
return null;
}
String.prototype.reverse = function () {
return this.split('').reverse().join('');
}
String.prototype.swapcase = function () {
const isAsciiLetter = /[A-z]/;
const result = [];
for (const l of this)
result.push(isAsciiLetter.test(l) ? String.fromCharCode(l.charCodeAt() ^ 32) : l);
return result.join('');
}
function getRandomString(length) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
let result = "";
for (let i = 0; i < length; i++) {
const random = Math.floor(Math.random() * 61);
result += chars[random];
}
return result;
}
//--------------------------------------------------------------------------------------------------
// Encode/Decode Functions
//--------------------------------------------------------------------------------------------------
function decodeUTF8(data) {
const codes = [];
for (let i = 0; i < data.length;) {
const c = data[i++];
const len = (c > 0xBF) + (c > 0xDF) + (c > 0xEF);
let val = c & (0xFF >> (len + 1));
for (const end = i + len; i < end; i++) {
val = (val << 6) + (data[i] & 0x3F);
}
codes.push(val);
}
return String.fromCharCode(...codes);
}
function encodeUTF8(string) {
const data = [];
for (const c of string) {
const code = c.charCodeAt(0);
const len = (code > 0x7F) + (code > 0x7FF) + (code > 0xFFFF);
let bits = len * 6;
data.push((len ? ~(0xFF >> len + 1) : (0)) + (code >> bits));
while (bits > 0) {
data.push(0x80 + ((code >> (bits -= 6)) & 0x3F))
}
}
return new Uint8Array(data);
}
//--------------------------------------------------------------------------------------------------
// Url
//--------------------------------------------------------------------------------------------------
function absUrl(url, base) {
if (url.search(/^\w+:\/\//) == 0) {
return url;
} else if (url.startsWith('/')) {
return base.slice(0, base.lastIndexOf('/')) + url;
} else {
return base.slice(0, base.lastIndexOf('/') + 1) + url;
}
}

View File

@@ -1,505 +0,0 @@
const mangayomiSources = [{
"name": "AllAnime",
"lang": "en",
"baseUrl": "https://allanime.to",
"apiUrl": "https://api.allanime.day/api",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/en.allanime.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.35",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/en/allanime.js"
}];
class DefaultExtension extends MProvider {
async request(body) {
const apiUrl = this.source.apiUrl;
const baseUrl = this.source.baseUrl;
return (await new Client().get(apiUrl + body, { "Referer": baseUrl })).body
}
async getPopular(page) {
const encodedGql = `?variables=%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22type%22:%20%22anime%22,%0A%20%20%20%20%20%20%20%20%20%20%22size%22:%2026,%0A%20%20%20%20%20%20%20%20%20%20%22dateRange%22:%201,%0A%20%20%20%20%20%20%20%20%20%20%22page%22:%20${page}%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20&query=%0A%20%20%20%20%20%20%20%20query($type:%20VaildPopularTypeEnumType!,%20$size:%20Int!,%20$dateRange:%20Int,%20$page:%20Int)%20%7B%0A%20%20%20%20%20%20%20%20%20%20queryPopular(type:%20$type,%20size:%20$size,%20dateRange:%20$dateRange,%20page:%20$page)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20recommendations%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20anyCard%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_id%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20englishName%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nativeName%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20thumbnail%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20slugTime%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20`
const resList = JSON.parse(await this.request(encodedGql)).data.queryPopular.recommendations.filter(e => e.anyCard !== null);
const preferences = new SharedPreferences();
const titleStyle = preferences.get("preferred_title_style");
const list = [];
for (const anime of resList) {
let title;
if (titleStyle === "romaji") {
title = anime.anyCard.name;
} else if (titleStyle === "eng") {
title = anime.anyCard.englishName || anime.anyCard.name;
} else {
title = anime.anyCard.nativeName || anime.anyCard.name;
}
const name = title;
const imageUrl = anime.anyCard.thumbnail;
const link = `/bangumi/${anime.anyCard._id}/${anime.anyCard.name.replace(/[^a-zA-Z0-9]/g, "-")
.replace(/-{2,}/g, "-")
.toLowerCase()}`;
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: list.length === 26
}
}
async getLatestUpdates(page) {
return await this.search("", page, []);
}
async search(query, page, filters) {
query = query.replace(" ", "%20");
const encodedGql = `?variables=%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22search%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22query%22:%20%22${query}%22,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22allowAdult%22:%20false,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22allowUnknown%22:%20false%0A%20%20%20%20%20%20%20%20%20%20%7D,%0A%20%20%20%20%20%20%20%20%20%20%22countryOrigin%22:%20%22ALL%22,%0A%20%20%20%20%20%20%20%20%20%20%22limit%22:%2026,%0A%20%20%20%20%20%20%20%20%20%20%22page%22:%20${page}%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20&query=%0A%20%20%20%20%20%20%20%20query($search:%20SearchInput,%20$limit:%20Int,%20$countryOrigin:%20VaildCountryOriginEnumType,%20$page:%20Int)%20%7B%0A%20%20%20%20%20%20%20%20%20%20shows(search:%20$search,%20limit:%20$limit,%20countryOrigin:%20$countryOrigin,%20page:%20$page)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20_id%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20nativeName%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20englishName%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20thumbnail%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20slugTime%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20`;
const resList = JSON.parse(await this.request(encodedGql)).data.shows.edges;
const preferences = new SharedPreferences();
const titleStyle = preferences.get("preferred_title_style");
const list = [];
for (const anime of resList) {
let title;
if (titleStyle === "romaji") {
title = anime.name;
} else if (titleStyle === "eng") {
title = anime.englishName || anime.name;
} else {
title = anime.nativeName || anime.name;
}
const name = title;
const imageUrl = anime.thumbnail;
const link = `/bangumi/${anime._id}/${anime.name.replace(/[^a-zA-Z0-9]/g, "-")
.replace(/-{2,}/g, "-")
.toLowerCase()}`;
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: list.length === 26
}
}
async getDetail(url) {
const id = url.substringAfter('bangumi/').substringBefore('/');
const encodedGql = `?variables=%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22id%22:%20%22${id}%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20&query=%0A%20%20%20%20%20%20%20%20query($id:%20String!)%20%7B%0A%20%20%20%20%20%20%20%20%20%20show(_id:%20$id)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20thumbnail%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20type%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20season%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20score%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20genres%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20status%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20studios%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20availableEpisodesDetail%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20`;
const show = JSON.parse(await this.request(encodedGql)).data.show;
const genre = show.genres || [];
const status = this.parseStatus(show.status);
const author = show.studios.length > 0 ? show.studios[0] : "";
let description = "";
description = description.concat(show.description, "\n\n", `Type: ${show.type || "Unknown"}`, `\nAired: ${show.season?.quarter || "-"} ${show.season?.year || "-"}`, `\nScore: ${show.score || "-"}`);
let episodesSub = [];
for (const episode of show.availableEpisodesDetail.sub) {
const num = parseInt(episode) || 1;
const name = `Episode ${num}`;
const url = JSON.stringify({
showId: id,
translationType: ["sub"],
episodeString: episode
});
const scanlator = "sub";
episodesSub.push({ num, name, url, scanlator });
}
let episodesDub = [];
for (const episode of show.availableEpisodesDetail.dub) {
const num = parseInt(episode) || 1;
const name = `Episode ${num}`;
const url = JSON.stringify({
showId: id,
translationType: ["dub"],
episodeString: episode
});
const scanlator = "dub";
episodesDub.push({ num, name, url, scanlator });
}
let episodes = [];
if (episodesSub.length > 0 && episodesSub.length) {
episodes = episodesSub.map(ep => {
const f = episodesDub.filter(e => e.num === ep.num);
if (f.length > 0) {
const url = JSON.parse(ep.url);
return {
"name": ep.name, "url": JSON.stringify({
showId: url.showId,
translationType: ['sub', 'dub'],
episodeString: url.episodeString
}), scanlator: `sub, dub`
}
}
else {
return ep;
}
})
} else {
episodes = episodesDub;
}
return {
description, author, status, genre, episodes
}
}
parseStatus(string) {
switch (string) {
case "Releasing":
return 0;
case "Finished":
return 1;
case "Not Yet Released":
return 0;
default:
return 5;
}
}
async getVideoList(url) {
const baseUrl = this.source.baseUrl;
const preferences = new SharedPreferences();
const subPref = preferences.get("preferred_sub");
const ep = JSON.parse(url);
const translationType = ep.translationType.filter(t => t === subPref);
if (translationType.length == 0) {
return [];
}
const encodedGql = `?variables=%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22showId%22:%20%22${ep.showId}%22,%0A%20%20%20%20%20%20%20%20%20%20%22episodeString%22:%20%22${ep.episodeString}%22,%0A%20%20%20%20%20%20%20%20%20%20%22translationType%22:%20%22${translationType[0]}%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20&query=%0A%20%20%20%20%20%20%20%20query(%0A%20%20%20%20%20%20%20%20%20%20$showId:%20String!%0A%20%20%20%20%20%20%20%20%20%20$episodeString:%20String!%0A%20%20%20%20%20%20%20%20%20%20$translationType:%20VaildTranslationTypeEnumType!%0A%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20episode(%0A%20%20%20%20%20%20%20%20%20%20%20%20showId:%20$showId%0A%20%20%20%20%20%20%20%20%20%20%20%20episodeString:%20$episodeString%0A%20%20%20%20%20%20%20%20%20%20%20%20translationType:%20$translationType%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20sourceUrls%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20`;
const videoJson = JSON.parse(await this.request(encodedGql));
const videos = [];
const altHosterSelection = preferences.get('alt_hoster_selection1');
for (const video of videoJson.data.episode.sourceUrls) {
const videoUrl = this.decryptSource(video.sourceUrl);
let quality = "";
if (videoUrl.includes("/apivtwo/") && altHosterSelection.some(element => 'player' === element)) {
quality = `internal ${video.sourceName}`;
const vids = await new AllAnimeExtractor({ "Referer": baseUrl }, "https://allanime.to").videoFromUrl(videoUrl, quality);
for (const vid of vids) {
videos.push(vid);
}
} else if (["vidstreaming", "https://gogo", "playgo1.cc", "playtaku", "vidcloud"].some(element => videoUrl.includes(element)) && altHosterSelection.some(element => 'vidstreaming' === element)) {
const vids = await gogoCdnExtractor(videoUrl);
for (const vid of vids) {
videos.push(vid);
}
} else if (["dood", "d0"].some(element => videoUrl.includes(element)) && altHosterSelection.some(element => 'dood' === element)) {
const vids = await doodExtractor(videoUrl);
for (const vid of vids) {
videos.push(vid);
}
} else if (["ok.ru", "okru"].some(element => videoUrl.includes(element)) && altHosterSelection.some(element => 'okru' === element)) {
const vids = await okruExtractor(videoUrl);
for (const vid of vids) {
videos.push(vid);
}
} else if (videoUrl.includes("mp4upload.com") && altHosterSelection.some(element => 'mp4upload' === element)) {
const vids = await mp4UploadExtractor(videoUrl);
for (const vid of vids) {
videos.push(vid);
}
} else if (videoUrl.includes("streamlare.com") && altHosterSelection.some(element => 'streamlare' === element)) {
const vids = await streamlareExtractor(videoUrl, 'Streamlare ');
for (const vid of vids) {
videos.push(vid);
}
} else if (["filemoon", "moonplayer"].some(element => videoUrl.includes(element)) && altHosterSelection.some(element => 'filemoon' === element)) {
const vids = await filemoonExtractor(videoUrl);
for (const vid of vids) {
videos.push(vid);
}
} else if (videoUrl.includes("wish") && altHosterSelection.some(element => 'streamwish' === element)) {
const vids = await streamWishExtractor(videoUrl, 'StreamWish ');
for (const vid of vids) {
videos.push(vid);
}
}
}
return this.sortVideos(videos);
}
sortVideos(videos) {
const preferences = new SharedPreferences();
const hoster = preferences.get("preferred_hoster1");
const quality = preferences.get("preferred_quality");
videos.sort((a, b) => {
let qualityMatchA = 0;
if (a.quality.includes(hoster) &&
a.quality.includes(quality)) {
qualityMatchA = 1;
}
let qualityMatchB = 0;
if (b.quality.includes(hoster) &&
b.quality.includes(quality)) {
qualityMatchB = 1;
}
return qualityMatchB - qualityMatchA;
});
return videos;
}
decryptSource(str) {
if (str.startsWith("-")) {
return str.substring(str.lastIndexOf('-') + 1)
.match(/.{1,2}/g)
.map(hex => parseInt(hex, 16))
.map(byte => String.fromCharCode(byte ^ 56))
.join("");
} else {
return str;
}
}
getSourcePreferences() {
return [
{
"key": "preferred_title_style",
"listPreference": {
"title": "Preferred Title Style",
"summary": "",
"valueIndex": 0,
"entries": ["Romaji", "English", "Native"],
"entryValues": ["romaji", "eng", "native"]
}
},
{
"key": "preferred_quality",
"listPreference": {
"title": "Preferred quality",
"summary": "",
"valueIndex": 0,
"entries": [
"2160p",
"1440p",
"1080p",
"720p",
"480p",
"360p",
"240p",
"80p"],
"entryValues": [
"2160",
"1440",
"1080",
"720",
"480",
"360",
"240",
"80"]
}
},
{
"key": "preferred_sub",
"listPreference": {
"title": "Prefer subs or dubs?",
"summary": "",
"valueIndex": 0,
"entries": ["Subs", "Dubs"],
"entryValues": ["sub", "dub"]
}
},
{
"key": "preferred_hoster1",
"listPreference": {
"title": "Preferred Video Server",
"summary": "",
"valueIndex": 0,
"entries": [
"Ac", "Ak", "Kir", "Rab", "Luf-mp4",
"Si-Hls", "S-mp4", "Ac-Hls", "Uv-mp4", "Pn-Hls",
"vidstreaming", "okru", "mp4upload", "streamlare", "doodstream",
"filemoon",
"streamwish"
],
"entryValues": [
"Ac", "Ak", "Kir", "Rab", "Luf-mp4",
"Si-Hls", "S-mp4", "Ac-Hls", "Uv-mp4", "Pn-Hls",
"vidstreaming", "okru", "mp4upload", "streamlare", "doodstream",
"filemoon",
"streamwish"
]
}
},
{
"key": "alt_hoster_selection1",
"multiSelectListPreference": {
"title": "Enable/Disable Alternative Hosts",
"summary": "",
"entries": [
"player",
"vidstreaming",
"okru",
"mp4upload",
"streamlare",
"doodstream",
"filemoon",
"streamwish"
],
"entryValues": [
"player",
"vidstreaming",
"okru",
"mp4upload",
"streamlare",
"doodstream",
"filemoon",
"streamwish"
],
"values": [
"player",
"vidstreaming",
"okru",
"mp4upload",
"streamlare",
"doodstream",
"filemoon",
"streamwish"
]
}
}
];
}
}
class AllAnimeExtractor {
constructor(headers, baseUrl) {
this.headers = headers;
this.baseUrl = baseUrl;
}
bytesIntoHumanReadable(bytes) {
const kilobyte = 1000;
const megabyte = kilobyte * 1000;
const gigabyte = megabyte * 1000;
const terabyte = gigabyte * 1000;
if (bytes >= 0 && bytes < kilobyte) {
return `${bytes} b/s`;
} else if (bytes >= kilobyte && bytes < megabyte) {
return `${Math.floor(bytes / kilobyte)} kb/s`;
} else if (bytes >= megabyte && bytes < gigabyte) {
return `${Math.floor(bytes / megabyte)} mb/s`;
} else if (bytes >= gigabyte && bytes < terabyte) {
return `${Math.floor(bytes / gigabyte)} gb/s`;
} else if (bytes >= terabyte) {
return `${Math.floor(bytes / terabyte)} tb/s`;
} else {
return `${bytes} bits/s`;
}
}
async videoFromUrl(url, name) {
const videoList = [];
const endPointResponse = JSON.parse((await new Client().get(`${this.baseUrl}/getVersion`, this.headers)).body);
const endPoint = endPointResponse.episodeIframeHead;
const resp = await new Client().get(endPoint + url.replace("/clock?", "/clock.json?"), this.headers);
if (resp.statusCode !== 200) {
return [];
}
const linkJson = JSON.parse(resp.body);
for (const link of linkJson.links) {
try {
const subtitles = [];
if (link.subtitles && link.subtitles.length > 0) {
subtitles.push(...link.subtitles.map(sub => {
const label = sub.label ? ` - ${sub.label}` : '';
return { file: sub.src, label: `${sub.lang}${label}` };
}));
}
if (link.mp4) {
videoList.push({
url:
link.link,
quality: `Original (${name} - ${link.resolutionStr})`,
originalUrl: link.link,
subtitles,
});
} else if (link.hls) {
const headers =
{
'Host': link.link.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/)[1],
'Origin': endPoint,
'Referer': `${endPoint}/`
};
const resp = await new Client().get(link.link, headers);
if (resp.statusCode === 200) {
const masterPlaylist = resp.body;
const audios = [];
if (masterPlaylist.includes('#EXT-X-MEDIA:TYPE=AUDIO')) {
const audioInfo = masterPlaylist.substringAfter('#EXT-X-MEDIA:TYPE=AUDIO').substringBefore('\n');
const language = audioInfo.substringAfter('NAME="').substringBefore('"');
const url = audioInfo.substringAfter('URI="').substringBefore('"');
audios.push({ file: url, label: language });
}
if (!masterPlaylist.includes('#EXT-X-STREAM-INF:')) {
if (audios.length === 0) {
videoList.push({ url: link.link, quality: `${name} - ${link.resolutionStr}`, originalUrl: link.link, subtitles, headers });
} else {
videoList.push({ url: link.link, quality: `${name} - ${link.resolutionStr}`, originalUrl: link.link, subtitles, audios, headers });
}
} else {
masterPlaylist.substringAfter('#EXT-X-STREAM-INF:').split('#EXT-X-STREAM-INF:').forEach(it => {
let bandwidth = '';
if (it.includes('AVERAGE-BANDWIDTH')) {
bandwidth = ` ${this.bytesIntoHumanReadable(it.substringAfter('AVERAGE-BANDWIDTH=').substringBefore(','))}`;
}
const quality = `${it.substringAfter('RESOLUTION=').substringAfter('x').substringBefore(',')}p${bandwidth} (${name} - ${link.resolutionStr})`;
let videoUrl = it.substringAfter('\n').substringBefore('\n');
if (!videoUrl.startsWith('http')) {
videoUrl = resp.request.url.substringBeforeLast('/') + `/${videoUrl}`;
}
const headers =
{
'Host': videoUrl.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/)[1],
'Origin': endPoint,
'Referer': `${endPoint}/`
};
if (audios.length === 0) {
videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, subtitles, headers });
} else {
videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, subtitles, audios, headers });
}
});
}
}
} else if (link.crIframe) {
for (const stream of link.portData.streams) {
if (stream.format === 'adaptive_dash') {
videoList.push({
url:
stream.url,
quality: `Original (AC - Dash${stream.hardsub_lang.length === 0 ? '' : ` - Hardsub: ${stream.hardsub_lang}`})`,
originalUrl: stream.url,
subtitles,
});
} else if (stream.format === 'adaptive_hls') {
const resp = await new Client().get(stream.url, { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0' })
if (resp.statusCode === 200) {
const masterPlaylist = resp.body;
masterPlaylist.substringAfter('#EXT-X-STREAM-INF:').split('#EXT-X-STREAM-INF:').forEach(t => {
const quality = `${t.substringAfter('RESOLUTION=').substringAfter('x').substringBefore(',')}p (AC - HLS${stream.hardsub_lang.length === 0 ? '' : ` - Hardsub: ${stream.hardsub_lang}`})`;
const videoUrl = t.substringAfter('\n').substringBefore('\n');
videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, subtitles });
});
}
}
}
} else if (link.dash) {
const audios = link.rawUrls && link.rawUrls.audios ? link.rawUrls.audios.map(it => { return { file: it.url, label: this.bytesIntoHumanReadable(it.bandwidth) }; }) : [];
const videos = link.rawUrls && link.rawUrls.vids ? link.rawUrls.vids.map
(it => {
if (!audios) {
return { url: it.url, quality: `${name} - ${it.height} ${this.bytesIntoHumanReadable(it.bandwidth)}`, originalUrl: it.url, subtitles };
} else {
return { url: it.url, quality: `${name} - ${it.height} ${this.bytesIntoHumanReadable(it.bandwidth)}`, originalUrl: it.url, audios, subtitles };
}
}) : [];
if (videos.length > 0) {
videoList.push(...videos);
}
}
} catch (_) {
}
}
return videoList;
}
}

View File

@@ -1,254 +0,0 @@
const mangayomiSources = [
{
"name": "AnimeGG",
"lang": "en",
"id": 209614032,
"baseUrl": "https://www.animegg.org",
"apiUrl": "",
"iconUrl":
"https://www.google.com/s2/favicons?sz=256&domain=https://www.animegg.org/",
"typeSource": "single",
"itemType": 1,
"version": "1.0.3",
"pkgPath": "anime/src/en/animegg.js"
}
];
class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
getHeaders(url) {
return {
Referer: this.source.baseUrl,
Origin: this.source.baseUrl,
};
}
getPreference(key) {
return parseInt(new SharedPreferences().get(key));
}
async requestText(slug) {
var url = `${this.source.baseUrl}${slug}`;
var res = await this.client.get(url, this.getHeaders());
return res.body;
}
async request(slug) {
return new Document(await this.requestText(slug));
}
async fetchPopularnLatest(slug) {
var body = await this.request(slug);
var items = body.select("li.fea");
var list = [];
var hasNextPage = true;
if (items.length > 0) {
for (var item of items) {
var imageUrl = item.selectFirst("img").getSrc;
var linkSection = item.selectFirst(".rightpop").selectFirst("a");
var link = linkSection.getHref;
var name = linkSection.text;
list.push({
name,
imageUrl,
link,
});
}
} else {
hasNextPage = false;
}
return { list, hasNextPage };
}
async getPopular(page) {
var start = (page - 1) * 25;
var limit = start + 25;
var category = "";
var pop = this.getPreference("animegg_popular_category");
switch (pop) {
case 1: {
category = "sortBy=createdAt&sortDirection=DESC&";
break;
}
case 2: {
category = "ongoing=true&";
break;
}
case 3: {
category = "ongoing=false&";
break;
}
case 4: {
category = "sortBy=sortLetter&sortDirection=ASC&";
break;
}
}
var slug = `/popular-series?${category}start=${start}&limit=${limit}`;
return await this.fetchPopularnLatest(slug);
}
get supportsLatest() {
throw new Error("supportsLatest not implemented");
}
async getLatestUpdates(page) {
var start = (page - 1) * 25;
var limit = start + 25;
var slug = `/releases?start=${start}&limit=${limit}`;
return await this.fetchPopularnLatest(slug);
}
async search(query, page, filters) {
var slug = `/search?q=${query}`;
var body = await this.request(slug);
var items = body.select(".moose.page > a");
var list = [];
for (var item of items) {
var imageUrl = item.selectFirst("img").getSrc;
var link = item.getHref;
var name = item.selectFirst("h2").text;
list.push({
name,
imageUrl,
link,
});
}
return { list, hasNextPage: false };
}
statusCode(status) {
return (
{
Ongoing: 0,
Completed: 1,
}[status] ?? 5
);
}
async getDetail(url) {
var baseUrl = this.source.baseUrl;
var slug = url.replace(baseUrl, "");
var link = baseUrl + slug;
var body = await this.request(slug);
var media = body.selectFirst(".media");
var title = media.selectFirst("h1").text;
var spans = media.selectFirst("p.infoami").select("span");
var statusText = spans[spans.length - 1].text.replace("Status: ", "");
var status = this.statusCode(statusText);
var tagscat = media.select(".tagscat > li");
var genre = [];
tagscat.forEach((tag) => genre.push(tag.text));
var description = body.selectFirst("p.ptext").text;
var chapters = [];
var episodesList = body.select(".newmanga > li");
episodesList.forEach((ep) => {
var epTitle = ep.selectFirst("i.anititle").text;
var epNumber = ep.selectFirst("strong").text.replace(title, "Episode");
var epName = epNumber == epTitle ? epNumber : `${epNumber} - ${epTitle}`;
var epUrl = ep.selectFirst("a").getHref;
var scanlator = "";
var type = ep.select("span.btn-xs");
type.forEach((t) => {
scanlator += t.text + ", ";
});
scanlator = scanlator.slice(0, -2);
chapters.push({ name: epName, url: epUrl, scanlator });
});
return { description, status, genre, chapters, link };
}
async exxtractStreams(div, audio) {
var slug = div.selectFirst("iframe").getSrc;
var streams = [];
if (slug.length < 1) {
return streams;
}
var body = await this.requestText(slug);
var sKey = "var videoSources = ";
var eKey = "var httpProtocol";
var start = body.indexOf(sKey) + sKey.length;
var end = body.indexOf(eKey) - 8;
var videoSourcesStr = body.substring(start, end);
let videoSources = eval("(" + videoSourcesStr + ")");
var headers = this.getHeaders();
videoSources.forEach((videoSource) => {
var url = this.source.baseUrl + videoSource.file;
var quality = `${videoSource.label} - ${audio}`;
streams.push({
url,
originalUrl: url,
quality,
headers,
});
});
return streams.reverse();
}
// For anime episode video list
async getVideoList(url) {
var body = await this.request(url);
var sub = body.selectFirst("#subbed-Animegg");
var subStreams = await this.exxtractStreams(sub, "Sub");
var dub = body.selectFirst("#dubbed-Animegg");
var dubStreams = await this.exxtractStreams(dub, "Dub");
var raw = body.selectFirst("#raw-Animegg");
var rawStreams = await this.exxtractStreams(raw, "Raw");
var pref = this.getPreference("animegg_stream_type_1");
var streams = [];
if (pref == 0) {
streams = [...subStreams, ...dubStreams, ...rawStreams];
} else if (pref == 1) {
streams = [...dubStreams, ...subStreams, ...rawStreams];
} else {
streams = [...rawStreams, ...subStreams, ...dubStreams];
}
return streams;
}
getSourcePreferences() {
return [
{
key: "animegg_popular_category",
listPreference: {
title: "Preferred popular category",
summary: "",
valueIndex: 0,
entries: [
"Popular",
"Newest",
"Ongoing",
"Completed",
"Alphabetical",
],
entryValues: ["0", "1", "2", "3", "4"],
},
},
{
key: "animegg_stream_type_1",
listPreference: {
title: "Preferred stream type",
summary: "",
valueIndex: 0,
entries: ["Sub", "Dub", "Raw"],
entryValues: ["0", "1", "2"],
},
},
];
}
}

View File

@@ -1,715 +0,0 @@
const mangayomiSources = [{
"name": "AnimeKai",
"lang": "en",
"baseUrl": "https://animekai.to",
"apiUrl": "",
"iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/",
"typeSource": "single",
"itemType": 1,
"version": "0.2.4",
"pkgPath": "anime/src/en/animekai.js"
}];
class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
getPreference(key) {
return new SharedPreferences().get(key);
}
getBaseUrl() {
return this.getPreference("animekai_base_url");
}
async request(slug) {
var url = slug
var baseUrl = this.getBaseUrl()
if (!slug.includes(baseUrl)) url = baseUrl + slug;
var res = await this.client.get(url);
return res.body
}
async getPage(slug) {
var res = await this.request(slug);
return new Document(res);
}
async searchPage({ query = "", type = [], genre = [], status = [], sort = "", season = [], year = [], rating = [], country = [], language = [], page = 1 } = {}) {
function bundleSlug(category, items) {
var rd = ""
for (var item of items) {
rd += `&${category}[]=${item.toLowerCase()}`;
}
return rd;
}
var slug = "/browser?"
slug += "keyword=" + query;
slug += bundleSlug("type", type);
slug += bundleSlug("genre", genre);
slug += bundleSlug("status", status);
slug += bundleSlug("status", status);
slug += bundleSlug("season", season);
slug += bundleSlug("year", year);
slug += bundleSlug("rating", rating);
slug += bundleSlug("country", country);
slug += bundleSlug("language", language);
sort = sort.length < 1 ? "updated_date" : sort// default sort is updated date
slug += "&sort=" + sort;
slug += `&page=${page}`;
var list = []
var body = await this.getPage(slug);
var paginations = body.select(".pagination > li")
var hasNextPage = paginations.length > 0 ? !paginations[paginations.length - 1].className.includes("active") : false
var titlePref = this.getPreference("animekai_title_lang")
var animes = body.selectFirst(".aitem-wrapper").select(".aitem")
animes.forEach(anime => {
var link = anime.selectFirst("a").getHref
var imageUrl = anime.selectFirst("img").attr("data-src")
var name = anime.selectFirst("a.title").attr(titlePref)
list.push({ name, link, imageUrl });
})
return { list, hasNextPage }
}
async getPopular(page) {
var types = this.getPreference("animekai_popular_latest_type")
return await this.searchPage({ sort: "trending", type: types, page: page });
}
async getLatestUpdates(page) {
var types = this.getPreference("animekai_popular_latest_type")
return await this.searchPage({ sort: "updated_date", type: types, page: page });
}
async search(query, page, filters) {
function getFilter(state) {
var rd = []
state.forEach(item => {
if (item.state) {
rd.push(item.value)
}
})
return rd
}
var isFiltersAvailable = !filters || filters.length != 0
var type = isFiltersAvailable ? getFilter(filters[0].state) : []
var genre = isFiltersAvailable ? getFilter(filters[1].state) : []
var status = isFiltersAvailable ? getFilter(filters[2].state) : []
var sort = isFiltersAvailable ? filters[3].values[filters[3].state].value : ""
var season = isFiltersAvailable ? getFilter(filters[4].state) : []
var year = isFiltersAvailable ? getFilter(filters[5].state) : []
var rating = isFiltersAvailable ? getFilter(filters[6].state) : []
var country = isFiltersAvailable ? getFilter(filters[7].state) : []
var language = isFiltersAvailable ? getFilter(filters[8].state) : []
return await this.searchPage({ query, type, genre, status, sort, season, year, rating, country, language, page });
}
async getDetail(url) {
function statusCode(status) {
return {
"Releasing": 0,
"Completed": 1,
"Not Yet Aired": 4,
}[status] ?? 5;
}
var slug = url
var link = this.getBaseUrl() + slug
var body = await this.getPage(slug)
var mainSection = body.selectFirst(".watch-section")
var imageUrl = mainSection.selectFirst("div.poster").selectFirst("img").getSrc
var namePref = this.getPreference("animekai_title_lang")
var nameSection = mainSection.selectFirst("div.title")
var name = namePref.includes("jp") ? nameSection.attr(namePref) : nameSection.text
var description = mainSection.selectFirst("div.desc").text
var detailSection = mainSection.select("div.detail > div")
var genre = []
var status = 5
detailSection.forEach(item => {
var itemText = item.text.trim()
if (itemText.includes("Genres")) {
genre = itemText.replace("Genres: ", "").split(", ")
}
if (itemText.includes("Status")) {
var statusText = item.selectFirst("span").text
status = statusCode(statusText)
}
})
var chapters = []
var animeId = body.selectFirst("#anime-rating").attr("data-id")
var token = await this.kaiEncrypt(animeId)
var res = await this.request(`/ajax/episodes/list?ani_id=${animeId}&_=${token}`)
body = JSON.parse(res)
if (body.status == 200) {
var doc = new Document(body["result"])
var episodes = doc.selectFirst("div.eplist.titles").select("li")
var showUncenEp = this.getPreference("animekai_show_uncen_epsiodes")
for (var item of episodes) {
var aTag = item.selectFirst("a")
var num = parseInt(aTag.attr("num"))
var title = aTag.selectFirst("span").text
title = title.includes("Episode") ? "" : `: ${title}`
var epName = `Episode ${num}${title}`
var langs = aTag.attr("langs")
var scanlator = langs === "1" ? "SUB" : "SUB, DUB"
var token = aTag.attr("token")
var epData = {
name: epName,
url: token,
scanlator
}
// Check if the episode is uncensored
var slug = aTag.attr("slug")
if (slug.includes("uncen")) {
// if dont show uncensored episodes, skip this episode
if (!showUncenEp) continue
scanlator += ", UNCENSORED"
epName = `Episode ${num}: (Uncensored)`
// Build for uncensored episode
epData = {
name: epName,
url: token,
scanlator
}
// Check if the episode already exists as censored if so, add to existing data
var exData = chapters[num - 1]
if (exData) {
exData.url += "||" + epData.url
exData.scanlator += ", " + epData.scanlator
chapters[num - 1] = exData
continue
}
}
chapters.push(epData)
}
}
chapters.reverse()
return { name, imageUrl, link, description, genre, status, chapters }
}
// For anime episode video list
async getVideoList(url) {
var streams = []
var prefServer = this.getPreference("animekai_pref_stream_server")
// If no server is chosen, use the default server 1
if (prefServer.length < 1) prefServer.push("1")
var prefDubType = this.getPreference("animekai_pref_stream_subdub_type")
// If no dubtype is chosen, use the default dubtype sub
if (prefDubType.length < 1) prefDubType.push("sub")
var epSlug = url.split("||")
// The 1st time the loop runs its for censored version
var isUncensoredVersion = false
for (var epId of epSlug) {
var token = await this.kaiEncrypt(epId)
var res = await this.request(`/ajax/links/list?token=${epId}&_=${token}`)
var body = JSON.parse(res)
if (body.status != 200) continue
var serverResult = new Document(body.result)
// [{"serverName":"Server 1","dataId":"","dubType":"sub"},{"serverName":"Server 2","dataId":"","dubType":"softsub"}]
var SERVERDATA = []
// Gives 2 server for each Sub, softsub, dub
var server_items = serverResult.select("div.server-items")
for (var dubSection of server_items) {
var dubType = dubSection.attr("data-id")
// If dubtype is not in preference dont include it
if (!prefDubType.includes(dubType)) continue
for (var ser of dubSection.select("span.server")) {
var serverName = ser.text
// If servername is not in preference dont include it
if (!prefServer.includes(serverName.replace("Server ", ""))) continue
var dataId = ser.attr("data-lid")
SERVERDATA.push({
serverName,
dataId,
dubType
})
}
}
for (var serverData of SERVERDATA) {
var serverName = serverData.serverName
var dataId = serverData.dataId
var dubType = serverData.dubType.toUpperCase()
dubType = dubType == "SUB" ? "HARDSUB" : dubType
dubType = isUncensoredVersion ? `${dubType} [Uncensored]` : dubType
var megaUrl = await this.getMegaUrl(dataId)
var serverStreams = await this.decryptMegaEmbed(megaUrl, serverName, dubType)
streams = [...streams, ...serverStreams]
// Dubs have subtitles separately, so we need to fetch them too
if (dubType.includes("DUB")) {
if (!megaUrl.includes("sub.list=")) continue;
var subList = megaUrl.split("sub.list=")[1]
var subres = await this.client.get(subList)
var subtitles = JSON.parse(subres.body)
var subs = this.formatSubtitles(subtitles, dubType)
streams[streams.length - 1].subtitles = subs;
}
}
// The 2nd time the loop runs its for uncensored version
isUncensoredVersion = true;
// Main for ends
}
return streams
}
getFilterList() {
function formateState(type_name, items, values) {
var state = [];
for (var i = 0; i < items.length; i++) {
state.push({ type_name: type_name, name: items[i], value: values[i] })
}
return state;
}
var filters = [];
// Types
var items = ["TV", "Special", "OVA", "ONA", "Music", "Movie"]
var values = ["tv", "special", "ova", "ona", "music", "movie"]
filters.push({
type_name: "GroupFilter",
name: "Types",
state: formateState("CheckBox", items, values)
})
// Genre
items = [
"Action", "Adventure", "Avant Garde", "Boys Love", "Comedy", "Demons", "Drama", "Ecchi", "Fantasy",
"Girls Love", "Gourmet", "Harem", "Horror", "Isekai", "Iyashikei", "Josei", "Kids", "Magic",
"Mahou Shoujo", "Martial Arts", "Mecha", "Military", "Music", "Mystery", "Parody", "Psychological",
"Reverse Harem", "Romance", "School", "Sci-Fi", "Seinen", "Shoujo", "Shounen", "Slice of Life",
"Space", "Sports", "Super Power", "Supernatural", "Suspense", "Thriller", "Vampire"
];
values = [
"47", "1", "235", "184", "7", "127", "66", "8", "34", "926", "436", "196", "421", "77", "225",
"555", "35", "78", "857", "92", "219", "134", "27", "48", "356", "240", "798", "145", "9", "36",
"189", "183", "37", "125", "220", "10", "350", "49", "322", "241", "126"
];
filters.push({
type_name: "GroupFilter",
name: "Genres",
state: formateState("CheckBox", items, values)
})
// Status
items = ["Not Yet Aired", "Releasing", "Completed"]
values = ["info", "releasing", "completed"]
filters.push({
type_name: "GroupFilter",
name: "Status",
state: formateState("CheckBox", items, values)
})
// Sort
items = [
"All", "Updated date", "Released date", "End date", "Added date", "Trending",
"Name A-Z", "Average score", "MAL score", "Total views", "Total bookmarks", "Total episodes"
];
values = [
"", "updated_date", "released_date", "end_date", "added_date", "trending",
"title_az", "avg_score", "mal_score", "total_views", "total_bookmarks", "total_episodes"
];
filters.push({
type_name: "SelectFilter",
name: "Sort by",
state: 0,
values: formateState("SelectOption", items, values)
})
// Season
items = ["Fall", "Summer", "Spring", "Winter", "Unknown"];
values = ["fall", "summer", "spring", "winter", "unknown"];
filters.push({
type_name: "GroupFilter",
name: "Season",
state: formateState("CheckBox", items, values)
})
// Years
const currentYear = new Date().getFullYear();
var years = Array.from({ length: currentYear - 1999 }, (_, i) => (2000 + i).toString()).reverse()
items = [...years, "1990s", "1980s", "1970s", "1960s", "1950s", "1940s", "1930s", "1920s", "1910s", "1900s",]
filters.push({
type_name: "GroupFilter",
name: "Years",
state: formateState("CheckBox", items, items)
})
// Ratings
items = [
"G - All Ages",
"PG - Children",
"PG 13 - Teens 13 and Older",
"R - 17+, Violence & Profanity",
"R+ - Profanity & Mild Nudity",
"Rx - Hentai"
];
values = ["g", "pg", "pg_13", "r", "r+", "rx"];
filters.push({
type_name: "GroupFilter",
name: "Ratings",
state: formateState("CheckBox", items, items)
})
// Country
items = ["Japan", "China"];
values = ["11", "2"];
filters.push({
type_name: "GroupFilter",
name: "Country",
state: formateState("CheckBox", items, items)
})
// Language
items = ["Hard Sub", "Soft Sub", "Dub", "Sub & Dub"];
values = ["sub", "softsub", "dub", "subdub"];
filters.push({
type_name: "GroupFilter",
name: "Language",
state: formateState("CheckBox", items, items)
})
return filters;
}
getSourcePreferences() {
return [
{
key: "animekai_base_url",
editTextPreference: {
title: "Override base url",
summary: "",
value: "https://animekai.to",
dialogTitle: "Override base url",
dialogMessage: "",
}
}, {
key: "animekai_popular_latest_type",
multiSelectListPreference: {
title: 'Preferred type of anime to be shown in popular & latest section',
summary: 'Choose which type of anime you want to see in the popular &latest section',
values: ["tv", "special", "ova", "ona"],
entries: ["TV", "Special", "OVA", "ONA", "Music", "Movie"],
entryValues: ["tv", "special", "ova", "ona", "music", "movie"]
}
}, {
key: "animekai_title_lang",
listPreference: {
title: 'Preferred title language',
summary: 'Choose in which language anime title should be shown',
valueIndex: 1,
entries: ["English", "Romaji"],
entryValues: ["title", "data-jp"]
}
},
{
key: "animekai_show_uncen_epsiodes",
switchPreferenceCompat: {
title: 'Show uncensored episodes',
summary: "",
value: true
}
}, {
key: "animekai_pref_stream_server",
multiSelectListPreference: {
title: 'Preferred server',
summary: 'Choose the server/s you want to extract streams from',
values: ["1"],
entries: ["Server 1", "Server 2"],
entryValues: ["1", "2"]
}
}, {
key: "animekai_pref_stream_subdub_type",
multiSelectListPreference: {
title: 'Preferred stream sub/dub type',
summary: '',
values: ["sub", "softsub", "dub"],
entries: ["Hard Sub", "Soft Sub", "Dub"],
entryValues: ["sub", "softsub", "dub"]
}
}, {
key: "animekai_pref_extract_streams",
switchPreferenceCompat: {
title: 'Split stream into different quality streams',
summary: "Split stream Auto into 360p/720p/1080p",
value: true
}
},
]
}
// -------------------------------
formatSubtitles(subtitles, dubType) {
var subs = []
subtitles.forEach(sub => {
if (!sub.kind.includes("thumbnail")) {
subs.push({
file: sub.file,
label: `${sub.label} - ${dubType}`
})
}
})
return subs
}
async formatStreams(sUrl, serverName, dubType) {
function streamNamer(res) {
return `${res} - ${dubType} : ${serverName}`
}
var streams = [{
url: sUrl,
originalUrl: sUrl,
quality: streamNamer("Auto")
}]
var pref = this.getPreference("animekai_pref_extract_streams")
if (!pref) return streams
var baseUrl = sUrl.split("/list.m3u8")[0].split("/list,")[0]
const response = await new Client().get(sUrl);
const body = response.body;
const lines = body.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-STREAM-INF:')) {
var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
var qUrl = lines[i + 1].trim();
var m3u8Url = `${baseUrl}/${qUrl}`
streams.push({
url: m3u8Url,
originalUrl: m3u8Url,
quality: streamNamer(resolution)
});
}
}
return streams
}
async getMegaUrl(vidId) {
var token = await this.kaiEncrypt(vidId)
var res = await this.request(`/ajax/links/view?id=${vidId}&_=${token}`)
var body = JSON.parse(res)
if (body.status != 200) return
var outEnc = body.result
var out = await this.kaiDecrypt(outEnc)
var o = JSON.parse(out)
return decodeURIComponent(o.url)
}
async decryptMegaEmbed(megaUrl, serverName, dubType) {
var streams = []
megaUrl = megaUrl.replace("/e/", "/media/")
var res = await this.client.get(megaUrl)
var body = JSON.parse(res.body)
if (body.status != 200) return
var outEnc = body.result
var streamData = await this.megaDecrypt(outEnc)
var url = streamData.sources[0].file
var streams = await this.formatStreams(url, serverName, dubType)
var subtitles = streamData.tracks
streams[0].subtitles = this.formatSubtitles(subtitles, dubType)
return streams
}
//----------------AnimeKai Decoders----------------
// Credits :- https://github.com/amarullz/kaicodex/
base64UrlDecode(input) {
let base64 = input
.replace(/-/g, "+")
.replace(/_/g, "/");
while (base64.length % 4 !== 0) {
base64 += "=";
}
const base64abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const outputBytes = [];
for (let i = 0; i < base64.length; i += 4) {
const c1 = base64abc.indexOf(base64[i]);
const c2 = base64abc.indexOf(base64[i + 1]);
const c3 = base64abc.indexOf(base64[i + 2]);
const c4 = base64abc.indexOf(base64[i + 3]);
const triplet = (c1 << 18) | (c2 << 12) | ((c3 & 63) << 6) | (c4 & 63);
outputBytes.push((triplet >> 16) & 0xFF);
if (base64[i + 2] !== "=") outputBytes.push((triplet >> 8) & 0xFF);
if (base64[i + 3] !== "=") outputBytes.push(triplet & 0xFF);
}
// Convert bytes to ISO-8859-1 string
return String.fromCharCode(...outputBytes);
}
base64UrlEncode(str) {
// Convert to ISO-8859-1 byte array
const bytes = [];
for (let i = 0; i < str.length; i++) {
bytes.push(str.charCodeAt(i) & 0xFF);
}
// Base64 alphabet
const base64abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Manual base64 encoding
let base64 = "";
for (let i = 0; i < bytes.length; i += 3) {
const b1 = bytes[i];
const b2 = bytes[i + 1] ?? 0;
const b3 = bytes[i + 2] ?? 0;
const triplet = (b1 << 16) | (b2 << 8) | b3;
base64 += base64abc[(triplet >> 18) & 0x3F];
base64 += base64abc[(triplet >> 12) & 0x3F];
base64 += i + 1 < bytes.length ? base64abc[(triplet >> 6) & 0x3F] : "=";
base64 += i + 2 < bytes.length ? base64abc[triplet & 0x3F] : "=";
}
// URL-safe Base64
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
transform(key, text) {
const v = Array.from({ length: 256 }, (_, i) => i);
let c = 0;
const f = [];
for (let w = 0; w < 256; w++) {
c = (c + v[w] + key.charCodeAt(w % key.length)) % 256;
[v[w], v[c]] = [v[c], v[w]];
}
let a = 0, w = 0, sum = 0;
while (a < text.length) {
w = (w + 1) % 256;
sum = (sum + v[w]) % 256;
[v[w], v[sum]] = [v[sum], v[w]];
f.push(String.fromCharCode(text.charCodeAt(a) ^ v[(v[w] + v[sum]) % 256]));
a++;
}
return f.join('');
}
reverseString(input) {
return input.split('').reverse().join('');
}
substitute(input, keys, values) {
const map = {};
for (let i = 0; i < keys.length; i++) {
map[keys[i]] = values[i] || keys[i];
}
return input.split('').map(char => map[char] || char).join('');
}
async getDecoderPattern() {
const preferences = new SharedPreferences();
let pattern = preferences.getString("anime_kai_decoder_pattern", "");
var pattern_ts = parseInt(preferences.getString("anime_kai_decoder_pattern_ts", "0"));
var now_ts = parseInt(new Date().getTime() / 1000);
// pattern is checked from API every 30 minutes
if (now_ts - pattern_ts > 30 * 60) {
var res = await this.client.get("https://raw.githubusercontent.com/amarullz/kaicodex/refs/heads/main/generated/kai_codex.json")
pattern = res.body
preferences.setString("anime_kai_decoder_pattern", pattern);
preferences.setString("anime_kai_decoder_pattern_ts", `${now_ts}`);
}
return JSON.parse(pattern);
}
async patternExecutor(key, type, id) {
var result = id
var pattern = await this.getDecoderPattern()
var logic = pattern[key][type]
logic.forEach(step => {
var method = step[0]
if (method == "urlencode") result = encodeURIComponent(result);
else if (method == "urldecode") result = decodeURIComponent(result);
else if (method == "rc4") result = this.transform(step[1], result);
else if (method == "reverse") result = this.reverseString(result);
else if (method == "substitute") result = this.substitute(result, step[1], step[2]);
else if (method == "safeb64_decode") result = this.base64UrlDecode(result);
else if (method == "safeb64_encode") result = this.base64UrlEncode(result);
})
return result
}
async kaiEncrypt(id) {
var token = await this.patternExecutor("kai", "encrypt", id)
return token;
}
async kaiDecrypt(id) {
var token = await this.patternExecutor("kai", "decrypt", id)
return token;
}
async megaDecrypt(data) {
var streamData = await this.patternExecutor("megaup", "decrypt", data)
return JSON.parse(streamData);
}
}

View File

@@ -1,246 +0,0 @@
const mangayomiSources = [{
"name": "Animeonsen",
"langs": ["en", "ja"],
"baseUrl": "https://www.animeonsen.xyz",
"apiUrl": "https://api.animeonsen.xyz",
"iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://www.animeonsen.xyz",
"typeSource": "single",
"itemType": 1,
"version": "1.0.1",
"pkgPath": "anime/src/all/animeonsen.js"
}];
class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
getPreference(key) {
return new SharedPreferences().get(key);
}
async getToken() {
const preferences = new SharedPreferences();
var token_ts = parseInt(preferences.getString("animeosen_token_expiry_at", "0"))
var now_ts = parseInt(new Date().getTime() / 1000);
// token lasts for 7days but still checking after 6days
if (now_ts - token_ts > 60 * 60 * 24 * 6) {
var tokenBody = {
client_id: "f296be26-28b5-4358-b5a1-6259575e23b7",
client_secret: "349038c4157d0480784753841217270c3c5b35f4281eaee029de21cb04084235",
grant_type: "client_credentials"
}
var res = await this.client.post("https://auth.animeonsen.xyz/oauth/token", {}, tokenBody)
res = JSON.parse(res.body)
var token = res.access_token
preferences.setString("animeosen_token", token);
preferences.setString("animeosen_token_expiry_at", "" + now_ts);
return token
} else {
return preferences.getString("animeosen_token", "");
}
}
async getHeaders(slug) {
var brToken = ""
if (slug.endsWith("/search")) {
brToken = "0e36d0275d16b40d7cf153634df78bc229320d073f565db2aaf6d027e0c30b13"
}
else {
brToken = await this.getToken()
}
return {
'Authorization': `Bearer ${brToken}`,
'content-type': "application/json"
}
}
async request(slug, body = {}) {
var headers = await this.getHeaders(slug)
if (slug.endsWith("/search")) {
var api = `https://search.animeonsen.xyz${slug}`
var res = await this.client.post(api, headers, body)
return JSON.parse(res.body)
}
var api = `${this.source.apiUrl}/v4/content${slug}`
var res = await this.client.get(api, headers)
return JSON.parse(res.body)
}
animeContent(anime, pref_name, imgRes) {
var name_eng = anime.content_title_en
var name_jp = anime.content_title
var name = pref_name == "jpn" ? name_jp : name_eng;
var link = anime.content_id
var imageUrl = `${this.source.apiUrl}/v4/image/${imgRes}/${link}`
return { name, imageUrl, link };
}
async getHome(page) {
var limit = 20
var start = (page - 1) * limit;
var slug = `/index?start=${start}&limit=${limit}`
var res = await this.request(slug)
var pref_name = this.getPreference("animeonsen__pref_title_lang")
var imgRes = this.getPreference("animeonsen__pref_img_res_1")
var hasNextPage = res.cursor.next[0]
var list = []
for (var anime of res.content) {
list.push(this.animeContent(anime, pref_name, imgRes));
}
return { list, hasNextPage }
}
async getPopular(page) {
return await this.getHome(page)
}
get supportsLatest() {
throw new Error("supportsLatest not implemented");
}
async search(query, page, filters) {
var slug = "/indexes/content/search"
var limit = 30;
var offset = (page - 1) * limit;
var nextOffset = offset + limit;
var params = { limit, offset, q: query };
var res = await this.request(slug, params);
var estimatedTotalHits = res.estimatedTotalHits
var hasNextPage = estimatedTotalHits > nextOffset;
var list = []
var hits = res.hits
var pref_name = this.getPreference("animeonsen__pref_title_lang")
var imgRes = this.getPreference("animeonsen__pref_img_res_1")
if (hits.length > 0) {
for (var anime of hits) {
list.push(this.animeContent(anime, pref_name, imgRes));
}
}
return { list, hasNextPage }
}
statusCode(status) {
return {
"currently_airing": 0,
"finished_airing": 1,
}[status] ?? 5;
}
async getDetail(url) {
var linkSlug = `${this.source.baseUrl}/details/`
url = url.replace(linkSlug, "")
var link = `${linkSlug}${url}`
var detailsApiSlug = `/${url}/extensive`
var animeDetails = await this.request(detailsApiSlug);
var pref_name = this.getPreference("animeonsen_pref_ep_title_lang")
var imgRes = this.getPreference("animeonsen__pref_img_res_1")
var name_eng = animeDetails.content_title_en
var name_jp = animeDetails.content_title
var name = pref_name == "jpn" ? name_jp : name_eng;
var link = animeDetails.content_id
var imageUrl = `${this.source.apiUrl}/v4/image/${imgRes}/${link}`
var is_movie = animeDetails.is_movie
var mal_data = animeDetails.mal_data
var description = mal_data.synopsis
var genre = []
mal_data.genres.forEach(g => genre.push(g.name))
var status = this.statusCode(mal_data.status);
var chapters = [];
var episodeAPISlug = `/${url}/episodes`
var episodeDetails = await this.request(episodeAPISlug);
Object.keys(episodeDetails).forEach(ep => {
var ep_data = episodeDetails[ep]
var ep_name_eng = ep_data.contentTitle_episode_en
var ep_name_jp = ep_data.contentTitle_episode_jp
var ep_name = pref_name == "jpn" ? ep_name_jp : ep_name_eng;
chapters.push({
name:`E${ep}: ${ep_name}`,
url: `/${url}/video/${ep}`,
})
})
chapters.reverse()
return { name, imageUrl, status, description, genre,link, chapters }
}
// For anime episode video list
async getVideoList(url) {
var streamDetails = await this.request(url);
var streamData = streamDetails.uri
var streams = [
{
quality:`Default (720p)`,
url: streamData.stream,
originalUrl: streamData.stream
}
];
var subtitles = [];
var subData = streamDetails.subtitles;
Object.keys(subData).forEach(sub => {
subtitles.push({
label:sub,
file: subData[url]
})
});
streams[0].subtitles = subtitles
return streams
}
getSourcePreferences() {
return [{
key: 'animeonsen__pref_title_lang',
listPreference: {
title: 'Preferred title language',
summary: '',
valueIndex: 0,
entries: ["Japenese", "English"],
entryValues: ["jpn", "en"]
}
}, {
key: 'animeonsen_pref_ep_title_lang',
listPreference: {
title: 'Preferred episode title language',
summary: '',
valueIndex: 1,
entries: ["Japenese", "English"],
entryValues: ["jpn", "en"]
}
},{
key: 'animeonsen__pref_img_res_1',
listPreference: {
title: 'Preferred image resolution',
summary: '',
valueIndex: 1,
entries: ["Low", "Medium", "High"],
entryValues: ["210x300", "420x600", "840x1200"]
}
}];
}
}

View File

@@ -1,270 +0,0 @@
const mangayomiSources = [
{
"name": "AnimeParadise",
"lang": "en",
"baseUrl": "https://animeparadise.moe",
"apiUrl": "https://api.animeparadise.moe",
"iconUrl":
"https://www.google.com/s2/favicons?sz=128&domain=https://animeparadise.moe",
"typeSource": "single",
"itemType": 1,
"version": "0.0.2",
"pkgPath": "anime/src/en/animeparadise.js"
}
];
class DefaultExtension extends MProvider {
getPreference(key) {
const preferences = new SharedPreferences();
return preferences.get(key);
}
async extractFromUrl(url) {
var res = await new Client().get(this.source.baseUrl + url);
var doc = new Document(res.body);
var jsonData = doc.selectFirst("#__NEXT_DATA__").text;
return JSON.parse(jsonData).props.pageProps;
}
async requestAPI(slug) {
var api = `${this.source.apiUrl}/${slug}`;
var response = await new Client().get(api);
var body = JSON.parse(response.body);
return body;
}
async formList(slug) {
var jsonData = await this.requestAPI(slug);
var list = [];
if ("episodes" in jsonData) {
jsonData.episodes.forEach((item) => {
list.push({
"name": item.origin.title,
"link": item.origin.link,
"imageUrl": item.image,
});
});
} else {
jsonData.data.forEach((item) => {
list.push({
"name": item.title,
"link": item.link,
"imageUrl": item.posterImage.original,
});
});
}
return {
"list": list,
"hasNextPage": false,
};
}
async getPopular(page) {
return await this.formList('?sort={"rate": -1 }');
}
async getLatestUpdates(page) {
var slug = '?sort={"postDate": -1 }';
var choice = this.getPreference("animeparadise_pref_latest_tab");
if (choice === "recent_ep") slug = "ep/recently-added";
return await this.formList(slug);
}
async search(query, page, filters) {
var season = filters[0].values[filters[0].state].value;
var year = filters[1].values[filters[1].state].value;
var genre = "genre[]=";
for (var filter of filters[2].state) {
if (filter.state == true) genre += `${filter.value}&genre[]=`;
}
var slug = `search?q=${query}&year=${year}&season=${season}&${genre}`;
return await this.formList(slug);
}
statusCode(status) {
return (
{
"current": 0,
"finished": 1,
}[status] ?? 5
);
}
async getDetail(url) {
var linkSlug = this.source.baseUrl + `/anime/`;
if (url.includes(linkSlug)) url = url.replace(linkSlug, "");
var jsonData = await this.extractFromUrl(`/anime/${url}`);
jsonData = jsonData.data;
var details = {};
var chapters = [];
details.imageUrl = jsonData.posterImage.original;
details.description = jsonData.synopsys;
details.genre = jsonData.genres;
details.status = this.statusCode(jsonData.status);
var id = jsonData._id;
var epAPI = await this.requestAPI(`anime/${id}/episode`);
epAPI.data.forEach((ep) => {
var epName = `E${ep.number}: ${ep.title}`;
var epUrl = `${ep.uid}?origin=${ep.origin}`;
chapters.push({ name: epName, url: epUrl });
});
details.link = `${linkSlug}${url}`;
details.chapters = chapters.reverse();
return details;
}
// Sorts streams based on user preference.
async sortStreams(streams) {
var sortedStreams = [];
var copyStreams = streams.slice();
var pref = await this.getPreference("animeparadise_pref_video_resolution");
for (var stream of streams) {
if (stream.quality.indexOf(pref) > -1) {
sortedStreams.push(stream);
var index = copyStreams.indexOf(stream);
if (index > -1) {
copyStreams.splice(index, 1);
}
break;
}
}
return [...sortedStreams, ...copyStreams];
}
// Extracts the streams url for different resolutions from a hls stream.
async extractStreams(url) {
const response = await new Client().get(url);
const body = response.body;
const lines = body.split("\n");
var streams = [
{
url: url,
originalUrl: url,
quality: `Auto`,
},
];
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
var m3u8Url = lines[i + 1].trim();
m3u8Url = url.replace("master.m3u8", m3u8Url);
streams.push({
url: m3u8Url,
originalUrl: m3u8Url,
quality: resolution,
});
}
}
return streams;
}
// For anime episode video list
async getVideoList(url) {
var streams = [];
var jsonData = await this.extractFromUrl(`/watch/${url}`);
var epData = jsonData.episode;
streams = await this.extractStreams(epData.streamLink);
var subtitles = [];
epData.subData.forEach((sub) => {
subtitles.push({
"label": sub.label,
"file": `${this.source.apiUrl}/stream/file/${sub.src}`,
});
});
streams[0].subtitles = subtitles;
return streams;
}
addCatogory(arr, typ) {
arr = arr.map((x) => ({ type_name: typ, name: x, value: x }));
arr.unshift({
type_name: typ,
name: "All",
value: "",
});
return arr;
}
getFilterList() {
var seasons = ["Winter", "Spring", "Summer", "Fall"];
const currentYear = new Date().getFullYear();
var years = Array.from({ length: currentYear - 1939 }, (_, i) =>
(i + 1940).toString()
).reverse();
var genres = [
"Action",
"Adventure",
"Comedy",
"Drama",
"Ecchi",
"Fantasy",
"Horror",
"Mahou Shojo",
"Mecha",
"Music",
"Mystery",
"Psychological",
"Romance",
"Sci-Fi",
"Slice of Life",
"Sports",
"Supernatural",
"Thriller",
].map((x) => ({ type_name: "CheckBox", name: x, value: x }));
return [
{
type_name: "SelectFilter",
name: "Season",
state: 0,
values: this.addCatogory(seasons, "SelectOption"),
},
{
type_name: "SelectFilter",
name: "Year",
state: 0,
values: this.addCatogory(years, "SelectOption"),
},
{
type_name: "GroupFilter",
name: "Genres",
state: genres,
},
];
}
getSourcePreferences() {
return [
{
key: "animeparadise_pref_latest_tab",
listPreference: {
title: "Latest tab category",
summary: "Anime list to be shown in latest tab",
valueIndex: 0,
entries: ["Recently added anime", "Recently added episode"],
entryValues: ["recent_ani", "recent_ep"],
},
},
{
key: "animeparadise_pref_video_resolution",
listPreference: {
title: "Preferred video resolution",
summary: "",
valueIndex: 0,
entries: ["Auto", "1080p", "720p", "360p"],
entryValues: ["auto", "1080", "720", "360"],
},
},
];
}
}

View File

@@ -1,300 +0,0 @@
const mangayomiSources = [
{
"name": "AnimeZ",
"lang": "en",
"baseUrl": "https://animez.org",
"apiUrl": "",
"iconUrl":
"https://www.google.com/s2/favicons?sz=256&domain=https://animez.org/",
"typeSource": "multi",
"itemType": 1,
"version": "1.0.2",
"pkgPath": "anime/src/en/animez.js"
}
];
class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
getHeaders(url) {
return {
"Referer": this.source.baseUrl,
};
}
getPreference(key) {
return new SharedPreferences().get(key);
}
async request(slug) {
var url = this.source.baseUrl + slug;
var res = await this.client.get(url, this.getHeaders());
return new Document(res.body);
}
async page(slug) {
var body = await this.request(slug);
var list = [];
var hasNextPage = false;
var animes = body.select("li.TPostMv");
animes.forEach((anime) => {
var link = anime.selectFirst("a").getHref;
var name = anime.selectFirst("h2.Title").text;
var imageUrl =
this.source.baseUrl + "/" + anime.selectFirst("img").getSrc;
list.push({ name, link, imageUrl });
});
var paginations = body.select(".pagination > li");
hasNextPage =
paginations[paginations.length - 1].text == "Last" ? true : false;
return { list, hasNextPage };
}
sortByPref(key) {
var sort = parseInt(this.getPreference(key));
var sortBy = "hot";
switch (sort) {
case 1: {
sortBy = "lastest-chap";
break;
}
case 2: {
sortBy = "hot";
break;
}
case 3: {
sortBy = "lastest-manga";
break;
}
case 4: {
sortBy = "top-manga";
break;
}
case 5: {
sortBy = "top-month";
break;
}
case 6: {
sortBy = "top-week";
break;
}
case 7: {
sortBy = "top-day";
break;
}
case 8: {
sortBy = "follow";
break;
}
case 9: {
sortBy = "comment";
break;
}
case 10: {
sortBy = "num-chap";
break;
}
}
return sortBy;
}
async getPopular(page) {
var sortBy = this.sortByPref("animez_pref_popular_section");
var slug = `/?act=search&f[status]=all&f[sortby]=${sortBy}&&pageNum=${page}`;
return await this.page(slug);
}
get supportsLatest() {
throw new Error("supportsLatest not implemented");
}
async getLatestUpdates(page) {
var sortBy = this.sortByPref("animez_pref_latest_section");
var slug = `/?act=search&f[status]=all&f[sortby]=${sortBy}&&pageNum=${page}`;
return await this.page(slug);
}
async search(query, page, filters) {
var slug = `/?act=search&f[status]=all&f[keyword]=${query}&&pageNum=${page}`;
return await this.page(slug);
}
async getDetail(url) {
var baseUrl = this.source.baseUrl;
if (url.includes(baseUrl)) url = url.replace(baseUrl, "");
var link = +url;
var body = await this.request(url);
var name = body.selectFirst("#title-detail-manga").text;
var animeId = body.selectFirst("#title-detail-manga").attr("data-manga");
var genre = [];
body
.select("li.AAIco-adjust")[3]
.select("a")
.forEach((g) => genre.push(g.text));
var description = body.selectFirst("#summary_shortened").text;
var chapters = [];
var chapLen = 0;
var pageNum = 1;
var hasNextPage = true;
while (hasNextPage) {
var pageSlug = `?act=ajax&code=load_list_chapter&manga_id=${animeId}&page_num=${pageNum}&chap_id=0&keyword=`;
var pageBody = await this.request(pageSlug);
var parsedBody = JSON.parse(pageBody.html);
var nav = parsedBody.nav;
if (nav == null) {
// if "nav" doesnt exists there is no next page
hasNextPage = false;
} else {
var navLi = new Document(nav).select(".page-link.next").length;
if (navLi > 0) {
// if "nav" exists and has li.next then there is next page
pageNum++;
} else {
// if "nav" exists and doesn't have li.next then there is no next page
hasNextPage = false;
}
}
var list_chap = new Document(parsedBody.list_chap).select(
"li.wp-manga-chapter"
);
list_chap.forEach((chapter) => {
var a = chapter.selectFirst("a");
var title = a.text;
var epLink = a.getHref;
var scanlator = "Sub";
if (title.indexOf("Dub") > 0) {
title = title.replace("-Dub", "");
scanlator = "Dub";
}
title = title.indexOf("Movie") > -1 ? title : `Episode ${title}`;
var epData = {
name: title,
url: epLink,
scanlator,
};
if (chapLen > 0) {
var pos = chapLen - 1;
var lastEntry = chapters[pos];
if (lastEntry.name == epData.name) {
// if last entries name is same then append url and scanlator to last entry
chapters.pop(); // remove the last entry
epData.url = `${epData.url}||${lastEntry.url}`;
epData.scanlator = `${lastEntry.scanlator}, ${epData.scanlator}`;
chapLen = pos; // since the last entry is removed the chapLen will decrease
}
}
chapters.push(epData);
chapLen++;
});
}
return {
link,
description,
chapters,
genre,
};
}
// Sorts streams based on user preference.
sortStreams(streams) {
var sortedStreams = [];
var copyStreams = streams.slice();
var pref = this.getPreference("animez_pref_stream_audio");
for (var stream of streams) {
if (stream.quality.indexOf(pref) > -1) {
sortedStreams.push(stream);
var index = copyStreams.indexOf(stream);
if (index > -1) {
copyStreams.splice(index, 1);
}
break;
}
}
return [...sortedStreams, ...copyStreams];
}
// For anime episode video list
async getVideoList(url) {
var linkSlugs = url.split("||");
var streams = [];
for (var slug of linkSlugs) {
var body = await this.request(slug);
var iframeSrc = body.selectFirst("iframe").getSrc;
var streamLink = iframeSrc.replace("/embed/", "/anime/");
var audio = slug.indexOf("dub-") > -1 ? "Dub" : "Sub";
streams.push({
url: streamLink,
originalUrl: streamLink,
quality: audio,
});
}
return sortStreams(streams);
}
getSourcePreferences() {
return [
{
key: "animez_pref_popular_section",
listPreference: {
title: "Preferred popular content",
summary: "",
valueIndex: 1,
entries: [
"Latest update",
"Hot",
"New releases",
"Top all",
"Top month",
"Top week",
"Top day",
"Top follow",
"Top comments",
"Number of episodes",
],
entryValues: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
},
},
{
key: "animez_pref_latest_section",
listPreference: {
title: "Preferred latest content",
summary: "",
valueIndex: 0,
entries: [
"Latest update",
"Hot",
"New releases",
"Top all",
"Top month",
"Top week",
"Top day",
"Top follow",
"Top comments",
"Number of episodes",
],
entryValues: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
},
},
{
key: "animez_pref_stream_audio",
listPreference: {
title: "Preferred stream audio",
summary: "",
valueIndex: 0,
entries: ["Sub", "Dub"],
entryValues: ["Sub", "Dub"],
},
},
];
}
}

View File

@@ -1,705 +0,0 @@
const mangayomiSources = [{
"name": "Aniplay",
"lang": "en",
"baseUrl": "https://aniplaynow.live",
"apiUrl": "https://aniplaynow.live",
"iconUrl": "https://www.google.com/s2/favicons?sz=128&domain=https://aniplaynow.live/",
"typeSource": "single",
"itemType": 1,
"version": "1.4.5",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/en/aniplay.js"
}];
class DefaultExtension extends MProvider {
constructor() {
super();
this.client = new Client();
}
getPreference(key) {
const preferences = new SharedPreferences();
return preferences.get(key);
}
getBaseUrl() {
return "https://" + this.getPreference("aniplay_override_base_url")
}
// code from torrentioanime.js
anilistQuery() {
return `
query ($page: Int, $perPage: Int, $sort: [MediaSort], $search: String) {
Page(page: $page, perPage: $perPage) {
pageInfo {
currentPage
hasNextPage
}
media(type: ANIME, sort: $sort, search: $search, status_in: [RELEASING, FINISHED, NOT_YET_RELEASED]) {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
`.trim();
}
// code from torrentioanime.js
anilistLatestQuery() {
const currentTimeInSeconds = Math.floor(Date.now() / 1000);
return `
query ($page: Int, $perPage: Int, $sort: [AiringSort]) {
Page(page: $page, perPage: $perPage) {
pageInfo {
currentPage
hasNextPage
}
airingSchedules(
airingAt_greater: 0
airingAt_lesser: ${currentTimeInSeconds - 10000}
sort: $sort
) {
media {
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
countryOfOrigin
isAdult
}
}
}
}
`.trim();
}
// code from torrentioanime.js
async getAnimeDetails(anilistId) {
const query = `
query($id: Int){
Media(id: $id){
id
title {
romaji
english
native
}
coverImage {
extraLarge
large
}
description
status
tags {
name
}
genres
studios {
nodes {
name
}
}
format
countryOfOrigin
isAdult
}
}
`.trim();
const variables = JSON.stringify({ id: anilistId });
const res = await this.makeGraphQLRequest(query, variables);
const media = JSON.parse(res.body).data.Media;
const anime = {};
anime.name = (() => {
var preferenceTitle = this.getPreference("aniplay_pref_title");
switch (preferenceTitle) {
case "romaji":
return media?.title?.romaji || "";
case "english":
return media?.title?.english?.trim() || media?.title?.romaji || "";
case "native":
return media?.title?.native || "";
default:
return "";
}
})();
anime.imageUrl = media?.coverImage?.extraLarge ||
media?.coverImage?.large || "";
anime.description = (media?.description || "No Description")
.replace(/<br><br>/g, "\n")
.replace(/<.*?>/g, "");
anime.status = (() => {
switch (media?.status) {
case "RELEASING":
return 0;
case "FINISHED":
return 1;
case "HIATUS":
return 2;
case "NOT_YET_RELEASED":
return 3;
default:
return 5;
}
})();
const tagsList = media?.tags?.map(tag => tag.name).filter(Boolean) || [];
const genresList = media?.genres || [];
anime.genre = [...new Set([...tagsList, ...genresList])].sort();
const studiosList = media?.studios?.nodes?.map(node => node.name).filter(Boolean) || [];
anime.author = studiosList.sort().join(", ");
anime.format = media.format
return anime;
}
// code from torrentioanime.js
async makeGraphQLRequest(query, variables) {
const res = await this.client.post("https://graphql.anilist.co", {},
{
query, variables
});
return res;
}
// code from torrentioanime.js
parseSearchJson(jsonLine, isLatestQuery = false) {
const jsonData = JSON.parse(jsonLine);
jsonData.type = isLatestQuery ? "AnilistMetaLatest" : "AnilistMeta";
const metaData = jsonData;
const mediaList = metaData.type == "AnilistMeta"
? metaData.data?.Page?.media || []
: metaData.data?.Page?.airingSchedules.map(schedule => schedule.media) || [];
const hasNextPage = metaData.type == "AnilistMeta" || metaData.type == "AnilistMetaLatest"
? metaData.data?.Page?.pageInfo?.hasNextPage || false
: false;
const animeList = mediaList
.filter(media => !((media?.countryOfOrigin === "CN" || media?.isAdult) && isLatestQuery))
.map(media => {
const anime = {};
anime.link = media?.id?.toString() || "";
anime.name = (() => {
var preferenceTitle = this.getPreference("aniplay_pref_title");
switch (preferenceTitle) {
case "romaji":
return media?.title?.romaji || "";
case "english":
return media?.title?.english?.trim() || media?.title?.romaji || "";
case "native":
return media?.title?.native || "";
default:
return "";
}
})();
anime.imageUrl = media?.coverImage?.extraLarge ||
media?.coverImage?.large || "";
return anime;
});
return { "list": animeList, "hasNextPage": hasNextPage };
}
async getPopular(page) {
const variables = JSON.stringify({
page: page,
perPage: 30,
sort: "TRENDING_DESC"
});
const res = await this.makeGraphQLRequest(this.anilistQuery(), variables);
return this.parseSearchJson(res.body)
}
async getLatestUpdates(page) {
const variables = JSON.stringify({
page: page,
perPage: 30,
sort: "TIME_DESC"
});
const res = await this.makeGraphQLRequest(this.anilistLatestQuery(), variables);
return this.parseSearchJson(res.body, true)
}
async search(query, page, filters) {
const variables = JSON.stringify({
page: page,
perPage: 30,
sort: "POPULARITY_DESC",
search: query
});
const res = await this.makeGraphQLRequest(this.anilistQuery(), variables);
return this.parseSearchJson(res.body)
}
get supportsLatest() {
throw new Error("supportsLatest not implemented");
}
async aniplayRequest(slug, body) {
var baseUrl = this.getBaseUrl()
var next_action_overrides = await this.extractKeys(baseUrl)
var next_action = ""
if (slug.indexOf("info/") > -1) {
next_action = next_action_overrides["getEpisodes"]
} else if (slug.indexOf("watch/") > -1) {
next_action = next_action_overrides["getSources"]
}
var url = `${baseUrl}/anime/${slug}`
var headers = {
"referer": baseUrl,
'next-action': next_action,
"Content-Type": "application/json",
}
var response = await this.client.post(url, headers, body);
if (response.statusCode != 200) {
throw new Error("Error: " + response.statusText);
}
return JSON.parse(response.body.split('1:')[1])
}
async getDetail(url) {
var anilistId = url
if (url.includes("info/")) {
anilistId = url.substring(url.lastIndexOf("info/") + 5)
}
var animeData = await this.getAnimeDetails(anilistId)
var slug = `info/${anilistId}`
var body = [anilistId, true, false]
var result = await this.aniplayRequest(slug, body)
if (result.length < 1) {
throw new Error("Error: No data found for the given URL");
}
var chapters = []
var chaps = {}
for (var item of result) {
var providerId = item["providerId"]
// Hika has bunch of embeds, not stream url, so avoiding it for now
if (providerId == "hika") continue
var episodes = item['episodes']
for (var episode of episodes) {
var id = episode.id
var number = episode.number.toString()
var chap = chaps.hasOwnProperty(number) ? chaps[number] : {}
var updatedAt = episode.hasOwnProperty("updatedAt") ? episode.updatedAt.toString() : null
var title = episode.hasOwnProperty("title") ? episode.title : ""
var isFiller = episode.hasOwnProperty("isFiller") ? episode.isFiller : false
var hasDub = episode.hasOwnProperty("hasDub") ? episode.hasDub : false
chap.title = title == "" ? chap.title : title
chap.isFiller = isFiller || chap.isFiller
chap.hasDub = hasDub || chap.hasDub
chap.updatedAt = updatedAt ?? chap.updatedAt
var prvds = chap.hasOwnProperty("prvds") ? chap["prvds"] : {}
prvds[providerId] = { anilistId, providerId, id, number, hasDub }
chap["prvds"] = prvds
chaps[number] = chap
}
}
var markFillers = this.getPreference("aniplay_pref_mark_filler")
for (var episodeNum in chaps) {
var chap = chaps[episodeNum]
var title = chap.title
var dateUpload = chap.updatedAt
var scanlator = "SUB"
if (chap.hasDub) {
scanlator += ", DUB"
}
var isFillers = chap.isFiller
if (markFillers && isFillers) {
scanlator = "FILLER, " + scanlator
}
var epData = JSON.stringify(chap["prvds"])
chapters.push({ name: `E${episodeNum}: ${title}`, url: epData, dateUpload, scanlator })
}
var format = animeData.format
if (format === "MOVIE") chapters[0].name = "Movie"
var baseUrl = this.getBaseUrl()
animeData.link = `${baseUrl}/anime/${slug}`
animeData.chapters = chapters.reverse()
return animeData
}
// For anime episode video list
async getVideoList(url) {
var providerInfos = JSON.parse(url)
var pref_provider = this.getPreference("aniplay_pref_provider_4")
var providers = Object.keys(providerInfos)
if (pref_provider == "any") {
//any = randomly choose one
var randomIndex = Math.floor(Math.random() * providers.length);
providers = [providers[randomIndex]]
} else if (pref_provider == "pahe") {
//pahe = if "pahe" is available then chose it
if (providerInfos.hasOwnProperty("pahe")) {
providers = ["pahe"]
}
} else if (pref_provider == "yuki") {
//yuki = if "yuki" is available then chose it
if (providerInfos.hasOwnProperty("yuki")) {
providers = ["yuki"]
}
}
var finalStreams = []
var user_audio_type = this.getPreference("aniplay_pref_audio_type_1")
for (var provider of providers) {
var streams = []
var providerInfo = providerInfos[provider]
var anilistId = providerInfo.anilistId
var providerId = providerInfo.providerId
var id = providerInfo.id
var number = providerInfo.number
var hasDub = providerInfo.hasDub
var audios = []
// if there "sub" is prefered or there are no preference then add sub
if (user_audio_type.includes("sub") || user_audio_type.length < 1) audios.push("sub")
if (hasDub && user_audio_type.includes("dub")) audios.push("dub")
var slug = `watch/${anilistId}`
for (var audio of audios) {
var body = [
anilistId,
providerId,
id,
number,
audio
]
var result = await this.aniplayRequest(slug, body)
if (result === null) {
continue
}
if (providerId == "yuki") {
// Yuki always has softsubs aka subtitles are seperate.
streams = await this.getYukiStreams(result, "soft" + audio)
} else if (providerId == "pahe") {
// Pahe always has hardsubs aka subtitles printed on video.
streams = await this.getPaheStreams(result, "hard" + audio)
} else {
continue
}
if (this.getPreference("aniplay_proxy")) streams = this.streamProxy(providerId, streams)
var sortedStreams = this.sortStreams(streams)
finalStreams = [...sortedStreams, ...finalStreams]
}
}
if (finalStreams.length < 1) {
throw new Error("Error: No data found for the given URL");
}
return finalStreams
}
getSourcePreferences() {
return [{
key: 'aniplay_override_base_url',
listPreference: {
title: 'Override base url',
summary: '',
valueIndex: 0,
entries: ["aniplaynow.live (Main)", "aniplay.lol (Backup)"],
entryValues: ["aniplaynow.live", "aniplay.lol"]
}
},
{
key: "aniplay_pref_title",
listPreference: {
title: "Preferred Title",
summary: "",
valueIndex: 0,
entries: ["Romaji", "English", "Native"],
entryValues: ["romaji", "english", "native"],
}
},
{
key: "aniplay_pref_provider_4",
listPreference: {
title: "Preferred provider",
summary: "",
valueIndex: 1,
entries: ["Any", "All", "Yuki", "Pahe"],
entryValues: ["any", "all", "yuki", "pahe"],
}
}, {
key: "aniplay_pref_mark_filler",
switchPreferenceCompat: {
title: "Mark filler episodes",
summary: "Filler episodes will be marked with (F)",
value: false
}
},
{
key: "aniplay_pref_audio_type_1",
multiSelectListPreference: {
title: 'Preferred stream sub/dub type',
summary: '',
values: ["sub"],
entries: ["Sub", "Dub"],
entryValues: ["sub", "dub"]
}
}, {
key: "aniplay_pref_extract_streams",
switchPreferenceCompat: {
'title': 'Split stream into different quality streams',
summary: "Split stream Auto into 360p/720p/1080p",
value: true
}
}, {
key: 'aniplay_pref_video_resolution',
listPreference: {
title: 'Preferred video resolution',
summary: '',
valueIndex: 0,
entries: ["Auto", "1080p", "720p", "480p", "360p"],
entryValues: ["auto", "1080", "720", "480", "360"]
}
}, {
key: "aniplay_proxy",
switchPreferenceCompat: {
title: 'Enable stream proxy',
summary: "",
value: false
}
}, {
key: "aniplay_stream_proxy",
editTextPreference: {
title: "Override stream proxy url",
summary: "https://prox.aniplaynow.live",
value: "https://prox.aniplaynow.live",
dialogTitle: "Override stream proxy url",
dialogMessage: "",
}
}
]
}
// ----------- Stream manipulations -------
// Sorts streams based on user preference.
sortStreams(streams) {
var sortedStreams = [];
var copyStreams = streams.slice()
var pref = this.getPreference("aniplay_pref_video_resolution");
for (var stream of streams) {
if (stream.quality.indexOf(pref) > -1) {
sortedStreams.push(stream);
var index = copyStreams.indexOf(stream);
if (index > -1) {
copyStreams.splice(index, 1);
}
break;
}
}
return [...sortedStreams, ...copyStreams]
}
// Adds proxy to streams
streamProxy(providerId, streams) {
var proxyBaseUrl = this.getPreference("aniplay_stream_proxy");
var slug = "/fetch?url="
var ref = "&ref="
if (providerId == "yuki") {
ref += "https://hianime.to"
} else if (providerId == "pahe") {
ref += "https://kwik.si"
}
streams.forEach(stream => {
stream.url = proxyBaseUrl + slug + stream.url + ref;
stream.originalUrl = proxyBaseUrl + slug + stream.originalUrl + ref;
});
return streams
}
// Extracts the streams url for different resolutions from a hls stream.
async extractStreams(url, audio, providerId, hdr = {}) {
audio = audio.toUpperCase()
var streams = [{
url: url,
originalUrl: url,
quality: `Auto - ${providerId} : ${audio}`,
headers: hdr
}];
var doExtract = this.getPreference("aniplay_pref_extract_streams");
// Pahe only has auto
if (providerId === "pahe" || !doExtract) {
return streams;
}
const response = await this.client.get(url, hdr);
const body = response.body;
const lines = body.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-STREAM-INF:')) {
var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1];
var m3u8Url = lines[i + 1].trim();
if (providerId === "yuki") {
var orginalUrl = url
m3u8Url = orginalUrl.replace("master.m3u8", m3u8Url)
}
streams.push({
url: m3u8Url,
originalUrl: m3u8Url,
quality: `${resolution} - ${providerId} : ${audio}`,
headers: hdr
});
}
}
return streams
}
async getYukiStreams(result, audio) {
var m3u8Url = result.sources[0].url
var streams = await this.extractStreams(m3u8Url, audio, "yuki");
var subtitles = []
result.subtitles.forEach(sub => {
var label = sub.label
if (label.indexOf("thumbnail") < 0) { // thumbnails shouldnt be included
subtitles.push({
"label": sub.lang,
"file": sub.url,
});
}
})
streams[0].subtitles = subtitles
return streams
}
async getPaheStreams(result, audio) {
var m3u8Url = result.sources[0].url
var hdr = result.headers;
return await this.extractStreams(m3u8Url, audio, "pahe", hdr);
}
//------------ Extract keys ---------------
async extractKeys(baseUrl) {
const preferences = new SharedPreferences();
let KEYS = preferences.getString("aniplay_keys", "");
var KEYS_TS = parseInt(preferences.getString("aniplay_keys_ts", "0"));
var now_ts = parseInt(new Date().getTime() / 1000);
// Checks for keys every 60 minutes and baseUrl is present in the map
if (now_ts - KEYS_TS < 60 * 60 && KEYS.includes(baseUrl)) {
return JSON.parse(KEYS)
}
var randomAnimeUrl = baseUrl + "/anime/watch/1"
var res = (await this.client.get(randomAnimeUrl)).body
var sKey = "/_next/static/chunks/app/(user)/(media)/"
var eKey = '"'
var start = res.indexOf(sKey) + sKey.length
var end = res.indexOf(eKey, start)
var jsSlug = res.substring(start, end)
var jsUrl = baseUrl + sKey + jsSlug
res = (await this.client.get(jsUrl)).body
var regex = /\(0,\w+\.createServerReference\)\("([a-f0-9]+)",\w+\.callServer,void 0,\w+\.findSourceMapURL,"(getSources|getEpisodes)"\)/g;
var matches = [...res.matchAll(regex)];
var keysMap = {};
matches.forEach(match => {
var hashId = match[1];
var functionName = match[2];
keysMap[functionName] = hashId;
});
keysMap["baseUrl"] = baseUrl;
preferences.setString("aniplay_keys", JSON.stringify(keysMap));
preferences.setString("aniplay_keys_ts", now_ts.toString());
return keysMap;
}
// End
}

View File

@@ -1,312 +0,0 @@
const mangayomiSources = [{
"name": "Gojo",
"lang": "en",
"baseUrl": "https://gojo.wtf",
"apiUrl": "",
"iconUrl": "https://www.google.com/s2/favicons?sz=128&domain=https://gojo.wtf/",
"typeSource": "multi",
"itemType": 1,
"version": "0.0.6",
"pkgPath": "anime/src/en/gojo.js"
}];
class DefaultExtension extends MProvider {
getHeaders() {
return {
'Referer': this.source.baseUrl,
'Origin': this.source.baseUrl,
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4084.56 Safari/537.3"
}
}
constructor() {
super();
this.client = new Client();
}
getPreference(key) {
const preferences = new SharedPreferences();
return preferences.get(key);
}
async gojoAPI(slug) {
var url = `https://backend.gojo.wtf/api/anime${slug}`
var res = await this.client.post(url, this.getHeaders())
if (res.statusCode != 200) return null
return JSON.parse(res.body)
}
getTitle(data) {
var pref = this.getPreference('gojo_pref_title')
if (data.hasOwnProperty(pref)) {
return data[pref]
}
return data['romaji']
}
formatList(animeList) {
var list = []
//
animeList.forEach(anime => {
var name = this.getTitle(anime.title)
var image = anime.coverImage
var imageUrl = ""
if (typeof (image) == 'object' && image.hasOwnProperty('large')) {
imageUrl = image.large
} else {
imageUrl = image
}
var link = "" + anime.id
list.push({ name, imageUrl, link });
})
return list
}
async getPopular(page) {
var list = []
var res = await this.gojoAPI("/home")
if (res != null) {
list.push(...this.formatList(res.popular))
list.push(...this.formatList(res.trending))
list.push(...this.formatList(res.seasonal))
list.push(...this.formatList(res.top))
}
return { list, hasNextPage: true }
}
async getLatestUpdates(page) {
var list = []
var res = await this.gojoAPI(`/recent?type=anime&page=${page}&perPage=30`)
if (res != null) {
list.push(...this.formatList(res))
}
var hasNextPage = true;
if (list.length < 30) hasNextPage = false;
return { list, hasNextPage }
}
async search(query, page, filters) {
var list = []
var hasNextPage = false;
var res = await this.gojoAPI(`/search?query=${query}&page=${page}&perPage=30`)
if (res != null) {
list.push(...this.formatList(res.results))
if (res.lastPage < page) hasNextPage = true;
}
return { list, hasNextPage }
}
async getDetail(url) {
var linkSlug = `${this.source.baseUrl}/watch/`
if (url.includes(linkSlug)) url = url.replace(linkSlug, "");
var anilistId = url
var res = await this.gojoAPI(`/info/${anilistId}`)
if (res == null) {
throw new Error("Error on getDetail");
}
var name = this.getTitle(res.title)
var imageUrl = res.coverImage.large
var description = res.description;
var link = `${linkSlug}${anilistId}`
var genres = res.genres
var status = (() => {
switch (res.status) {
case "RELEASING":
return 0;
case "FINISHED":
return 1;
case "HIATUS":
return 2;
case "NOT_YET_RELEASED":
return 3;
default:
return 5;
}
})();
var chapters = [];
var body = await this.gojoAPI(`/episodes/${anilistId}`)
if (body != null && body.length > 0) {
// Find the maximum episodes as some providers may not have all.
var maxEpisodes = 0
for (var prd of body) {
if (prd['episodes'].length > maxEpisodes) {
maxEpisodes = prd['episodes'].length;
}
}
for (var i = 0; i < maxEpisodes; i++) {
var chapNum = -1
var chapName = ""
var chapLink = {}
var chapScan = "Sub"
for (var prd of body) {
var chap = prd.episodes[i];
// Check if the current provider episode is the same as the previous one.
// If not, break out of the loop.
var epNum = chap.number
if (chapNum == -1) {
chapNum = epNum
}
if (chapNum != epNum) continue;
// Episode Name is stored only once.
if (chapName.length == 0) {
chapName = `E${epNum}`
if (chap.hasOwnProperty("title")) {
if (chap.title != null) chapName += ": " + chap.title;
}
}
// If Dub is available, add it to the scanlator list.
if (chap.hasOwnProperty("hasDub")) {
if (!(chapScan.includes("Dub")) && chap.hasDub) {
chapScan += ", Dub"
}
}
// If isFiller is available, add it to the scanlator list.
if (chap.hasOwnProperty("isFiller")) {
if (!(chapScan.includes("Filler")) && chap.isFiller && this.getPreference("gojo_pref_mark_filler")) {
chapScan = "Filler, " + chapScan
}
}
// Delete unnecessary properties from the chapter object.
delete chap.image
delete chap.description
delete chap.isFiller
delete chap.title
var prdName = prd.providerId
chapLink[prdName] = chap
}
chapters.push({ name: chapName, url: `${anilistId}||` + JSON.stringify(chapLink), scanlator: chapScan })
}
}
chapters.reverse()
return { name, imageUrl, description, link, chapters, genres, status }
}
strixNzazaExtractor(res, prvd, type) {
if(res == null) return {}
var src = res.sources[0]
var url = src.url
var quality = `${prvd} - ${src.quality} - ${type.toUpperCase()}`
return {
url: url,
quality,
originalUrl: url
}
}
paheExtractor(res, type) {
var streams = []
if (res != null) {
var srcs = res.sources
var hdr = this.getHeaders()
for (var src of srcs) {
var url = src.url
var quality = `Pahe - ${src.quality} - ${type.toUpperCase()}`
streams.push({
url: url,
headers: hdr,
quality,
originalUrl: url
})
}
}
return streams
}
async getStream(prvd, anilistId, epNum, subType, id, dub_id) {
var slug = `/tiddies?provider=${prvd}&id=${anilistId}&num=${epNum}&subType=${subType}&watchId=${id}&dub_id=${dub_id}`
return await this.gojoAPI(slug)
}
// For anime episode video list
async getVideoList(url) {
var split = url.split('||')
var anilistId = split[0]
var info = JSON.parse(split[1])
var streams = []
var extractDubs = this.getPreference("gojo_extract_dub_streams")
for (var prvd in info) {
var prd = info[prvd]
var epNum = prd.number
var subType = "sub"
var id = prd.id
var dub_id = null
var res = await this.getStream(prvd, anilistId, epNum, subType, id, dub_id)
if (prvd != "pahe") {
streams.push(this.strixNzazaExtractor(res, prvd, subType))
} else {
streams.push(...this.paheExtractor(res, subType))
}
if(!extractDubs) continue
subType = "dub"
if (prd.hasOwnProperty("dub_id")) dub_id = prd.dub_id
var res = await this.getStream(prvd, anilistId, epNum, subType, id, dub_id)
if (prvd != "pahe") {
streams.push(this.strixNzazaExtractor(res, prvd, subType))
} else {
streams.push(...this.paheExtractor(res, subType))
}
}
return streams
}
getSourcePreferences() {
return [
{
key: "gojo_pref_title",
listPreference: {
title: "Preferred Title",
summary: "",
valueIndex: 0,
entries: ["Romaji", "English", "Native"],
entryValues: ["romaji", "english", "native"],
}
}, {
key: "gojo_pref_mark_filler",
switchPreferenceCompat: {
title: "Mark filler episodes",
summary: "",
value: true
}
}, {
key: "gojo_extract_dub_streams",
switchPreferenceCompat: {
title: 'Extract dub streams',
summary: "",
value: false
}
},
]
}
}

View File

@@ -1,292 +0,0 @@
const mangayomiSources = [{
"name": "Sudatchi",
"lang": "en",
"baseUrl": "https://sudatchi.com",
"apiUrl": "",
"iconUrl": "https://www.google.com/s2/favicons?sz=128&domain=https://sudatchi.com",
"typeSource": "single",
"version": "1.1.1",
"dateFormat": "",
"dateFormatLocale": "",
"itemType": 1,
"pkgPath": "anime/src/en/sudatchi.js"
}];
class DefaultExtension extends MProvider {
getHeaders(url) {
return {
"Referer": this.source.baseUrl,
}
}
getPreference(key) {
const preferences = new SharedPreferences();
return preferences.get(key);
}
getUrl(slug) {
return `https://ipfs.sudatchi.com/ipfs/${slug}`
}
async requestApi(slug) {
var url = this.source.baseUrl + "/api" + slug
var res = await new Client().get(url, this.getHeaders());
return JSON.parse(res.body);
}
async formListForAnilist(animes) {
var list = []
var lang = this.getPreference("sudatchi_pref_lang")
for (var item of animes) {
var titles = item.title
var name = titles.romaji
switch (lang) {
case "e": {
name = titles.english != null ? titles.english : name;
break;
}
case "j": {
name = titles.native != null ? titles.native : name;
break;
}
}
var link = item.id
var coverImage = item.coverImage
var imageUrl = "large" in coverImage ? coverImage.large : coverImage.medium
list.push({
name,
imageUrl,
link: `${link}`
});
}
return list;
}
async formList(animes) {
var list = []
var lang = this.getPreference("sudatchi_pref_lang")
for (var item of animes) {
var details = "Anime" in item ? item.Anime : item
var name = "titleRomanji" in details ? details.titleRomanji : details.title
switch (lang) {
case "e": {
name = "titleEnglish" in details ? details.titleEnglish : name;
break;
}
case "j": {
name = "titleJapanese" in details ? details.titleJapanese : name;
break;
}
}
var link = "anilistId" in details ? details.anilistId : details.id
var imageUrl = "coverImage" in details ? details.coverImage : this.getUrl(details.imgUrl)
list.push({
name,
imageUrl,
link: `${link}`
});
}
return list;
}
async getPopular(page) {
var pageProps = await this.requestApi("/fetchHomeData")
// var = extract
var latestEpisodes = await this.formList(pageProps.latestEpisodes)
var latestAnimes = await this.formListForAnilist(pageProps.ongoingAnimes)
var animeSpotlight = await this.formList(pageProps.AnimeSpotlight)
var list = [...animeSpotlight, ...latestAnimes, ...latestEpisodes]
return {
list,
hasNextPage: false
};
}
get supportsLatest() {
throw new Error("supportsLatest not implemented");
}
async getLatestUpdates(page) {
var pageProps = await this.requestApi("/fetchHomeData")
var list = await this.formList(pageProps.latestEpisodes)
return {
list,
hasNextPage: false
};
}
async search(query, page, filters) {
var body = await this.requestApi("/fetchAnime",);
var url = this.source.baseUrl + "/api/fetchAnime"
var res = await new Client().post(url, this.getHeaders(), { "query": query });
var body = JSON.parse(res.body);
var list = await this.formListForAnilist(body.results)
var hasNextPage = body.pages > page ? true : false;
return {
list,
hasNextPage
};
}
statusCode(status) {
return {
"Currently Airing": 0,
"Finished Airing": 1,
"Hiatus": 2,
"Discontinued": 3,
"Not Yet Released": 4,
}[status] ?? 5;
}
async getDetail(url) {
var linkSlug = "https://sudatchi.com/anime/"
if (url.includes(linkSlug)) url = url.replace(linkSlug, "");
var lang = this.getPreference("sudatchi_pref_lang")
var link = `${linkSlug}${url}`
var details = await this.requestApi(`/anime/${url}`);
var titles = details.title
var name = titles.romaji
switch (lang) {
case "e": {
name = titles.english != null ? titles.english : name;
break;
}
case "j": {
name = titles.native != null ? titles.native : name;
break;
}
}
var description = details.description
var status = this.statusCode(details.status)
var imageUrl = details.coverImage
var genre = details.genres
var chapters = []
var episodes = details.episodes
if (episodes.length > 0) {
var typeId = details.format
if (typeId == "MOVIE") {
var number = episodes[0].number
var epUrl = `${url}/${number}`
chapters.push({ name: "Movie", url: epUrl })
} else {
for (var eObj of episodes) {
var epName = eObj.title
var number = eObj.number
var epUrl = `${url}/${number}`
chapters.push({ name: epName, url: epUrl })
}
}
}
chapters.reverse()
return { name, description, status, imageUrl, genre, chapters, link }
}
// For novel html content
async getHtmlContent(url) {
throw new Error("getHtmlContent not implemented");
}
// Clean html up for reader
async cleanHtmlContent(html) {
throw new Error("cleanHtmlContent not implemented");
}
async extractStreams(url) {
const response = await new Client().get(url);
const body = response.body;
const lines = body.split('\n');
var audios = []
var streams = [{
url: url,
originalUrl: url,
quality: "auto",
}];
for (let i = 0; i < lines.length; i++) {
var currentLine = lines[i]
if (currentLine.startsWith('#EXT-X-STREAM-INF:')) {
var resolution = currentLine.match(/RESOLUTION=(\d+x\d+)/)[1];
var m3u8Url = lines[i + 1].trim();
m3u8Url = m3u8Url.replace("./", `${url}/`)
streams.push({
url: m3u8Url,
originalUrl: m3u8Url,
quality: resolution,
});
} else if (currentLine.startsWith('#EXT-X-MEDIA:TYPE=AUDIO')) {
var attributesString = currentLine.split(",")
var attributeRegex = /([A-Z-]+)=("([^"]*)"|[^,]*)/g;
let match;
var trackInfo = {};
while ((match = attributeRegex.exec(attributesString)) !== null) {
var key = match[1];
var value = match[3] || match[2];
if (key === "NAME") {
trackInfo.label = value
} else if (key === "URI") {
trackInfo.file = value
}
}
audios.push(trackInfo);
}
}
streams[0].audios = audios
return streams
}
// For anime episode video list
async getVideoList(url) {
var jsonData = await this.requestApi(`/episode/${url}`);
var episodeData = jsonData.episode;
var epId = episodeData.id;
var epLink = `https://sudatchi.com/videos/m3u8/episode-${epId}.m3u8`
var streams = await this.extractStreams(epLink);
var subs = JSON.parse(jsonData.subtitlesJson)
var subtitles = [];
for (var sub of subs) {
var file = `https://ipfs.sudatchi.com${sub.url}`
var label = sub.SubtitlesName.name;
subtitles.push({ file: file, label: label });
}
streams[0].subtitles = subtitles
return streams;
}
// For manga chapter pages
async getPageList() {
throw new Error("getPageList not implemented");
}
getFilterList() {
throw new Error("getFilterList not implemented");
}
getSourcePreferences() {
return [{
key: 'sudatchi_pref_lang',
listPreference: {
title: 'Preferred title language',
summary: '',
valueIndex: 0,
entries: ["Romanji", "English", "Japanese"],
entryValues: ["r", "e", "j"]
}
},]
}
}

View File

@@ -1,937 +0,0 @@
const mangayomiSources = [{
"name": "AnimeFénix",
"lang": "es",
"baseUrl": "https://www3.animefenix.tv",
"apiUrl": "",
"iconUrl": "https://www3.animefenix.tv/themes/fenix-neo/images/AveFenix.png",
"typeSource": "single",
"itemType": 1,
"version": "0.1.13",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/es/animefenix.js"
}];
class DefaultExtension extends MProvider {
constructor () {
super();
this.client = new Client();
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async parseAnimeList(url) {
const res = await this.client.get(url);
const doc = new Document(res.body);
const elements = doc.select("main div.grid a");
const list = [];
for (const element of elements) {
const name = element.selectFirst("h3").text;
const imageUrl = element.selectFirst("img").getSrc;
const link = element.getHref;
list.push({ name, imageUrl, link });
}
const hasNextPage = doc.selectFirst("main nav a:last-child").text.trim() == "Next";
return { "list": list, "hasNextPage": hasNextPage };
}
statusFromString(status) {
return {
"en emision": 0, // releasing
"emisión": 0, // releasing
"finalizado": 1, // finished
"proximamente": 4, // unreleased
}[status.toLowerCase()] ?? 5;
}
async getPopular(page) {
return await this.parseAnimeList(`${this.source.baseUrl}/animes?order=visits&page=${page}`);
}
async getLatestUpdates(page) {
return await this.parseAnimeList(`${this.source.baseUrl}/animes?order=updated&page=${page}`);
}
async search(query, page, filters) {
query = query.trim().replaceAll(/\ +/g, "+");
let url = `${this.source.baseUrl}/animes?q=${query}`;
// Search sometimes failed because filters were empty. I experienced this mostly on android...
if (!filters || filters.length == 0) {
return this.parseAnimeList(url + `&page=${page}`);
}
for (const filter of filters[0].state) {
if (filter.state == true)
url += `&type[]=${filter.value}`;
}
url += `&genero[]=${filters[1].values[filters[1].state].value}`;
for (const filter of filters[2].state) {
if (filter.state == true)
url += `&estado[]=${filter.value}`;
}
url += `&order=${filters[3].values[filters[3].state].value}`;
url += `&page=${page}`;
return await this.parseAnimeList(url);
}
async getDetail(url) {
const detail = {};
const res = await this.client.get(url);
const doc = new Document(res.body);
const info = doc.selectFirst('main div.flex');
detail.name = info.selectFirst("h1").text;
detail.status = this.statusFromString(info.selectFirst("a").text.trim());
detail.imageUrl = info.selectFirst("img").getSrc;
detail.description = info.selectFirst("h2 + p").text.trim();
detail.genre = info.select("h2 + div a").map(e => e.text.trim());
detail.episodes = [];
for (const e of doc.select('main + div ul a').reverse()) {
const name = `Episodio ${detail.episodes.length + 1}`;
const url = e.getHref;
detail.episodes.push({ name, url });
}
detail.episodes.reverse();
return detail;
}
// For anime episode video list
async getVideoList(url) {
let res = await this.client.get(url);
let doc = new Document(res.body);
let promises = [];
const videos = [];
// get type
const type = /\blatino\b/i.test(url) ? 'Dub' : 'Sub';
// get names
const hosts = doc.select('main ul a').map(e => e.text);
// get links
const redirects = [];
let script = doc.selectFirst("script:contains(tabsArray)");
for (const e of script.text.matchAll(/tabsArray.*? = "(.*?)"/g)) {
redirects.push(e[1]);
};
// extract remote video links
const renameLUT = { 'amazones': 'amazon', 'burst': 'burstcloud', 'hide': 'vidhide',
'ru': 'okru', 'stream2': 'vidhide', };
for (let i = 0; i < hosts.length; i++) {
const host = hosts[i].trim();
const lhost = host.toLowerCase();
const method = renameLUT[lhost] ?? lhost;
if (method in extractAny.methods) {
promises.push((async (redirect) => {
const res = await this.client.get(redirect);
const doc = new Document(res.body);
const script = doc.selectFirst('script:contains(play)');
let link = script.text.match(/src="(.*?)"/)[1];
if (method == 'amazon')
link = this.source.baseUrl + link.slice(link.search('/'));
return await extractAny(link, method, 'Español', type, host);
})(redirects[i]));
}
}
for (const p of (await Promise.allSettled(promises))) {
if (p.status == 'fulfilled') {
videos.push.apply(videos, p.value);
}
}
return sortVideos(videos);
}
getFilterList() {
return [
{
type_name: "GroupFilter",
name: "Tipo",
state: [
["TV", "tv"],
["Película", "movie"],
["Especial", "special"],
["OVA", "ova"],
["DONGHUA", "donghua"]
].map(x => ({ type_name: 'CheckBox', name: x[0], value: x[1] }))
},
{
type_name: "SelectFilter",
type: "genero",
name: "Género",
state: 0,
values: [
["Todos", ""],
["Acción", "accion"],
["Ángeles", "angeles"],
["Artes Marciales", "artes-marciales"],
["Aventura", "aventura"],
["Ciencia Ficción", "Ciencia Ficción"],
["Comedia", "comedia"],
["Cyberpunk", "cyberpunk"],
["Demonios", "demonios"],
["Deportes", "deportes"],
["Dragones", "dragones"],
["Drama", "drama"],
["Ecchi", "ecchi"],
["Escolares", "escolares"],
["Fantasía", "fantasia"],
["Gore", "gore"],
["Harem", "harem"],
["Histórico", "historico"],
["Horror", "horror"],
["Infantil", "infantil"],
["Isekai", "isekai"],
["Josei", "josei"],
["Juegos", "juegos"],
["Magia", "magia"],
["Mecha", "mecha"],
["Militar", "militar"],
["Misterio", "misterio"],
["Música", "Musica"],
["Ninjas", "ninjas"],
["Parodia", "parodia"],
["Policía", "policia"],
["Psicológico", "psicologico"],
["Recuerdos de la vida", "Recuerdos de la vida"],
["Romance", "romance"],
["Samurai", "samurai"],
["Sci-Fi", "sci-fi"],
["Seinen", "seinen"],
["Shoujo", "shoujo"],
["Shoujo Ai", "shoujo-ai"],
["Shounen", "shounen"],
["Slice of life", "slice-of-life"],
["Sobrenatural", "sobrenatural"],
["Space", "space"],
["Spokon", "spokon"],
["Steampunk", "steampunk"],
["Superpoder", "superpoder"],
["Thriller", "thriller"],
["Vampiro", "vampiro"],
["Yaoi", "yaoi"],
["Yuri", "yuri"],
["Zombies", "zombies"]
].map(x => ({ type_name: 'SelectOption', name: x[0], value: x[1] }))
},
{
type_name: "GroupFilter",
name: "Estado",
state: [
["Emisión", "1"],
["Finalizado", "2"],
["Proximamente", "3"],
["En Cuarentena", "4"]
].map(x => ({ type_name: 'CheckBox', name: x[0], value: x[1] }))
},
{
type_name: "SelectFilter",
type: "sort",
name: "Orden",
state: 0,
values: [
["Por Defecto", "default"],
["Recientemente Actualizados", "updated"],
["Recientemente Agregados", "added"],
["Nombre A-Z", "title"],
["Calificación", "likes"],
["Más Vistos", "visits"]
].map(x => ({ type_name: 'SelectOption', name: x[0], value: x[1] }))
}
];
}
getSourcePreferences() {
const languages = ['Español'];
const types = ['Sub'];
const resolutions = ['1080p', '720p', '480p'];
const hosts = ['Amazon', 'AmazonEs', 'Burst', 'Mp4Upload', 'RU', 'Sendvid', 'STREAM2', 'HIDE', 'YourUpload'];
return [
{
key: 'lang',
listPreference: {
title: 'Preferred Language',
summary: 'If available, this language will be chosen by default. Priority = 0 (lower is better)',
valueIndex: 0,
entries: languages,
entryValues: languages
}
},
{
key: 'type',
listPreference: {
title: 'Preferred Type',
summary: 'If available, this type will be chosen by default. Priority = 1 (lower is better)',
valueIndex: 0,
entries: types,
entryValues: types
}
},
{
key: 'res',
listPreference: {
title: 'Preferred Resolution',
summary: 'If available, this resolution will be chosen by default. Priority = 2 (lower is better)',
valueIndex: 0,
entries: resolutions,
entryValues: resolutions
}
},
{
key: 'host',
listPreference: {
title: 'Preferred Host',
summary: 'If available, this hoster will be chosen by default. Priority = 3 (lower is better)',
valueIndex: 0,
entries: hosts,
entryValues: hosts
}
}
];
}
}
/***************************************************************************************************
*
* mangayomi-js-helpers v1.2
*
* # Video Extractors
* - vidGuardExtractor
* - doodExtractor
* - vidozaExtractor
* - okruExtractor
* - amazonExtractor
* - vidHideExtractor
* - filemoonExtractor
* - mixdropExtractor
* - speedfilesExtractor
* - luluvdoExtractor
* - burstcloudExtractor (not working, see description)
*
* # Video Extractor Wrappers
* - streamWishExtractor
* - voeExtractor
* - mp4UploadExtractor
* - yourUploadExtractor
* - streamTapeExtractor
* - sendVidExtractor
*
* # Video Extractor helpers
* - extractAny
*
* # Playlist Extractors
* - m3u8Extractor
* - jwplayerExtractor
*
* # Extension Helpers
* - sortVideos()
*
* # Uint8Array
* - Uint8Array.fromBase64()
* - Uint8Array.prototype.toBase64()
* - Uint8Array.prototype.decode()
*
* # String
* - String.prototype.encode()
* - String.decode()
* - String.prototype.reverse()
* - String.prototype.swapcase()
* - getRandomString()
*
* # Encode/Decode Functions
* - decodeUTF8
* - encodeUTF8
*
* # Url
* - absUrl()
*
***************************************************************************************************/
//--------------------------------------------------------------------------------------------------
// Video Extractors
//--------------------------------------------------------------------------------------------------
async function vidGuardExtractor(url) {
// get html
const res = await new Client().get(url);
const doc = new Document(res.body);
const script = doc.selectFirst('script:contains(eval)');
// eval code
const code = script.text;
eval?.('var window = {};');
eval?.(code);
const playlistUrl = globalThis.window.svg.stream;
// decode sig
const encoded = playlistUrl.match(/sig=(.*?)&/)[1];
const charCodes = [];
for (let i = 0; i < encoded.length; i += 2) {
charCodes.push(parseInt(encoded.slice(i, i + 2), 16) ^ 2);
}
let decoded = Uint8Array.fromBase64(
String.fromCharCode(...charCodes))
.slice(5, -5)
.reverse();
for (let i = 0; i < decoded.length; i += 2) {
let tmp = decoded[i];
decoded[i] = decoded[i + 1];
decoded[i + 1] = tmp;
}
decoded = decoded.decode();
return await m3u8Extractor(playlistUrl.replace(encoded, decoded), null);
}
async function doodExtractor(url) {
const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false });
let response = await dartClient.get(url);
while ("location" in response.headers) {
response = await dartClient.get(response.headers.location);
}
const newUrl = response.request.url;
const doodhost = newUrl.match(/https:\/\/(.*?)\//, newUrl)[0].slice(8, -1);
const md5 = response.body.match(/'\/pass_md5\/(.*?)',/, newUrl)[0].slice(11, -2);
const token = md5.substring(md5.lastIndexOf("/") + 1);
const expiry = new Date().valueOf();
const randomString = getRandomString(10);
response = await new Client().get(`https://${doodhost}/pass_md5/${md5}`, { "Referer": newUrl });
const videoUrl = `${response.body}${randomString}?token=${token}&expiry=${expiry}`;
const headers = { "User-Agent": "Mangayomi", "Referer": doodhost };
return [{ url: videoUrl, originalUrl: videoUrl, headers: headers, quality: '' }];
}
async function vidozaExtractor(url) {
let response = await new Client({ 'useDartHttpClient': true, "followRedirects": true }).get(url);
const videoUrl = response.body.match(/https:\/\/\S*\.mp4/)[0];
return [{ url: videoUrl, originalUrl: videoUrl, quality: '' }];
}
async function okruExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const tag = doc.selectFirst('div[data-options]');
const playlistUrl = tag.attr('data-options').match(/hlsManifestUrl.*?(h.*?id=\d+)/)[1].replaceAll('\\\\u0026', '&');
return await m3u8Extractor(playlistUrl, null);
}
async function amazonExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const videoUrl = doc.selectFirst('video').getSrc;
return videoUrl ? [{ url: videoUrl, originalUrl: videoUrl, headers: null, quality: '' }] : [];
}
async function vidHideExtractor(url) {
const res = await new Client().get(url);
return await jwplayerExtractor(res.body);
}
async function filemoonExtractor(url, headers) {
headers = headers ?? {};
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
delete headers['user-agent'];
let res = await new Client().get(url, headers);
const src = res.body.match(/iframe src="(.*?)"/)?.[1];
if (src) {
res = await new Client().get(src, {
'Referer': url,
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'User-Agent': headers['User-Agent']
});
}
return await jwplayerExtractor(res.body, headers);
}
async function mixdropExtractor(url) {
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'};
let res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(url, headers);
while ("location" in res.headers) {
res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(res.headers.location, headers);
}
const newUrl = res.request.url;
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(MDCore):contains(eval)').text;
const unpacked = unpackJs(code);
let videoUrl = unpacked.match(/wurl="(.*?)"/)?.[1];
if (!videoUrl) return [];
videoUrl = 'https:' + videoUrl;
headers.referer = newUrl;
return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: headers}];
}
async function speedfilesExtractor(url) {
let res = await new Client().get(url);
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(var)').text;
let b64;
// Get b64
for (const match of code.matchAll(/(?:var|let|const)\s*\w+\s*=\s*["']([^"']+)/g)) {
if (match[1].match(/[g-zG-Z]/)) {
b64 = match[1];
break;
}
}
// decode b64 => b64
const step1 = Uint8Array.fromBase64(b64).reverse().decode().swapcase();
// decode b64 => hex
const step2 = Uint8Array.fromBase64(step1).reverse().decode();
// decode hex => b64
let step3 = [];
for (let i = 0; i < step2.length; i += 2) {
step3.push(parseInt(step2.slice(i, i + 2), 16) - 3);
}
step3 = String.fromCharCode(...step3.reverse()).swapcase();
// decode b64 => url
const videoUrl = Uint8Array.fromBase64(step3).decode();
return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: null}];
}
async function luluvdoExtractor(url) {
const client = new Client();
const match = url.match(/(.*?:\/\/.*?)\/.*\/(.*)/);
const headers = {'user-agent': 'Mangayomi'};
const res = await client.get(`${match[1]}/dl?op=embed&file_code=${match[2]}`, headers);
return await jwplayerExtractor(res.body, headers);
}
/** Does not work: Client always sets 'charset=utf-8' in Content-Type. */
async function burstcloudExtractor(url) {
let client = new Client();
let res = await client.get(url);
const id = res.body.match(/data-file-id="(.*?)"/)[1];
const headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
'Referer': url,
};
const data = {
'fileId': id
};
res = await client.post(`https://www.burstcloud.co/file/play-request/`, headers, data);
const videoUrl = res.body.match(/cdnUrl":"(.*?)"/)[1];
return [{
url: videoUrl,
originalUrl: videoUrl,
headers: { 'Referer': url.match(/.*?:\/\/.*?\//) },
quality: ''
}];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Wrappers
//--------------------------------------------------------------------------------------------------
_streamWishExtractor = streamWishExtractor;
streamWishExtractor = async (url) => {
return (await _streamWishExtractor(url, '')).map(v => {
v.quality = v.quality.slice(3, -1);
return v;
});
}
_voeExtractor = voeExtractor;
voeExtractor = async (url) => {
return (await _voeExtractor(url, '')).map(v => {
v.quality = v.quality.replace(/Voe: (\d+p?)/i, '$1');
return v;
});
}
_mp4UploadExtractor = mp4UploadExtractor;
mp4UploadExtractor = async (url) => {
return (await _mp4UploadExtractor(url)).map(v => {
v.quality = v.quality.match(/\d+p/)?.[0] ?? '';
return v;
});
}
_yourUploadExtractor = yourUploadExtractor;
yourUploadExtractor = async (url) => {
return (await _yourUploadExtractor(url))
.filter(v => !v.url.includes('/novideo'))
.map(v => {
v.quality = '';
return v;
});
}
_streamTapeExtractor = streamTapeExtractor;
streamTapeExtractor = async (url) => {
return await _streamTapeExtractor(url, '');
}
_sendVidExtractor = sendVidExtractor;
sendVidExtractor = async (url) => {
let res = await new Client().get(url);
var videoUrl, quality;
try {
videoUrl = res.body.match(/og:video" content="(.*?\.mp4.*?)"/)[1];
quality = res.body.match(/og:video:height" content="(.*?)"/)?.[1];
quality = quality ? quality + 'p' : '';
} catch (error) {
}
if (!videoUrl) {
return _sendVidExtractor(url, null, '');
}
return [{url: videoUrl, originalUrl: videoUrl, quality: quality, headers: null}];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Helpers
//--------------------------------------------------------------------------------------------------
async function extractAny(url, method, lang, type, host, headers = null) {
const m = extractAny.methods[method];
return (!m) ? [] : (await m(url, headers)).map(v => {
v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`;
return v;
});
};
extractAny.methods = {
'amazon': amazonExtractor,
'burstcloud': burstcloudExtractor,
'doodstream': doodExtractor,
'filemoon': filemoonExtractor,
'luluvdo': luluvdoExtractor,
'mixdrop': mixdropExtractor,
'mp4upload': mp4UploadExtractor,
'okru': okruExtractor,
'sendvid': sendVidExtractor,
'speedfiles': speedfilesExtractor,
'streamtape': streamTapeExtractor,
'streamwish': vidHideExtractor,
'vidguard': vidGuardExtractor,
'vidhide': vidHideExtractor,
'vidoza': vidozaExtractor,
'voe': voeExtractor,
'yourupload': yourUploadExtractor
};
//--------------------------------------------------------------------------------------------------
// Playlist Extractors
//--------------------------------------------------------------------------------------------------
async function m3u8Extractor(url, headers = null) {
// https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist
// https://developer.apple.com/documentation/http-live-streaming/adding-alternate-media-to-a-playlist
// define attribute lists
const streamAttributes = [
['avg_bandwidth', /AVERAGE-BANDWIDTH=(\d+)/],
['bandwidth', /\bBANDWIDTH=(\d+)/],
['resolution', /\bRESOLUTION=([\dx]+)/],
['framerate', /\bFRAME-RATE=([\d\.]+)/],
['codecs', /\bCODECS="(.*?)"/],
['video', /\bVIDEO="(.*?)"/],
['audio', /\bAUDIO="(.*?)"/],
['subtitles', /\bSUBTITLES="(.*?)"/],
['captions', /\bCLOSED-CAPTIONS="(.*?)"/]
];
const mediaAttributes = [
['type', /\bTYPE=([\w-]*)/],
['group', /\bGROUP-ID="(.*?)"/],
['lang', /\bLANGUAGE="(.*?)"/],
['name', /\bNAME="(.*?)"/],
['autoselect', /\bAUTOSELECT=(\w*)/],
['default', /\bDEFAULT=(\w*)/],
['instream-id', /\bINSTREAM-ID="(.*?)"/],
['assoc-lang', /\bASSOC-LANGUAGE="(.*?)"/],
['channels', /\bCHANNELS="(.*?)"/],
['uri', /\bURI="(.*?)"/]
];
const streams = [], videos = {}, audios = {}, subtitles = {}, captions = {};
const dict = { 'VIDEO': videos, 'AUDIO': audios, 'SUBTITLES': subtitles, 'CLOSED-CAPTIONS': captions };
const res = await new Client().get(url, headers);
const text = res.body;
if (res.statusCode != 200) {
return [];
}
// collect media
for (const match of text.matchAll(/#EXT-X-MEDIA:(.*)/g)) {
const info = match[1], medium = {};
for (const attr of mediaAttributes) {
const m = info.match(attr[1]);
medium[attr[0]] = m ? m[1] : null;
}
const type = medium.type;
delete medium.type;
const group = medium.group;
delete medium.group;
const typedict = dict[type];
if (typedict[group] == undefined)
typedict[group] = [];
typedict[group].push(medium);
}
// collect streams
for (const match of text.matchAll(/#EXT-X-STREAM-INF:(.*)\s*(.*)/g)) {
const info = match[1], stream = { 'url': absUrl(match[2], url) };
for (const attr of streamAttributes) {
const m = info.match(attr[1]);
stream[attr[0]] = m ? m[1] : null;
}
stream['video'] = videos[stream.video] ?? null;
stream['audio'] = audios[stream.audio] ?? null;
stream['subtitles'] = subtitles[stream.subtitles] ?? null;
stream['captions'] = captions[stream.captions] ?? null;
// format resolution or bandwidth
let quality;
if (stream.resolution) {
quality = stream.resolution.match(/x(\d+)/)[1] + 'p';
} else {
quality = (parseInt(stream.avg_bandwidth ?? stream.bandwidth) / 1000000) + 'Mb/s'
}
// add stream to list
const subs = stream.subtitles?.map((s) => {
return { file: s.uri, label: s.name };
});
const auds = stream.audio?.map((a) => {
return { file: a.uri, label: a.name };
});
streams.push({
url: stream.url,
quality: quality,
originalUrl: stream.url,
headers: headers,
subtitles: subs ?? null,
audios: auds ?? null
});
}
return streams.length ? streams : [{
url: url,
quality: '',
originalUrl: url,
headers: headers,
subtitles: null,
audios: null
}];
}
async function jwplayerExtractor(text, headers) {
// https://docs.jwplayer.com/players/reference/playlists
const getsetup = /setup\(({[\s\S]*?})\)/;
const getsources = /sources:\s*(\[[\s\S]*?\])/;
const gettracks = /tracks:\s*(\[[\s\S]*?\])/;
const unpacked = unpackJs(text);
const videos = [], subtitles = [];
const data = eval('(' + (getsetup.exec(text) || getsetup.exec(unpacked))?.[1] + ')');
if (data){
var sources = data.sources;
var tracks = data.tracks;
} else {
var sources = eval('(' + (getsources.exec(text) || getsources.exec(unpacked))?.[1] + ')');
var tracks = eval('(' + (gettracks.exec(text) || gettracks.exec(unpacked))?.[1] + ')');
}
for (t of tracks) {
if (t.type == "captions") {
subtitles.push({file: t.file, label: t.label});
}
}
for (s of sources) {
if (s.file.includes('master.m3u8')) {
videos.push(...(await m3u8Extractor(s.file, headers)));
} else if (s.file.includes('.mpd')) {
} else {
videos.push({url: s.file, originalUrl: s.file, quality: '', headers: headers});
}
}
return videos.map(v => {
v.subtitles = subtitles;
return v;
});
}
//--------------------------------------------------------------------------------------------------
// Extension Helpers
//--------------------------------------------------------------------------------------------------
function sortVideos(videos) {
const pref = new SharedPreferences();
const getres = RegExp('(\\d+)p?', 'i');
const lang = RegExp(pref.get('lang'), 'i');
const type = RegExp(pref.get('type'), 'i');
const res = RegExp(getres.exec(pref.get('res'))[1], 'i');
const host = RegExp(pref.get('host'), 'i');
let getScore = (q, hasRes) => {
const bLang = lang.test(q), bType = type.test(q), bRes = res.test(q), bHost = host.test(q);
if (hasRes) {
return bLang * (8 + bType * (4 + bRes * (2 + bHost * 1)));
} else {
return bLang * (8 + bType * (4 + (bHost * 3)));
}
}
return videos.sort((a, b) => {
const resA = getres.exec(a.quality)?.[1];
const resB = getres.exec(b.quality)?.[1];
const score = getScore(b.quality, resB) - getScore(a.quality, resA);
if (score) return score;
const qA = resA ? a.quality.replace(resA, (9999 - parseInt(resA)).toString()) : a.quality;
const qB = resA ? b.quality.replace(resB, (9999 - parseInt(resB)).toString()) : b.quality;
return qA.localeCompare(qB);
});
}
//--------------------------------------------------------------------------------------------------
// Uint8Array
//--------------------------------------------------------------------------------------------------
Uint8Array.fromBase64 = function (b64) {
// [00,01,02,03,04,05,06,07,08,\t,\n,0b,0c,\r,0e,0f,10,11,12,13,14,15,16,17,18,19,1a,1b,1c,1d,1e,1f,' ', !, ", #, $, %, &, ', (, ), *, +,',', -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <,'=', >, ?, @,A,B,C,D,E,F,G,H,I,J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~,7f]
const m = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1]
let data = [], val = 0, bits = -8
for (const c of b64) {
let n = m[c.charCodeAt(0)];
if (n == -1) break;
val = (val << 6) + n;
bits += 6;
for (; bits >= 0; bits -= 8)
data.push((val >> bits) & 0xFF);
}
return new Uint8Array(data);
}
Uint8Array.prototype.toBase64 = function () {
const m = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
let b64 = '', val = 0, bits = -6;
for (const b of this) {
val = (val << 8) + b;
bits += 8;
while (bits >= 0) {
b64 += m[(val >> bits) & 0x3F];
bits -= 6;
}
}
if (bits > -6)
b64 += m[(val << -bits) & 0x3F];
return b64 + ['', '', '==', '='][b64.length % 4];
}
Uint8Array.prototype.decode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(this);
}
return null;
}
//--------------------------------------------------------------------------------------------------
// String
//--------------------------------------------------------------------------------------------------
String.prototype.encode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return encodeUTF8(this);
}
return null;
}
String.decode = function (data, encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(data);
}
return null;
}
String.prototype.reverse = function () {
return this.split('').reverse().join('');
}
String.prototype.swapcase = function () {
const isAsciiLetter = /[A-z]/;
const result = [];
for (const l of this)
result.push(isAsciiLetter.test(l) ? String.fromCharCode(l.charCodeAt() ^ 32) : l);
return result.join('');
}
function getRandomString(length) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
let result = "";
for (let i = 0; i < length; i++) {
const random = Math.floor(Math.random() * 61);
result += chars[random];
}
return result;
}
//--------------------------------------------------------------------------------------------------
// Encode/Decode Functions
//--------------------------------------------------------------------------------------------------
function decodeUTF8(data) {
const codes = [];
for (let i = 0; i < data.length;) {
const c = data[i++];
const len = (c > 0xBF) + (c > 0xDF) + (c > 0xEF);
let val = c & (0xFF >> (len + 1));
for (const end = i + len; i < end; i++) {
val = (val << 6) + (data[i] & 0x3F);
}
codes.push(val);
}
return String.fromCharCode(...codes);
}
function encodeUTF8(string) {
const data = [];
for (const c of string) {
const code = c.charCodeAt(0);
const len = (code > 0x7F) + (code > 0x7FF) + (code > 0xFFFF);
let bits = len * 6;
data.push((len ? ~(0xFF >> len + 1) : (0)) + (code >> bits));
while (bits > 0) {
data.push(0x80 + ((code >> (bits -= 6)) & 0x3F))
}
}
return new Uint8Array(data);
}
//--------------------------------------------------------------------------------------------------
// Url
//--------------------------------------------------------------------------------------------------
function absUrl(url, base) {
if (url.search(/^\w+:\/\//) == 0) {
return url;
} else if (url.startsWith('/')) {
return base.slice(0, base.lastIndexOf('/')) + url;
} else {
return base.slice(0, base.lastIndexOf('/') + 1) + url;
}
}

View File

@@ -1,183 +0,0 @@
const mangayomiSources = [{
"name": "AnimeFLV",
"lang": "es",
"baseUrl": "https://www3.animeflv.net",
"apiUrl": "",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/es.animeflv.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.1",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/es/animeflv.js"
}];
class DefaultExtension extends MProvider {
async getPopular(page) {
const baseUrl = this.source.baseUrl;
const res = await new Client().get(`${baseUrl}/browse?order=rating&page=${page}`);
return this.animeFromElement(res.body);
}
async getLatestUpdates(page) {
const baseUrl = this.source.baseUrl;
const res = await new Client().get(`${baseUrl}/browse?order=added&page=${page}`);
return this.animeFromElement(res.body);
}
async search(query, page, filters) {
const baseUrl = this.source.baseUrl;
const res = await new Client().get(`${baseUrl}/browse?&q=${query}&page=${page}`);
return this.animeFromElement(res.body);
}
async getDetail(url) {
const baseUrl = this.source.baseUrl;
const res = await new Client().get(baseUrl + url);
const document = new Document(res.body);
const genre = document.select("nav.Nvgnrs a").map(e => e.text);
const description = document.selectFirst("div.Description").text.trim();
const status = this.parseStatus(document.selectFirst("span.fa-tv").text);
const episodeList = [];
for (const script of document.select("script")) {
if (script.text.includes("var anime_info =")) {
const animeInfo = script.text.substringAfter("var anime_info = [").substringBefore("];");
const arrInfo = JSON.parse(`[${animeInfo}]`);
const animeUri = arrInfo[2].replace(/"/g, "");
const episodes = script.text.substringAfter("var episodes = [").substringBefore("];").trim();
const arrEpisodes = episodes.split("],[");
for (const arrEp of arrEpisodes) {
const noEpisode = arrEp.replace("[", "").replace("]", "").split(",")[0];
const url = `${baseUrl}/ver/${animeUri}-${noEpisode}`;
const name = `Episodio ${noEpisode}`;
episodeList.push({ name, url });
}
}
}
return {
description, status: status, genre, episodes: episodeList
};
}
async getVideoList(url) {
const res = await new Client().get(url);
const document = new Document(res.body);
const script = document.selectFirst("script:contains('var videos = {')");
if (!script) return [];
const jsonString = script.text;
const responseString = jsonString.substringAfter("var videos =").substringBefore(";").trim();
const serverModel = JSON.parse(responseString);
const videos = [];
for (const item of serverModel.SUB) {
let videoList = [];
switch (item.title) {
case "Stape":
videoList = await streamTapeExtractor(item.url || item.code);
break;
case "Okru":
videoList = await okruExtractor(item.url || item.code);
break;
case "YourUpload":
videoList = await yourUploadExtractor(item.url || item.code);
break;
case "SW":
videoList = await streamWishExtractor(item.url || item.code, "StreamWish:");
break;
default:
videoList = [];
}
videos.push(...videoList);
}
return this.sortVideos(videos);
}
sortVideos(videos) {
const preferences = new SharedPreferences();
const server = preferences.get("preferred_server");
const quality = preferences.get("preferred_quality");
videos.sort((a, b) => {
let qualityMatchA = 0;
if (a.quality.includes(server) &&
a.quality.includes(quality)) {
qualityMatchA = 1;
}
let qualityMatchB = 0;
if (b.quality.includes(server) &&
b.quality.includes(quality)) {
qualityMatchB = 1;
}
return qualityMatchB - qualityMatchA;
});
return videos;
}
animeFromElement(body) {
const elements = new Document(body).select("div.Container ul.ListAnimes li article");
const list = [];
for (const element of elements) {
const name = element.selectFirst("a h3").text;
const thumbnailUrl = element.selectFirst("a div.Image figure img").attr("src");
const imageUrl = thumbnailUrl ? thumbnailUrl : element.selectFirst("a div.Image figure img").attr("data-cfsrc");
const link = element.selectFirst("div.Description a.Button").attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: new Document(body).select("div.Container ul.ListAnimes li article").length > 0
}
}
parseStatus(statusString) {
if (statusString.includes("En emision")) {
return 0;
} else if (statusString.includes("Finalizado")) {
return 1;
} else {
return 5;
}
}
getSourcePreferences() {
return [
{
"key": "preferred_quality",
"listPreference": {
"title": "Preferred quality",
"summary": "",
"valueIndex": 0,
"entries": [
"1080p",
"720p",
"480p",
"360p",
],
"entryValues": [
"1080",
"720",
"480",
"360",
]
}
},
{
"key": "preferred_server",
"listPreference": {
"title": "Preferred server",
"summary": "",
"valueIndex": 0,
"entries": ["StreamWish", "YourUpload", "Okru", "Streamtape"],
"entryValues": ["StreamWish", "YourUpload", "Okru", "Streamtape"]
}
}
];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,802 +0,0 @@
const mangayomiSources = [{
"name": "TioAnime",
"lang": "es",
"baseUrl": "https://tioanime.com",
"apiUrl": "",
"iconUrl": "https://tioanime.com/assets/img/tio_fb.jpg",
"typeSource": "single",
"itemType": 1,
"version": "0.1.12",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/es/tioanime.js"
}];
class DefaultExtension extends MProvider {
constructor () {
super();
this.client = new Client();
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async parseAnimeList(url) {
const res = await this.client.get(url);
const doc = new Document(res.body);
const elements = doc.select("ul.animes > li");
const list = [];
for (const element of elements) {
const name = element.selectFirst(".title").text;
const imageUrl = this.source.baseUrl + element.selectFirst("img").getSrc;
const link = element.selectFirst("a").getHref;
list.push({ name, imageUrl, link });
}
const isLastPage = doc.selectFirst("li.page-item.active + li").text == "»";
return { "list": list, "hasNextPage": !isLastPage };
}
statusFromString(status) {
return {
"En emision": 0,
"Finalizado": 1
}[status] ?? 5;
}
async getPopular(page) {
return await this.parseAnimeList(`${this.source.baseUrl}/directorio?p=${page}`);
}
async getLatestUpdates(page) {
return await this.parseAnimeList(`${this.source.baseUrl}/directorio?p=${page}`);
}
async search(query, page, filters) {
query = query.trim().replaceAll(/\ +/g, "+");
let url = `${this.source.baseUrl}/directorio?q=${query}&p=${page}`;
return await this.parseAnimeList(url);
}
async getDetail(url) {
const res = await this.client.get(this.source.baseUrl + url);
const doc = new Document(res.body);
const detail = {};
const info = doc.selectFirst("article.anime-single");
const episodeCount = parseInt(/episodes = \[(\d+)/.exec(doc.select("script").pop().innerHtml)[1]);
const episodeUrl = url.replace("/anime", "/ver") + "-";
detail.name = info.selectFirst("h1").text;
detail.status = this.statusFromString(info.selectFirst("a").text);
detail.imageUrl = this.source.baseUrl + info.selectFirst("img").getSrc;
detail.description = info.selectFirst("p.sinopsis").text.trim();
detail.genre = info.select("p.genres a").map(e => e.text.trim());
detail.episodes = [];
for (let i = 0; i < episodeCount; i++) {
const name = `Episodio ${i + 1}`;
const url = episodeUrl + (i + 1);
detail.episodes.push({ name, url });
}
detail.episodes.reverse();
return detail;
}
// For anime episode video list
async getVideoList(url) {
const res = await this.client.get(this.source.baseUrl + url);
const doc = new Document(res.body);
let promises = [];
const videos = [];
// get type
const type = /\blatino\b/i.test(url) ? 'Dub' : 'Sub';
// get links
const raws = [...doc.select("script").pop().innerHtml.matchAll(/\[".*?\]/g)];
// extract videos
for (const raw of raws) {
const data = JSON.parse(raw[0]);
const host = data[0];
const link = data[1];
promises.push(extractAny(link, host.toLowerCase(), 'Español', type, host));
}
for (const p of (await Promise.allSettled(promises))) {
if (p.status == 'fulfilled') {
videos.push.apply(videos, p.value);
}
}
return sortVideos(videos);
}
getFilterList() {
throw new Error("getFilterList not implemented");
}
getSourcePreferences() {
const languages = ['Español'];
const types = ['Sub'];
const resolutions = ['1080p', '720p', '480p'];
const hosts = ['Okru', 'VidGuard', 'Voe', 'YourUpload'];
return [
{
key: 'lang',
listPreference: {
title: 'Preferred Language',
summary: 'If available, this language will be chosen by default. Priority = 0 (lower is better)',
valueIndex: 0,
entries: languages,
entryValues: languages
}
},
{
key: 'type',
listPreference: {
title: 'Preferred Type',
summary: 'If available, this type will be chosen by default. Priority = 1 (lower is better)',
valueIndex: 0,
entries: types,
entryValues: types
}
},
{
key: 'res',
listPreference: {
title: 'Preferred Resolution',
summary: 'If available, this resolution will be chosen by default. Priority = 2 (lower is better)',
valueIndex: 0,
entries: resolutions,
entryValues: resolutions
}
},
{
key: 'host',
listPreference: {
title: 'Preferred Host',
summary: 'If available, this hoster will be chosen by default. Priority = 3 (lower is better)',
valueIndex: 0,
entries: hosts,
entryValues: hosts
}
}
];
}
}
/***************************************************************************************************
*
* mangayomi-js-helpers v1.2
*
* # Video Extractors
* - vidGuardExtractor
* - doodExtractor
* - vidozaExtractor
* - okruExtractor
* - amazonExtractor
* - vidHideExtractor
* - filemoonExtractor
* - mixdropExtractor
* - speedfilesExtractor
* - luluvdoExtractor
* - burstcloudExtractor (not working, see description)
*
* # Video Extractor Wrappers
* - streamWishExtractor
* - voeExtractor
* - mp4UploadExtractor
* - yourUploadExtractor
* - streamTapeExtractor
* - sendVidExtractor
*
* # Video Extractor helpers
* - extractAny
*
* # Playlist Extractors
* - m3u8Extractor
* - jwplayerExtractor
*
* # Extension Helpers
* - sortVideos()
*
* # Uint8Array
* - Uint8Array.fromBase64()
* - Uint8Array.prototype.toBase64()
* - Uint8Array.prototype.decode()
*
* # String
* - String.prototype.encode()
* - String.decode()
* - String.prototype.reverse()
* - String.prototype.swapcase()
* - getRandomString()
*
* # Encode/Decode Functions
* - decodeUTF8
* - encodeUTF8
*
* # Url
* - absUrl()
*
***************************************************************************************************/
//--------------------------------------------------------------------------------------------------
// Video Extractors
//--------------------------------------------------------------------------------------------------
async function vidGuardExtractor(url) {
// get html
const res = await new Client().get(url);
const doc = new Document(res.body);
const script = doc.selectFirst('script:contains(eval)');
// eval code
const code = script.text;
eval?.('var window = {};');
eval?.(code);
const playlistUrl = globalThis.window.svg.stream;
// decode sig
const encoded = playlistUrl.match(/sig=(.*?)&/)[1];
const charCodes = [];
for (let i = 0; i < encoded.length; i += 2) {
charCodes.push(parseInt(encoded.slice(i, i + 2), 16) ^ 2);
}
let decoded = Uint8Array.fromBase64(
String.fromCharCode(...charCodes))
.slice(5, -5)
.reverse();
for (let i = 0; i < decoded.length; i += 2) {
let tmp = decoded[i];
decoded[i] = decoded[i + 1];
decoded[i + 1] = tmp;
}
decoded = decoded.decode();
return await m3u8Extractor(playlistUrl.replace(encoded, decoded), null);
}
async function doodExtractor(url) {
const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false });
let response = await dartClient.get(url);
while ("location" in response.headers) {
response = await dartClient.get(response.headers.location);
}
const newUrl = response.request.url;
const doodhost = newUrl.match(/https:\/\/(.*?)\//, newUrl)[0].slice(8, -1);
const md5 = response.body.match(/'\/pass_md5\/(.*?)',/, newUrl)[0].slice(11, -2);
const token = md5.substring(md5.lastIndexOf("/") + 1);
const expiry = new Date().valueOf();
const randomString = getRandomString(10);
response = await new Client().get(`https://${doodhost}/pass_md5/${md5}`, { "Referer": newUrl });
const videoUrl = `${response.body}${randomString}?token=${token}&expiry=${expiry}`;
const headers = { "User-Agent": "Mangayomi", "Referer": doodhost };
return [{ url: videoUrl, originalUrl: videoUrl, headers: headers, quality: '' }];
}
async function vidozaExtractor(url) {
let response = await new Client({ 'useDartHttpClient': true, "followRedirects": true }).get(url);
const videoUrl = response.body.match(/https:\/\/\S*\.mp4/)[0];
return [{ url: videoUrl, originalUrl: videoUrl, quality: '' }];
}
async function okruExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const tag = doc.selectFirst('div[data-options]');
const playlistUrl = tag.attr('data-options').match(/hlsManifestUrl.*?(h.*?id=\d+)/)[1].replaceAll('\\\\u0026', '&');
return await m3u8Extractor(playlistUrl, null);
}
async function amazonExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const videoUrl = doc.selectFirst('video').getSrc;
return videoUrl ? [{ url: videoUrl, originalUrl: videoUrl, headers: null, quality: '' }] : [];
}
async function vidHideExtractor(url) {
const res = await new Client().get(url);
return await jwplayerExtractor(res.body);
}
async function filemoonExtractor(url, headers) {
headers = headers ?? {};
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
delete headers['user-agent'];
let res = await new Client().get(url, headers);
const src = res.body.match(/iframe src="(.*?)"/)?.[1];
if (src) {
res = await new Client().get(src, {
'Referer': url,
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'User-Agent': headers['User-Agent']
});
}
return await jwplayerExtractor(res.body, headers);
}
async function mixdropExtractor(url) {
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'};
let res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(url, headers);
while ("location" in res.headers) {
res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(res.headers.location, headers);
}
const newUrl = res.request.url;
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(MDCore):contains(eval)').text;
const unpacked = unpackJs(code);
let videoUrl = unpacked.match(/wurl="(.*?)"/)?.[1];
if (!videoUrl) return [];
videoUrl = 'https:' + videoUrl;
headers.referer = newUrl;
return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: headers}];
}
async function speedfilesExtractor(url) {
let res = await new Client().get(url);
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(var)').text;
let b64;
// Get b64
for (const match of code.matchAll(/(?:var|let|const)\s*\w+\s*=\s*["']([^"']+)/g)) {
if (match[1].match(/[g-zG-Z]/)) {
b64 = match[1];
break;
}
}
// decode b64 => b64
const step1 = Uint8Array.fromBase64(b64).reverse().decode().swapcase();
// decode b64 => hex
const step2 = Uint8Array.fromBase64(step1).reverse().decode();
// decode hex => b64
let step3 = [];
for (let i = 0; i < step2.length; i += 2) {
step3.push(parseInt(step2.slice(i, i + 2), 16) - 3);
}
step3 = String.fromCharCode(...step3.reverse()).swapcase();
// decode b64 => url
const videoUrl = Uint8Array.fromBase64(step3).decode();
return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: null}];
}
async function luluvdoExtractor(url) {
const client = new Client();
const match = url.match(/(.*?:\/\/.*?)\/.*\/(.*)/);
const headers = {'user-agent': 'Mangayomi'};
const res = await client.get(`${match[1]}/dl?op=embed&file_code=${match[2]}`, headers);
return await jwplayerExtractor(res.body, headers);
}
/** Does not work: Client always sets 'charset=utf-8' in Content-Type. */
async function burstcloudExtractor(url) {
let client = new Client();
let res = await client.get(url);
const id = res.body.match(/data-file-id="(.*?)"/)[1];
const headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
'Referer': url,
};
const data = {
'fileId': id
};
res = await client.post(`https://www.burstcloud.co/file/play-request/`, headers, data);
const videoUrl = res.body.match(/cdnUrl":"(.*?)"/)[1];
return [{
url: videoUrl,
originalUrl: videoUrl,
headers: { 'Referer': url.match(/.*?:\/\/.*?\//) },
quality: ''
}];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Wrappers
//--------------------------------------------------------------------------------------------------
_streamWishExtractor = streamWishExtractor;
streamWishExtractor = async (url) => {
return (await _streamWishExtractor(url, '')).map(v => {
v.quality = v.quality.slice(3, -1);
return v;
});
}
_voeExtractor = voeExtractor;
voeExtractor = async (url) => {
return (await _voeExtractor(url, '')).map(v => {
v.quality = v.quality.replace(/Voe: (\d+p?)/i, '$1');
return v;
});
}
_mp4UploadExtractor = mp4UploadExtractor;
mp4UploadExtractor = async (url) => {
return (await _mp4UploadExtractor(url)).map(v => {
v.quality = v.quality.match(/\d+p/)?.[0] ?? '';
return v;
});
}
_yourUploadExtractor = yourUploadExtractor;
yourUploadExtractor = async (url) => {
return (await _yourUploadExtractor(url))
.filter(v => !v.url.includes('/novideo'))
.map(v => {
v.quality = '';
return v;
});
}
_streamTapeExtractor = streamTapeExtractor;
streamTapeExtractor = async (url) => {
return await _streamTapeExtractor(url, '');
}
_sendVidExtractor = sendVidExtractor;
sendVidExtractor = async (url) => {
let res = await new Client().get(url);
var videoUrl, quality;
try {
videoUrl = res.body.match(/og:video" content="(.*?\.mp4.*?)"/)[1];
quality = res.body.match(/og:video:height" content="(.*?)"/)?.[1];
quality = quality ? quality + 'p' : '';
} catch (error) {
}
if (!videoUrl) {
return _sendVidExtractor(url, null, '');
}
return [{url: videoUrl, originalUrl: videoUrl, quality: quality, headers: null}];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Helpers
//--------------------------------------------------------------------------------------------------
async function extractAny(url, method, lang, type, host, headers = null) {
const m = extractAny.methods[method];
return (!m) ? [] : (await m(url, headers)).map(v => {
v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`;
return v;
});
};
extractAny.methods = {
'amazon': amazonExtractor,
'burstcloud': burstcloudExtractor,
'doodstream': doodExtractor,
'filemoon': filemoonExtractor,
'luluvdo': luluvdoExtractor,
'mixdrop': mixdropExtractor,
'mp4upload': mp4UploadExtractor,
'okru': okruExtractor,
'sendvid': sendVidExtractor,
'speedfiles': speedfilesExtractor,
'streamtape': streamTapeExtractor,
'streamwish': vidHideExtractor,
'vidguard': vidGuardExtractor,
'vidhide': vidHideExtractor,
'vidoza': vidozaExtractor,
'voe': voeExtractor,
'yourupload': yourUploadExtractor
};
//--------------------------------------------------------------------------------------------------
// Playlist Extractors
//--------------------------------------------------------------------------------------------------
async function m3u8Extractor(url, headers = null) {
// https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist
// https://developer.apple.com/documentation/http-live-streaming/adding-alternate-media-to-a-playlist
// define attribute lists
const streamAttributes = [
['avg_bandwidth', /AVERAGE-BANDWIDTH=(\d+)/],
['bandwidth', /\bBANDWIDTH=(\d+)/],
['resolution', /\bRESOLUTION=([\dx]+)/],
['framerate', /\bFRAME-RATE=([\d\.]+)/],
['codecs', /\bCODECS="(.*?)"/],
['video', /\bVIDEO="(.*?)"/],
['audio', /\bAUDIO="(.*?)"/],
['subtitles', /\bSUBTITLES="(.*?)"/],
['captions', /\bCLOSED-CAPTIONS="(.*?)"/]
];
const mediaAttributes = [
['type', /\bTYPE=([\w-]*)/],
['group', /\bGROUP-ID="(.*?)"/],
['lang', /\bLANGUAGE="(.*?)"/],
['name', /\bNAME="(.*?)"/],
['autoselect', /\bAUTOSELECT=(\w*)/],
['default', /\bDEFAULT=(\w*)/],
['instream-id', /\bINSTREAM-ID="(.*?)"/],
['assoc-lang', /\bASSOC-LANGUAGE="(.*?)"/],
['channels', /\bCHANNELS="(.*?)"/],
['uri', /\bURI="(.*?)"/]
];
const streams = [], videos = {}, audios = {}, subtitles = {}, captions = {};
const dict = { 'VIDEO': videos, 'AUDIO': audios, 'SUBTITLES': subtitles, 'CLOSED-CAPTIONS': captions };
const res = await new Client().get(url, headers);
const text = res.body;
if (res.statusCode != 200) {
return [];
}
// collect media
for (const match of text.matchAll(/#EXT-X-MEDIA:(.*)/g)) {
const info = match[1], medium = {};
for (const attr of mediaAttributes) {
const m = info.match(attr[1]);
medium[attr[0]] = m ? m[1] : null;
}
const type = medium.type;
delete medium.type;
const group = medium.group;
delete medium.group;
const typedict = dict[type];
if (typedict[group] == undefined)
typedict[group] = [];
typedict[group].push(medium);
}
// collect streams
for (const match of text.matchAll(/#EXT-X-STREAM-INF:(.*)\s*(.*)/g)) {
const info = match[1], stream = { 'url': absUrl(match[2], url) };
for (const attr of streamAttributes) {
const m = info.match(attr[1]);
stream[attr[0]] = m ? m[1] : null;
}
stream['video'] = videos[stream.video] ?? null;
stream['audio'] = audios[stream.audio] ?? null;
stream['subtitles'] = subtitles[stream.subtitles] ?? null;
stream['captions'] = captions[stream.captions] ?? null;
// format resolution or bandwidth
let quality;
if (stream.resolution) {
quality = stream.resolution.match(/x(\d+)/)[1] + 'p';
} else {
quality = (parseInt(stream.avg_bandwidth ?? stream.bandwidth) / 1000000) + 'Mb/s'
}
// add stream to list
const subs = stream.subtitles?.map((s) => {
return { file: s.uri, label: s.name };
});
const auds = stream.audio?.map((a) => {
return { file: a.uri, label: a.name };
});
streams.push({
url: stream.url,
quality: quality,
originalUrl: stream.url,
headers: headers,
subtitles: subs ?? null,
audios: auds ?? null
});
}
return streams.length ? streams : [{
url: url,
quality: '',
originalUrl: url,
headers: headers,
subtitles: null,
audios: null
}];
}
async function jwplayerExtractor(text, headers) {
// https://docs.jwplayer.com/players/reference/playlists
const getsetup = /setup\(({[\s\S]*?})\)/;
const getsources = /sources:\s*(\[[\s\S]*?\])/;
const gettracks = /tracks:\s*(\[[\s\S]*?\])/;
const unpacked = unpackJs(text);
const videos = [], subtitles = [];
const data = eval('(' + (getsetup.exec(text) || getsetup.exec(unpacked))?.[1] + ')');
if (data){
var sources = data.sources;
var tracks = data.tracks;
} else {
var sources = eval('(' + (getsources.exec(text) || getsources.exec(unpacked))?.[1] + ')');
var tracks = eval('(' + (gettracks.exec(text) || gettracks.exec(unpacked))?.[1] + ')');
}
for (t of tracks) {
if (t.type == "captions") {
subtitles.push({file: t.file, label: t.label});
}
}
for (s of sources) {
if (s.file.includes('master.m3u8')) {
videos.push(...(await m3u8Extractor(s.file, headers)));
} else if (s.file.includes('.mpd')) {
} else {
videos.push({url: s.file, originalUrl: s.file, quality: '', headers: headers});
}
}
return videos.map(v => {
v.subtitles = subtitles;
return v;
});
}
//--------------------------------------------------------------------------------------------------
// Extension Helpers
//--------------------------------------------------------------------------------------------------
function sortVideos(videos) {
const pref = new SharedPreferences();
const getres = RegExp('(\\d+)p?', 'i');
const lang = RegExp(pref.get('lang'), 'i');
const type = RegExp(pref.get('type'), 'i');
const res = RegExp(getres.exec(pref.get('res'))[1], 'i');
const host = RegExp(pref.get('host'), 'i');
let getScore = (q, hasRes) => {
const bLang = lang.test(q), bType = type.test(q), bRes = res.test(q), bHost = host.test(q);
if (hasRes) {
return bLang * (8 + bType * (4 + bRes * (2 + bHost * 1)));
} else {
return bLang * (8 + bType * (4 + (bHost * 3)));
}
}
return videos.sort((a, b) => {
const resA = getres.exec(a.quality)?.[1];
const resB = getres.exec(b.quality)?.[1];
const score = getScore(b.quality, resB) - getScore(a.quality, resA);
if (score) return score;
const qA = resA ? a.quality.replace(resA, (9999 - parseInt(resA)).toString()) : a.quality;
const qB = resA ? b.quality.replace(resB, (9999 - parseInt(resB)).toString()) : b.quality;
return qA.localeCompare(qB);
});
}
//--------------------------------------------------------------------------------------------------
// Uint8Array
//--------------------------------------------------------------------------------------------------
Uint8Array.fromBase64 = function (b64) {
// [00,01,02,03,04,05,06,07,08,\t,\n,0b,0c,\r,0e,0f,10,11,12,13,14,15,16,17,18,19,1a,1b,1c,1d,1e,1f,' ', !, ", #, $, %, &, ', (, ), *, +,',', -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <,'=', >, ?, @,A,B,C,D,E,F,G,H,I,J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~,7f]
const m = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1]
let data = [], val = 0, bits = -8
for (const c of b64) {
let n = m[c.charCodeAt(0)];
if (n == -1) break;
val = (val << 6) + n;
bits += 6;
for (; bits >= 0; bits -= 8)
data.push((val >> bits) & 0xFF);
}
return new Uint8Array(data);
}
Uint8Array.prototype.toBase64 = function () {
const m = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
let b64 = '', val = 0, bits = -6;
for (const b of this) {
val = (val << 8) + b;
bits += 8;
while (bits >= 0) {
b64 += m[(val >> bits) & 0x3F];
bits -= 6;
}
}
if (bits > -6)
b64 += m[(val << -bits) & 0x3F];
return b64 + ['', '', '==', '='][b64.length % 4];
}
Uint8Array.prototype.decode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(this);
}
return null;
}
//--------------------------------------------------------------------------------------------------
// String
//--------------------------------------------------------------------------------------------------
String.prototype.encode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return encodeUTF8(this);
}
return null;
}
String.decode = function (data, encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(data);
}
return null;
}
String.prototype.reverse = function () {
return this.split('').reverse().join('');
}
String.prototype.swapcase = function () {
const isAsciiLetter = /[A-z]/;
const result = [];
for (const l of this)
result.push(isAsciiLetter.test(l) ? String.fromCharCode(l.charCodeAt() ^ 32) : l);
return result.join('');
}
function getRandomString(length) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
let result = "";
for (let i = 0; i < length; i++) {
const random = Math.floor(Math.random() * 61);
result += chars[random];
}
return result;
}
//--------------------------------------------------------------------------------------------------
// Encode/Decode Functions
//--------------------------------------------------------------------------------------------------
function decodeUTF8(data) {
const codes = [];
for (let i = 0; i < data.length;) {
const c = data[i++];
const len = (c > 0xBF) + (c > 0xDF) + (c > 0xEF);
let val = c & (0xFF >> (len + 1));
for (const end = i + len; i < end; i++) {
val = (val << 6) + (data[i] & 0x3F);
}
codes.push(val);
}
return String.fromCharCode(...codes);
}
function encodeUTF8(string) {
const data = [];
for (const c of string) {
const code = c.charCodeAt(0);
const len = (code > 0x7F) + (code > 0x7FF) + (code > 0xFFFF);
let bits = len * 6;
data.push((len ? ~(0xFF >> len + 1) : (0)) + (code >> bits));
while (bits > 0) {
data.push(0x80 + ((code >> (bits -= 6)) & 0x3F))
}
}
return new Uint8Array(data);
}
//--------------------------------------------------------------------------------------------------
// Url
//--------------------------------------------------------------------------------------------------
function absUrl(url, base) {
if (url.search(/^\w+:\/\//) == 0) {
return url;
} else if (url.startsWith('/')) {
return base.slice(0, base.lastIndexOf('/')) + url;
} else {
return base.slice(0, base.lastIndexOf('/') + 1) + url;
}
}

View File

@@ -1,959 +0,0 @@
const mangayomiSources = [{
"name": "AnimeWorld",
"lang": "it",
"baseUrl": "https://www.animeworld.so",
"apiUrl": "",
"iconUrl": "https://i.postimg.cc/RFRGfBvP/FVLyB1I.png",
"typeSource": "single",
"isManga": false,
"itemType": 1,
"version": "0.0.12",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/it/animeworld.js"
}];
class DefaultExtension extends MProvider {
constructor () {
super();
this.client = new Client();
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async parseAnimeList(url) {
const res = await this.client.get(url);
const doc = new Document(res.body);
const elements = doc.select("div#main div.film-list div.item");
const list = [];
for (const element of elements) {
let name = element.selectFirst('a + a').text;
const imageUrl = element.selectFirst('img').getSrc;
const link = element.selectFirst('a').getHref;
const type = element.selectFirst('div.dub').text == 'DUB';
if (type && !name.includes('ITA')) {
name += ' (ITA)';
}
list.push({ name, imageUrl, link });
}
const hasNextPage = parseInt(doc.selectFirst('span.total').text) > parseInt(url.match(/page=(\d+)/)[1]);
return { "list": list, "hasNextPage": hasNextPage };
}
parseStatus(status) {
return {
"in corso": 0,
"finito": 1,
"droppato": 3,
"non rilasciato": 4,
}[status.toLowerCase()] ?? 5;
}
async getPopular(page) {
const res = await this.client.get(this.source.baseUrl + '/tops/ongoing');
const doc = new Document(res.body);
const elements = doc.select('div.content div.item');
const list = [];
for (const element of elements) {
const name = element.selectFirst('div.name').text;
const imageUrl = element.selectFirst('img').getSrc;
const link = element.selectFirst('a').getHref;
list.push({ name, imageUrl, link });
}
return { "list": list, "hasNextPage": false };
}
async getLatestUpdates(page) {
return await this.parseAnimeList(`${this.source.baseUrl}/filter?sort=1&page=${page}`);
}
async search(query, page, filters) {
query = query.trim().replaceAll(/\ +/g, "+");
// Search sometimes failed because filters were empty. I experienced this mostly on android...
if (!filters || filters.length == 0) {
return await this.parseAnimeList(`${this.source.baseUrl}/search?keyword=${query}&page=${page}`);
}
let url = `${this.source.baseUrl}/filter?sort=${filters[5].values[filters[5].state].value}&keyword=${query}`;
for (const filter of filters[0].state) {
if (filter.state == true)
url += `&type=${filter.value}`;
}
for (const filter of filters[1].state) {
if (filter.state == true)
url += `&genre=${filter.value}`;
}
for (const filter of filters[2].state) {
if (filter.state == true)
url += `&status=${filter.value}`;
}
for (const filter of filters[3].state) {
if (filter.state == true)
url += `&dub=${filter.value}`;
}
for (const filter of filters[4].state) {
if (filter.state == true)
url += `&language=${filter.value}`;
}
return await this.parseAnimeList(url + `&page=${page}`);
}
async getDetail(url) {
const res = await this.client.get(this.source.baseUrl + url);
const doc = new Document(res.body);
const detail = {};
const info = doc.selectFirst('div.info div.info');
detail.name = info.selectFirst('h2').text;
detail.imageUrl = info.selectFirst('img').getSrc;
detail.description = info.selectFirst('div.desc').text;
detail.author = info.selectFirst('dt:contains(Studio) + dd').text.trim();
detail.status = this.parseStatus(info.selectFirst('dt:contains(Stato) + dd').text.trim());
detail.genre = info.select('dt:contains(Genere) + dd a').map(e => e.text);
detail.episodes = doc.select('div.server.active li.episode > a').map(e => ({
name: 'Ep. ' + e.text,
url: this.source.baseUrl + e.getHref
})).reverse();
const type = doc.selectFirst('div.info div.info dt:contains(Audio) + dd').text.trim() == 'Italiano' ?
'Doppiato' : 'Subbato';
if (type == 'Doppiato' && !detail.name.includes('ITA')) {
detail.name += ' (ITA)';
}
return detail;
}
// For anime episode video list
async getVideoList(url) {
const res = await this.client.get(url);
const doc = new Document(res.body);
const promises = [];
const videos = [];
const type = doc.selectFirst('div.info div.info dt:contains(Audio) + dd').text.trim() == 'Italiano' ?
'Doppiato' : 'Subbato';
for (const element of doc.select('div#download a')) {
const host = /Download (.*?) -/.exec(element.text)?.[1];
let url = element.getHref;
if (!host || host == 'Diretto') {
// ignore
continue;
} else if (host == 'Alternativo') {
videos.push({url: url, originalUrl: url, quality: `Italiano ${type} Alternativo`, headers: null});
continue;
} else {
url = url.replace('/d/', '/e/');
}
promises.push(extractAny(url, host.toLowerCase(), 'Italiano', type, host));
}
for (const p of (await Promise.allSettled(promises))) {
if (p.status == 'fulfilled') {
videos.push.apply(videos, p.value);
}
}
return sortVideos(videos);
}
getFilterList() {
return [
{
type_name: "GroupFilter",
name: "Tipo",
state: [
['Anime', '0'],
['Movie', '4'],
['OVA', '1'],
['ONA', '2'],
['Special', '3'],
['Music', '5']
].map(x => ({ type_name: 'CheckBox', name: x[0], value: x[1] }))
},
{
type_name: "GroupFilter",
name: "Generi",
state: [
["Arti Marziali", "3"],
["Avanguardia", "5"],
["Avventura", "2"],
["Azione", "1"],
["Bambini", "47"],
["Commedia", "4"],
["Demoni", "6"],
["Drammatico", "7"],
["Ecchi", "8"],
["Fantasy", "9"],
["Gioco", "10"],
["Harem", "11"],
["Hentai", "43"],
["Horror", "13"],
["Josei", "14"],
["Magia", "16"],
["Mecha", "18"],
["Militari", "19"],
["Mistero", "21"],
["Musicale", "20"],
["Parodia", "22"],
["Polizia", "23"],
["Psicologico", "24"],
["Romantico", "46"],
["Samurai", "26"],
["Sci-Fi", "28"],
["Scolastico", "27"],
["Seinen", "29"],
["Sentimentale", "25"],
["Shoujo", "30"],
["Shoujo Ai", "31"],
["Shounen", "32"],
["Shounen Ai", "33"],
["Slice of Life", "34"],
["Spazio", "35"],
["Soprannaturale", "37"],
["Sport", "36"],
["Storico", "12"],
["Superpoteri", "38"],
["Thriller", "39"],
["Vampiri", "40"],
["Veicoli", "48"],
["Yaoi", "41"],
["Yuri", "42"]
].map(x => ({ type_name: 'CheckBox', name: x[0], value: x[1] }))
},
{
type_name: "GroupFilter",
name: "Stato",
state: [
["In corso", "0"],
["Finito", "1"],
["Non rilasciato", "2"],
["Droppato", "3"]
].map(x => ({ type_name: 'CheckBox', name: x[0], value: x[1] }))
},
{
type_name: "GroupFilter",
name: "Sottotitoli",
state: [
["Subbato", "0"],
["Doppiato", "1"]
].map(x => ({ type_name: 'CheckBox', name: x[0], value: x[1] }))
},
{
type_name: "GroupFilter",
name: "Audio",
state: [
["Giapponese", "jp"],
["Italiano", "it"],
["Cinese", "ch"],
["Coreano", "kr"],
["Inglese", "en"]
].map(x => ({ type_name: 'CheckBox', name: x[0], value: x[1] }))
},
{
type_name: "SelectFilter",
type: "sort",
name: "Ordine",
state: 0,
values: [
["Standard", "0"],
["Ultime aggiunte", "1"],
["Lista A-Z", "2"],
["Lista Z-A", "3"],
["Più vecchi", "4"],
["Più recenti", "5"],
["Più visti", "6"]
].map(x => ({type_name: 'SelectOption', name: x[0], value: x[1] }))
}
];
}
getSourcePreferences() {
const languages = ['Italiano'];
const types = ['Doppiato', 'Subbato'];
const resolutions = ['1080p', '720p', '480p'];
const hosts = ['Alternativo', 'VidGuard'];
return [
{
key: 'lang',
listPreference: {
title: 'Preferred Language',
summary: 'If available, this language will be chosen by default. Priority = 0 (lower is better)',
valueIndex: 0,
entries: languages,
entryValues: languages
}
},
{
key: 'type',
listPreference: {
title: 'Preferred Type',
summary: 'If available, this type will be chosen by default. Priority = 1 (lower is better)',
valueIndex: 0,
entries: types,
entryValues: types
}
},
{
key: 'res',
listPreference: {
title: 'Preferred Resolution',
summary: 'If available, this resolution will be chosen by default. Priority = 2 (lower is better)',
valueIndex: 0,
entries: resolutions,
entryValues: resolutions
}
},
{
key: 'host',
listPreference: {
title: 'Preferred Host',
summary: 'If available, this hoster will be chosen by default. Priority = 3 (lower is better)',
valueIndex: 0,
entries: hosts,
entryValues: hosts
}
}
];
}
}
/***************************************************************************************************
*
* mangayomi-js-helpers v1.2
*
* # Video Extractors
* - vidGuardExtractor
* - doodExtractor
* - vidozaExtractor
* - okruExtractor
* - amazonExtractor
* - vidHideExtractor
* - filemoonExtractor
* - mixdropExtractor
* - speedfilesExtractor
* - luluvdoExtractor
* - burstcloudExtractor (not working, see description)
*
* # Video Extractor Wrappers
* - streamWishExtractor
* - voeExtractor
* - mp4UploadExtractor
* - yourUploadExtractor
* - streamTapeExtractor
* - sendVidExtractor
*
* # Video Extractor helpers
* - extractAny
*
* # Playlist Extractors
* - m3u8Extractor
* - jwplayerExtractor
*
* # Extension Helpers
* - sortVideos()
*
* # Uint8Array
* - Uint8Array.fromBase64()
* - Uint8Array.prototype.toBase64()
* - Uint8Array.prototype.decode()
*
* # String
* - String.prototype.encode()
* - String.decode()
* - String.prototype.reverse()
* - String.prototype.swapcase()
* - getRandomString()
*
* # Encode/Decode Functions
* - decodeUTF8
* - encodeUTF8
*
* # Url
* - absUrl()
*
***************************************************************************************************/
//--------------------------------------------------------------------------------------------------
// Video Extractors
//--------------------------------------------------------------------------------------------------
async function vidGuardExtractor(url) {
// get html
const res = await new Client().get(url);
const doc = new Document(res.body);
const script = doc.selectFirst('script:contains(eval)');
// eval code
const code = script.text;
eval?.('var window = {};');
eval?.(code);
const playlistUrl = globalThis.window.svg.stream;
// decode sig
const encoded = playlistUrl.match(/sig=(.*?)&/)[1];
const charCodes = [];
for (let i = 0; i < encoded.length; i += 2) {
charCodes.push(parseInt(encoded.slice(i, i + 2), 16) ^ 2);
}
let decoded = Uint8Array.fromBase64(
String.fromCharCode(...charCodes))
.slice(5, -5)
.reverse();
for (let i = 0; i < decoded.length; i += 2) {
let tmp = decoded[i];
decoded[i] = decoded[i + 1];
decoded[i + 1] = tmp;
}
decoded = decoded.decode();
return await m3u8Extractor(playlistUrl.replace(encoded, decoded), null);
}
async function doodExtractor(url) {
const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false });
let response = await dartClient.get(url);
while ("location" in response.headers) {
response = await dartClient.get(response.headers.location);
}
const newUrl = response.request.url;
const doodhost = newUrl.match(/https:\/\/(.*?)\//, newUrl)[0].slice(8, -1);
const md5 = response.body.match(/'\/pass_md5\/(.*?)',/, newUrl)[0].slice(11, -2);
const token = md5.substring(md5.lastIndexOf("/") + 1);
const expiry = new Date().valueOf();
const randomString = getRandomString(10);
response = await new Client().get(`https://${doodhost}/pass_md5/${md5}`, { "Referer": newUrl });
const videoUrl = `${response.body}${randomString}?token=${token}&expiry=${expiry}`;
const headers = { "User-Agent": "Mangayomi", "Referer": doodhost };
return [{ url: videoUrl, originalUrl: videoUrl, headers: headers, quality: '' }];
}
async function vidozaExtractor(url) {
let response = await new Client({ 'useDartHttpClient': true, "followRedirects": true }).get(url);
const videoUrl = response.body.match(/https:\/\/\S*\.mp4/)[0];
return [{ url: videoUrl, originalUrl: videoUrl, quality: '' }];
}
async function okruExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const tag = doc.selectFirst('div[data-options]');
const playlistUrl = tag.attr('data-options').match(/hlsManifestUrl.*?(h.*?id=\d+)/)[1].replaceAll('\\\\u0026', '&');
return await m3u8Extractor(playlistUrl, null);
}
async function amazonExtractor(url) {
const res = await new Client().get(url);
const doc = new Document(res.body);
const videoUrl = doc.selectFirst('video').getSrc;
return videoUrl ? [{ url: videoUrl, originalUrl: videoUrl, headers: null, quality: '' }] : [];
}
async function vidHideExtractor(url) {
const res = await new Client().get(url);
return await jwplayerExtractor(res.body);
}
async function filemoonExtractor(url, headers) {
headers = headers ?? {};
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
delete headers['user-agent'];
let res = await new Client().get(url, headers);
const src = res.body.match(/iframe src="(.*?)"/)?.[1];
if (src) {
res = await new Client().get(src, {
'Referer': url,
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'User-Agent': headers['User-Agent']
});
}
return await jwplayerExtractor(res.body, headers);
}
async function mixdropExtractor(url) {
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'};
let res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(url, headers);
while ("location" in res.headers) {
res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(res.headers.location, headers);
}
const newUrl = res.request.url;
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(MDCore):contains(eval)').text;
const unpacked = unpackJs(code);
let videoUrl = unpacked.match(/wurl="(.*?)"/)?.[1];
if (!videoUrl) return [];
videoUrl = 'https:' + videoUrl;
headers.referer = newUrl;
return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: headers}];
}
async function speedfilesExtractor(url) {
let res = await new Client().get(url);
let doc = new Document(res.body);
const code = doc.selectFirst('script:contains(var)').text;
let b64;
// Get b64
for (const match of code.matchAll(/(?:var|let|const)\s*\w+\s*=\s*["']([^"']+)/g)) {
if (match[1].match(/[g-zG-Z]/)) {
b64 = match[1];
break;
}
}
// decode b64 => b64
const step1 = Uint8Array.fromBase64(b64).reverse().decode().swapcase();
// decode b64 => hex
const step2 = Uint8Array.fromBase64(step1).reverse().decode();
// decode hex => b64
let step3 = [];
for (let i = 0; i < step2.length; i += 2) {
step3.push(parseInt(step2.slice(i, i + 2), 16) - 3);
}
step3 = String.fromCharCode(...step3.reverse()).swapcase();
// decode b64 => url
const videoUrl = Uint8Array.fromBase64(step3).decode();
return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: null}];
}
async function luluvdoExtractor(url) {
const client = new Client();
const match = url.match(/(.*?:\/\/.*?)\/.*\/(.*)/);
const headers = {'user-agent': 'Mangayomi'};
const res = await client.get(`${match[1]}/dl?op=embed&file_code=${match[2]}`, headers);
return await jwplayerExtractor(res.body, headers);
}
/** Does not work: Client always sets 'charset=utf-8' in Content-Type. */
async function burstcloudExtractor(url) {
let client = new Client();
let res = await client.get(url);
const id = res.body.match(/data-file-id="(.*?)"/)[1];
const headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
'Referer': url,
};
const data = {
'fileId': id
};
res = await client.post(`https://www.burstcloud.co/file/play-request/`, headers, data);
const videoUrl = res.body.match(/cdnUrl":"(.*?)"/)[1];
return [{
url: videoUrl,
originalUrl: videoUrl,
headers: { 'Referer': url.match(/.*?:\/\/.*?\//) },
quality: ''
}];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Wrappers
//--------------------------------------------------------------------------------------------------
_streamWishExtractor = streamWishExtractor;
streamWishExtractor = async (url) => {
return (await _streamWishExtractor(url, '')).map(v => {
v.quality = v.quality.slice(3, -1);
return v;
});
}
_voeExtractor = voeExtractor;
voeExtractor = async (url) => {
return (await _voeExtractor(url, '')).map(v => {
v.quality = v.quality.replace(/Voe: (\d+p?)/i, '$1');
return v;
});
}
_mp4UploadExtractor = mp4UploadExtractor;
mp4UploadExtractor = async (url) => {
return (await _mp4UploadExtractor(url)).map(v => {
v.quality = v.quality.match(/\d+p/)?.[0] ?? '';
return v;
});
}
_yourUploadExtractor = yourUploadExtractor;
yourUploadExtractor = async (url) => {
return (await _yourUploadExtractor(url))
.filter(v => !v.url.includes('/novideo'))
.map(v => {
v.quality = '';
return v;
});
}
_streamTapeExtractor = streamTapeExtractor;
streamTapeExtractor = async (url) => {
return await _streamTapeExtractor(url, '');
}
_sendVidExtractor = sendVidExtractor;
sendVidExtractor = async (url) => {
let res = await new Client().get(url);
var videoUrl, quality;
try {
videoUrl = res.body.match(/og:video" content="(.*?\.mp4.*?)"/)[1];
quality = res.body.match(/og:video:height" content="(.*?)"/)?.[1];
quality = quality ? quality + 'p' : '';
} catch (error) {
}
if (!videoUrl) {
return _sendVidExtractor(url, null, '');
}
return [{url: videoUrl, originalUrl: videoUrl, quality: quality, headers: null}];
}
//--------------------------------------------------------------------------------------------------
// Video Extractor Helpers
//--------------------------------------------------------------------------------------------------
async function extractAny(url, method, lang, type, host, headers = null) {
const m = extractAny.methods[method];
return (!m) ? [] : (await m(url, headers)).map(v => {
v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`;
return v;
});
};
extractAny.methods = {
'amazon': amazonExtractor,
'burstcloud': burstcloudExtractor,
'doodstream': doodExtractor,
'filemoon': filemoonExtractor,
'luluvdo': luluvdoExtractor,
'mixdrop': mixdropExtractor,
'mp4upload': mp4UploadExtractor,
'okru': okruExtractor,
'sendvid': sendVidExtractor,
'speedfiles': speedfilesExtractor,
'streamtape': streamTapeExtractor,
'streamwish': vidHideExtractor,
'vidguard': vidGuardExtractor,
'vidhide': vidHideExtractor,
'vidoza': vidozaExtractor,
'voe': voeExtractor,
'yourupload': yourUploadExtractor
};
//--------------------------------------------------------------------------------------------------
// Playlist Extractors
//--------------------------------------------------------------------------------------------------
async function m3u8Extractor(url, headers = null) {
// https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist
// https://developer.apple.com/documentation/http-live-streaming/adding-alternate-media-to-a-playlist
// define attribute lists
const streamAttributes = [
['avg_bandwidth', /AVERAGE-BANDWIDTH=(\d+)/],
['bandwidth', /\bBANDWIDTH=(\d+)/],
['resolution', /\bRESOLUTION=([\dx]+)/],
['framerate', /\bFRAME-RATE=([\d\.]+)/],
['codecs', /\bCODECS="(.*?)"/],
['video', /\bVIDEO="(.*?)"/],
['audio', /\bAUDIO="(.*?)"/],
['subtitles', /\bSUBTITLES="(.*?)"/],
['captions', /\bCLOSED-CAPTIONS="(.*?)"/]
];
const mediaAttributes = [
['type', /\bTYPE=([\w-]*)/],
['group', /\bGROUP-ID="(.*?)"/],
['lang', /\bLANGUAGE="(.*?)"/],
['name', /\bNAME="(.*?)"/],
['autoselect', /\bAUTOSELECT=(\w*)/],
['default', /\bDEFAULT=(\w*)/],
['instream-id', /\bINSTREAM-ID="(.*?)"/],
['assoc-lang', /\bASSOC-LANGUAGE="(.*?)"/],
['channels', /\bCHANNELS="(.*?)"/],
['uri', /\bURI="(.*?)"/]
];
const streams = [], videos = {}, audios = {}, subtitles = {}, captions = {};
const dict = { 'VIDEO': videos, 'AUDIO': audios, 'SUBTITLES': subtitles, 'CLOSED-CAPTIONS': captions };
const res = await new Client().get(url, headers);
const text = res.body;
if (res.statusCode != 200) {
return [];
}
// collect media
for (const match of text.matchAll(/#EXT-X-MEDIA:(.*)/g)) {
const info = match[1], medium = {};
for (const attr of mediaAttributes) {
const m = info.match(attr[1]);
medium[attr[0]] = m ? m[1] : null;
}
const type = medium.type;
delete medium.type;
const group = medium.group;
delete medium.group;
const typedict = dict[type];
if (typedict[group] == undefined)
typedict[group] = [];
typedict[group].push(medium);
}
// collect streams
for (const match of text.matchAll(/#EXT-X-STREAM-INF:(.*)\s*(.*)/g)) {
const info = match[1], stream = { 'url': absUrl(match[2], url) };
for (const attr of streamAttributes) {
const m = info.match(attr[1]);
stream[attr[0]] = m ? m[1] : null;
}
stream['video'] = videos[stream.video] ?? null;
stream['audio'] = audios[stream.audio] ?? null;
stream['subtitles'] = subtitles[stream.subtitles] ?? null;
stream['captions'] = captions[stream.captions] ?? null;
// format resolution or bandwidth
let quality;
if (stream.resolution) {
quality = stream.resolution.match(/x(\d+)/)[1] + 'p';
} else {
quality = (parseInt(stream.avg_bandwidth ?? stream.bandwidth) / 1000000) + 'Mb/s'
}
// add stream to list
const subs = stream.subtitles?.map((s) => {
return { file: s.uri, label: s.name };
});
const auds = stream.audio?.map((a) => {
return { file: a.uri, label: a.name };
});
streams.push({
url: stream.url,
quality: quality,
originalUrl: stream.url,
headers: headers,
subtitles: subs ?? null,
audios: auds ?? null
});
}
return streams.length ? streams : [{
url: url,
quality: '',
originalUrl: url,
headers: headers,
subtitles: null,
audios: null
}];
}
async function jwplayerExtractor(text, headers) {
// https://docs.jwplayer.com/players/reference/playlists
const getsetup = /setup\(({[\s\S]*?})\)/;
const getsources = /sources:\s*(\[[\s\S]*?\])/;
const gettracks = /tracks:\s*(\[[\s\S]*?\])/;
const unpacked = unpackJs(text);
const videos = [], subtitles = [];
const data = eval('(' + (getsetup.exec(text) || getsetup.exec(unpacked))?.[1] + ')');
if (data){
var sources = data.sources;
var tracks = data.tracks;
} else {
var sources = eval('(' + (getsources.exec(text) || getsources.exec(unpacked))?.[1] + ')');
var tracks = eval('(' + (gettracks.exec(text) || gettracks.exec(unpacked))?.[1] + ')');
}
for (t of tracks) {
if (t.type == "captions") {
subtitles.push({file: t.file, label: t.label});
}
}
for (s of sources) {
if (s.file.includes('master.m3u8')) {
videos.push(...(await m3u8Extractor(s.file, headers)));
} else if (s.file.includes('.mpd')) {
} else {
videos.push({url: s.file, originalUrl: s.file, quality: '', headers: headers});
}
}
return videos.map(v => {
v.subtitles = subtitles;
return v;
});
}
//--------------------------------------------------------------------------------------------------
// Extension Helpers
//--------------------------------------------------------------------------------------------------
function sortVideos(videos) {
const pref = new SharedPreferences();
const getres = RegExp('(\\d+)p?', 'i');
const lang = RegExp(pref.get('lang'), 'i');
const type = RegExp(pref.get('type'), 'i');
const res = RegExp(getres.exec(pref.get('res'))[1], 'i');
const host = RegExp(pref.get('host'), 'i');
let getScore = (q, hasRes) => {
const bLang = lang.test(q), bType = type.test(q), bRes = res.test(q), bHost = host.test(q);
if (hasRes) {
return bLang * (8 + bType * (4 + bRes * (2 + bHost * 1)));
} else {
return bLang * (8 + bType * (4 + (bHost * 3)));
}
}
return videos.sort((a, b) => {
const resA = getres.exec(a.quality)?.[1];
const resB = getres.exec(b.quality)?.[1];
const score = getScore(b.quality, resB) - getScore(a.quality, resA);
if (score) return score;
const qA = resA ? a.quality.replace(resA, (9999 - parseInt(resA)).toString()) : a.quality;
const qB = resA ? b.quality.replace(resB, (9999 - parseInt(resB)).toString()) : b.quality;
return qA.localeCompare(qB);
});
}
//--------------------------------------------------------------------------------------------------
// Uint8Array
//--------------------------------------------------------------------------------------------------
Uint8Array.fromBase64 = function (b64) {
// [00,01,02,03,04,05,06,07,08,\t,\n,0b,0c,\r,0e,0f,10,11,12,13,14,15,16,17,18,19,1a,1b,1c,1d,1e,1f,' ', !, ", #, $, %, &, ', (, ), *, +,',', -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <,'=', >, ?, @,A,B,C,D,E,F,G,H,I,J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~,7f]
const m = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1]
let data = [], val = 0, bits = -8
for (const c of b64) {
let n = m[c.charCodeAt(0)];
if (n == -1) break;
val = (val << 6) + n;
bits += 6;
for (; bits >= 0; bits -= 8)
data.push((val >> bits) & 0xFF);
}
return new Uint8Array(data);
}
Uint8Array.prototype.toBase64 = function () {
const m = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
let b64 = '', val = 0, bits = -6;
for (const b of this) {
val = (val << 8) + b;
bits += 8;
while (bits >= 0) {
b64 += m[(val >> bits) & 0x3F];
bits -= 6;
}
}
if (bits > -6)
b64 += m[(val << -bits) & 0x3F];
return b64 + ['', '', '==', '='][b64.length % 4];
}
Uint8Array.prototype.decode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(this);
}
return null;
}
//--------------------------------------------------------------------------------------------------
// String
//--------------------------------------------------------------------------------------------------
String.prototype.encode = function (encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return encodeUTF8(this);
}
return null;
}
String.decode = function (data, encoding = 'utf-8') {
encoding = encoding.toLowerCase();
if (encoding == 'utf-8') {
return decodeUTF8(data);
}
return null;
}
String.prototype.reverse = function () {
return this.split('').reverse().join('');
}
String.prototype.swapcase = function () {
const isAsciiLetter = /[A-z]/;
const result = [];
for (const l of this)
result.push(isAsciiLetter.test(l) ? String.fromCharCode(l.charCodeAt() ^ 32) : l);
return result.join('');
}
function getRandomString(length) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
let result = "";
for (let i = 0; i < length; i++) {
const random = Math.floor(Math.random() * 61);
result += chars[random];
}
return result;
}
//--------------------------------------------------------------------------------------------------
// Encode/Decode Functions
//--------------------------------------------------------------------------------------------------
function decodeUTF8(data) {
const codes = [];
for (let i = 0; i < data.length;) {
const c = data[i++];
const len = (c > 0xBF) + (c > 0xDF) + (c > 0xEF);
let val = c & (0xFF >> (len + 1));
for (const end = i + len; i < end; i++) {
val = (val << 6) + (data[i] & 0x3F);
}
codes.push(val);
}
return String.fromCharCode(...codes);
}
function encodeUTF8(string) {
const data = [];
for (const c of string) {
const code = c.charCodeAt(0);
const len = (code > 0x7F) + (code > 0x7FF) + (code > 0xFFFF);
let bits = len * 6;
data.push((len ? ~(0xFF >> len + 1) : (0)) + (code >> bits));
while (bits > 0) {
data.push(0x80 + ((code >> (bits -= 6)) & 0x3F))
}
}
return new Uint8Array(data);
}
//--------------------------------------------------------------------------------------------------
// Url
//--------------------------------------------------------------------------------------------------
function absUrl(url, base) {
if (url.search(/^\w+:\/\//) == 0) {
return url;
} else if (url.startsWith('/')) {
return base.slice(0, base.lastIndexOf('/')) + url;
} else {
return base.slice(0, base.lastIndexOf('/') + 1) + url;
}
}

View File

@@ -1,198 +0,0 @@
const mangayomiSources = [{
"name": "360资源",
"lang": "zh",
"baseUrl": "https://360zy.com",
"apiUrl": "",
"iconUrl": "https://360zy.com/favicon.ico",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.1",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/360zy.js"
}];
class DefaultExtension extends MProvider {
dict = new Map([
["&nbsp;", " "],
["&quot;", '"'],
["&lt;", "<"],
["&gt;", ">"],
["&amp;", "&"],
["&sdot;", "·"],
]);
text(content) {
if (!content) return "";
const str =
[...content.matchAll(/>([^<]+?)</g)]
.map((m) => m[1])
.join("")
.trim() || content;
return str.replace(/&[a-z]+;/g, (c) => this.dict.get(c) || c);
}
async request(url) {
const preference = new SharedPreferences();
return (await new Client({ 'useDartHttpClient': true }).get(preference.get("url") + "/api.php/provide/vod?ac=detail" + url, { "Referer": preference.get("url") })).body;
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getPopular(page) {
// let genres = [];
// const gen = JSON.parse(await this.request("&ac=list"));
// gen.class.forEach((e) => {
// genres.push({
// type_name: "SelectOption",
// value: e.type_id,
// name: e.type_name
// });
// });
// console.log(genres)
const res = JSON.parse(await this.request(`&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getLatestUpdates(page) {
const h = (new Date().getUTCHours() + 9) % 24;
const res = JSON.parse(await this.request(`&pg=${page}&h=${h || 24}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async search(query, page, filters) {
var categories;
for (const filter of filters) {
if (filter["type"] == "categories") {
categories = filter["values"][filter["state"]]["value"];
}
}
const res = JSON.parse(await this.request(`&wd=${query}&t=${categories ?? ""}&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getDetail(url) {
let desc = "无";
const anime = JSON.parse(await this.request(url)).list[0];
const blurb = this.text(anime.vod_blurb);
const content = this.text(anime.vod_content);
desc = desc.length < blurb?.length ? blurb : desc;
desc = desc.length < content.length ? content : desc;
const urls = anime.vod_play_url
.split("#")
.filter((e) => e)
.map((e) => {
const s = e.split("$");
return { name: s[0], url: s[1] };
});
return {
name: anime.vod_name,
imageUrl: anime.vod_pic,
description: desc,
episodes: urls
};
}
// For anime episode video list
async getVideoList(url) {
return [{
url: url,
originalUrl: url,
quality: "HLS"
}];
}
// For manga chapter pages
async getPageList() {
throw new Error("getPageList not implemented");
}
getFilterList() {
return [{
type: "categories",
name: "影片類型",
type_name: "SelectFilter",
values: [
// { type_name: "SelectOption", value: "1", name: "电影" },
// { type_name: "SelectOption", value: "2", name: "连续剧" },
// { type_name: "SelectOption", value: "3", name: "综艺" },
{ type_name: "SelectOption", value: "", name: "全部" },
{ type_name: "SelectOption", value: "5", name: "伦理片" },
{ type_name: "SelectOption", value: "6", name: "动作片" },
{ type_name: "SelectOption", value: "7", name: "喜剧片" },
{ type_name: "SelectOption", value: "8", name: "爱情片" },
{ type_name: "SelectOption", value: "9", name: "科幻片" },
{ type_name: "SelectOption", value: "10", name: "恐怖片" },
{ type_name: "SelectOption", value: "11", name: "剧情片" },
{ type_name: "SelectOption", value: "12", name: "战争片" },
{ type_name: "SelectOption", value: "13", name: "国产剧" },
{ type_name: "SelectOption", value: "14", name: "香港剧" },
{ type_name: "SelectOption", value: "15", name: "韩国剧" },
{ type_name: "SelectOption", value: "16", name: "欧美剧" },
{ type_name: "SelectOption", value: "17", name: "体育" },
{ type_name: "SelectOption", value: "18", name: "NBA" },
{ type_name: "SelectOption", value: "20", name: "惊悚片" },
{ type_name: "SelectOption", value: "21", name: "家庭篇" },
{ type_name: "SelectOption", value: "22", name: "古装片" },
{ type_name: "SelectOption", value: "23", name: "历史片" },
{ type_name: "SelectOption", value: "24", name: "悬疑片" },
{ type_name: "SelectOption", value: "25", name: "犯罪片" },
{ type_name: "SelectOption", value: "26", name: "灾难片" },
{ type_name: "SelectOption", value: "27", name: "纪录片" },
{ type_name: "SelectOption", value: "28", name: "短片" },
{ type_name: "SelectOption", value: "29", name: "动画片" },
{ type_name: "SelectOption", value: "30", name: "台湾剧" },
{ type_name: "SelectOption", value: "31", name: "日本剧" },
{ type_name: "SelectOption", value: "32", name: "海外剧" },
{ type_name: "SelectOption", value: "33", name: "泰国剧" },
{ type_name: "SelectOption", value: "34", name: "大陆综艺" },
{ type_name: "SelectOption", value: "35", name: "港台综艺" },
{ type_name: "SelectOption", value: "36", name: "日韩综艺" },
{ type_name: "SelectOption", value: "37", name: "欧美综艺" },
{ type_name: "SelectOption", value: "38", name: "国产动漫" },
{ type_name: "SelectOption", value: "39", name: "欧美动漫" },
{ type_name: "SelectOption", value: "40", name: "日韩动漫" },
{ type_name: "SelectOption", value: "41", name: "足球" },
{ type_name: "SelectOption", value: "42", name: "篮球" },
{ type_name: "SelectOption", value: "43", name: "未分类" },
{ type_name: "SelectOption", value: "45", name: "西部片" },
{ type_name: "SelectOption", value: "46", name: "爽文短剧" },
{ type_name: "SelectOption", value: "47", name: "现代都市" },
{ type_name: "SelectOption", value: "48", name: "脑洞悬疑" },
{ type_name: "SelectOption", value: "49", name: "年代穿越" },
{ type_name: "SelectOption", value: "50", name: "古装仙侠" },
{ type_name: "SelectOption", value: "51", name: "反转爽剧" },
{ type_name: "SelectOption", value: "52", name: "女频恋爱" },
{ type_name: "SelectOption", value: "53", name: "成长逆袭" }
]
}];
}
getSourcePreferences() {
return [
{
"key": "url",
"listPreference": {
"title": "Website Url",
"summary": "",
"valueIndex": 0,
"entries": ["360zycom", "360zynet", "360zytop", "360zytv"],
"entryValues": ["https://360zy.com", "https://360zy.net", "https://360zy.top", "https://360zy.tv"],
}
}
];
}
}

View File

@@ -1,190 +0,0 @@
const mangayomiSources = [{
"name": "非凡资源",
"lang": "zh",
"baseUrl": "http://ffzy.tv",
"apiUrl": "",
"iconUrl": "http://ffzy.tv/template/default/img/favicon.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.25",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/ffzy.js"
}];
class DefaultExtension extends MProvider {
dict = new Map([
["&nbsp;", " "],
["&quot;", '"'],
["&lt;", "<"],
["&gt;", ">"],
["&amp;", "&"],
["&sdot;", "·"],
]);
text(content) {
if (!content) return "";
const str =
[...content.matchAll(/>([^<]+?)</g)]
.map((m) => m[1])
.join("")
.trim() || content;
return str.replace(/&[a-z]+;/g, (c) => this.dict.get(c) || c);
}
async request(url) {
const preference = new SharedPreferences();
return (await new Client({ 'useDartHttpClient': true }).get(preference.get("url") + "/api.php/provide/vod?ac=detail" + url, { "Referer": preference.get("url") })).body;
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getPopular(page) {
// let genres = [];
// const gen = JSON.parse(await this.request("&ac=list"));
// gen.class.forEach((e) => {
// genres.push({
// type_name: "SelectOption",
// value: e.type_id,
// name: e.type_name
// });
// });
// console.log(genres)
const res = JSON.parse(await this.request(`&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getLatestUpdates(page) {
const h = (new Date().getUTCHours() + 9) % 24;
const res = JSON.parse(await this.request(`&pg=${page}&h=${h || 24}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async search(query, page, filters) {
var categories;
for (const filter of filters) {
if (filter["type"] == "categories") {
categories = filter["values"][filter["state"]]["value"];
}
}
const res = JSON.parse(await this.request(`&wd=${query}&t=${categories ?? ""}&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getDetail(url) {
let desc = "无";
const anime = JSON.parse(await this.request(url)).list[0];
const blurb = this.text(anime.vod_blurb);
const content = this.text(anime.vod_content);
desc = desc.length < blurb?.length ? blurb : desc;
desc = desc.length < content.length ? content : desc;
const playLists = anime.vod_play_url.split("$$$");
const urls = [];
for (const playList of playLists) {
const episodes = playList.split("#").filter(e => e);
for (const episode of episodes) {
const parts = episode.split("$");
const name = parts[0];
const episodeUrl = parts[1];
if (episodeUrl.includes("m3u8")) {
urls.push({ name, url: episodeUrl });
}
}
}
return {
name: anime.vod_name,
imageUrl: anime.vod_pic,
description: desc,
episodes: urls
};
}
// For anime episode video list
async getVideoList(url) {
return [
{
url: url,
originalUrl: url,
quality: "HLS"
}
];
}
// For manga chapter pages
async getPageList() {
throw new Error("getPageList not implemented");
}
getFilterList() {
return [{
type: "categories",
name: "影片類型",
type_name: "SelectFilter",
values: [
{ type_name: "SelectOption", value: "", name: "全部" },
// {type_name:"SelectOption",value:"1",name:"电影片"},
// {type_name:"SelectOption",value:"2",name:"连续剧"},
// {type_name:"SelectOption",value:"3",name:"综艺片"},
{ type_name: "SelectOption", value: "4", name: "动漫片" },
{ type_name: "SelectOption", value: "6", name: "动作片" },
{ type_name: "SelectOption", value: "7", name: "喜剧片" },
{ type_name: "SelectOption", value: "8", name: "爱情片" },
{ type_name: "SelectOption", value: "9", name: "科幻片" },
{ type_name: "SelectOption", value: "10", name: "恐怖片" },
{ type_name: "SelectOption", value: "11", name: "剧情片" },
{ type_name: "SelectOption", value: "12", name: "战争片" },
{ type_name: "SelectOption", value: "13", name: "国产剧" },
{ type_name: "SelectOption", value: "14", name: "香港剧" },
{ type_name: "SelectOption", value: "15", name: "韩国剧" },
{ type_name: "SelectOption", value: "16", name: "欧美剧" },
{ type_name: "SelectOption", value: "20", name: "记录片" },
{ type_name: "SelectOption", value: "21", name: "台湾剧" },
{ type_name: "SelectOption", value: "22", name: "日本剧" },
{ type_name: "SelectOption", value: "23", name: "海外剧" },
{ type_name: "SelectOption", value: "24", name: "泰国剧" },
{ type_name: "SelectOption", value: "25", name: "大陆综艺" },
{ type_name: "SelectOption", value: "26", name: "港台综艺" },
{ type_name: "SelectOption", value: "27", name: "日韩综艺" },
{ type_name: "SelectOption", value: "28", name: "欧美综艺" },
{ type_name: "SelectOption", value: "29", name: "国产动漫" },
{ type_name: "SelectOption", value: "30", name: "日韩动漫" },
{ type_name: "SelectOption", value: "31", name: "欧美动漫" },
{ type_name: "SelectOption", value: "32", name: "港台动漫" },
{ type_name: "SelectOption", value: "33", name: "海外动漫" },
{ type_name: "SelectOption", value: "34", name: "伦理片" },
{ type_name: "SelectOption", value: "36", name: "短剧" }
]
}];
}
getSourcePreferences() {
return [
{
"key": "url",
"listPreference": {
"title": "Website Url",
"summary": "",
"valueIndex": 0,
"entries": ["ffzy", "ffzy1", "ffzy2", "ffzy3", "ffzy4", "ffzy5"],
"entryValues": ["http://ffzy.tv", "http://ffzy1.tv", "http://ffzy2.tv", "http://ffzy3.tv", "http://ffzy4.tv", "http://ffzy5.tv"],
}
}
];
}
}

View File

@@ -1,188 +0,0 @@
const mangayomiSources = [{
"name": "华为吧资源",
"lang": "zh",
"baseUrl": "https://huaweiba.live",
"apiUrl": "",
"iconUrl": "https://huaweiba.live/template/ziyuan/images/logo2.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.1",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/huaweiba.js"
}];
class DefaultExtension extends MProvider {
dict = new Map([
["&nbsp;", " "],
["&quot;", '"'],
["&lt;", "<"],
["&gt;", ">"],
["&amp;", "&"],
["&sdot;", "·"],
]);
text(content) {
if (!content) return "";
const str =
[...content.matchAll(/>([^<]+?)</g)]
.map((m) => m[1])
.join("")
.trim() || content;
return str.replace(/&[a-z]+;/g, (c) => this.dict.get(c) || c);
}
async request(url) {
return (await new Client({ 'useDartHttpClient': true }).get(this.source.baseUrl + "/api.php/provide/vod?ac=detail" + url, { "Referer": this.source.baseUrl })).body;
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getPopular(page) {
// let genres = [];
// const gen = JSON.parse(await this.request("&ac=list"));
// gen.class.forEach((e) => {
// genres.push({
// type_name: "SelectOption",
// value: e.type_id,
// name: e.type_name
// });
// });
// console.log(genres)
const res = JSON.parse(await this.request(`&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getLatestUpdates(page) {
const h = (new Date().getUTCHours() + 9) % 24;
const res = JSON.parse(await this.request(`&pg=${page}&h=${h || 24}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async search(query, page, filters) {
var categories;
for (const filter of filters) {
if (filter["type"] == "categories") {
categories = filter["values"][filter["state"]]["value"];
}
}
const res = JSON.parse(await this.request(`&wd=${query}&t=${categories ?? ""}&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getDetail(url) {
let desc = "无";
const anime = JSON.parse(await this.request(url)).list[0];
const blurb = this.text(anime.vod_blurb);
const content = this.text(anime.vod_content);
desc = desc.length < blurb?.length ? blurb : desc;
desc = desc.length < content.length ? content : desc;
const urls = anime.vod_play_url
.split("#")
.filter((e) => e)
.map((e) => {
const s = e.split("$");
return { name: s[0], url: s[1] };
});
return {
name: anime.vod_name,
imageUrl: anime.vod_pic,
description: desc,
episodes: urls
};
}
// For anime episode video list
async getVideoList(url) {
return [{
url: url,
originalUrl: url,
quality: "HLS"
}];
}
// For manga chapter pages
async getPageList() {
throw new Error("getPageList not implemented");
}
getFilterList() {
return [{
type: "categories",
name: "影片類型",
type_name: "SelectFilter",
values: [
{ type_name: "SelectOption", value: "", name: "全部" },
{ type_name: "SelectOption", value: "20", name: "电影" },
{ type_name: "SelectOption", value: "22", name: "冒险片" },
{ type_name: "SelectOption", value: "24", name: "剧情片" },
{ type_name: "SelectOption", value: "26", name: "动作片" },
{ type_name: "SelectOption", value: "28", name: "动画电影" },
{ type_name: "SelectOption", value: "30", name: "同性片" },
{ type_name: "SelectOption", value: "32", name: "喜剧片" },
{ type_name: "SelectOption", value: "34", name: "奇幻片" },
{ type_name: "SelectOption", value: "36", name: "恐怖片" },
{ type_name: "SelectOption", value: "38", name: "悬疑片" },
{ type_name: "SelectOption", value: "40", name: "惊悚片" },
{ type_name: "SelectOption", value: "42", name: "歌舞片" },
{ type_name: "SelectOption", value: "44", name: "灾难片" },
{ type_name: "SelectOption", value: "46", name: "爱情片" },
{ type_name: "SelectOption", value: "48", name: "科幻片" },
{ type_name: "SelectOption", value: "50", name: "犯罪片" },
{ type_name: "SelectOption", value: "52", name: "经典片" },
{ type_name: "SelectOption", value: "54", name: "网络电影" },
{ type_name: "SelectOption", value: "56", name: "战争片" },
{ type_name: "SelectOption", value: "58", name: "伦理片" },
{ type_name: "SelectOption", value: "60", name: "电视剧" },
{ type_name: "SelectOption", value: "62", name: "欧美剧" },
{ type_name: "SelectOption", value: "64", name: "日剧" },
{ type_name: "SelectOption", value: "66", name: "韩剧" },
{ type_name: "SelectOption", value: "68", name: "台剧" },
{ type_name: "SelectOption", value: "70", name: "泰剧" },
{ type_name: "SelectOption", value: "72", name: "国产剧" },
{ type_name: "SelectOption", value: "74", name: "港剧" },
{ type_name: "SelectOption", value: "76", name: "新马剧" },
{ type_name: "SelectOption", value: "78", name: "其他剧" },
{ type_name: "SelectOption", value: "80", name: "动漫" },
{ type_name: "SelectOption", value: "82", name: "综艺" },
{ type_name: "SelectOption", value: "84", name: "体育" },
{ type_name: "SelectOption", value: "86", name: "纪录片" },
{ type_name: "SelectOption", value: "88", name: "篮球" },
{ type_name: "SelectOption", value: "90", name: "足球" },
{ type_name: "SelectOption", value: "92", name: "网球" },
{ type_name: "SelectOption", value: "94", name: "斯诺克" },
{ type_name: "SelectOption", value: "96", name: "欧美动漫" },
{ type_name: "SelectOption", value: "98", name: "日韩动漫" },
{ type_name: "SelectOption", value: "100", name: "国产动漫" },
{ type_name: "SelectOption", value: "102", name: "新马泰动漫" },
{ type_name: "SelectOption", value: "104", name: "港台动漫" },
{ type_name: "SelectOption", value: "106", name: "其他动漫" },
{ type_name: "SelectOption", value: "108", name: "国产综艺" },
{ type_name: "SelectOption", value: "110", name: "日韩综艺" },
{ type_name: "SelectOption", value: "112", name: "欧美综艺" },
{ type_name: "SelectOption", value: "114", name: "新马泰综艺" },
{ type_name: "SelectOption", value: "116", name: "港台综艺" },
{ type_name: "SelectOption", value: "118", name: "其他综艺" },
{ type_name: "SelectOption", value: "120", name: "短剧" },
{ type_name: "SelectOption", value: "122", name: "预告片" }
]
}];
}
getSourcePreferences() {
throw new Error("getSourcePreferences not implemented");
}
}

View File

@@ -1,196 +0,0 @@
const mangayomiSources = [{
"name": "极速资源",
"lang": "zh",
"baseUrl": "https://www.jisuzy.com",
"apiUrl": "",
"iconUrl": "https://www.jisuzy.com/template/default/images/site_logo.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.2",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/jisuzy.js"
}];
class DefaultExtension extends MProvider {
dict = new Map([
["&nbsp;", " "],
["&quot;", '"'],
["&lt;", "<"],
["&gt;", ">"],
["&amp;", "&"],
["&sdot;", "·"],
]);
text(content) {
if (!content) return "";
const str =
[...content.matchAll(/>([^<]+?)</g)]
.map((m) => m[1])
.join("")
.trim() || content;
return str.replace(/&[a-z]+;/g, (c) => this.dict.get(c) || c);
}
async request(url) {
const preference = new SharedPreferences();
return (await new Client({ 'useDartHttpClient': true }).get(preference.get("url") + "/api.php/provide/vod?ac=detail" + url, { "Referer": preference.get("url") })).body;
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getPopular(page) {
// let genres = [];
// const gen = JSON.parse(await this.request("&ac=list"));
// gen.class.forEach((e) => {
// genres.push({
// type_name: "SelectOption",
// value: e.type_id,
// name: e.type_name
// });
// });
// console.log(genres)
const res = JSON.parse(await this.request(`&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getLatestUpdates(page) {
const h = (new Date().getUTCHours() + 9) % 24;
const res = JSON.parse(await this.request(`&pg=${page}&h=${h || 24}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async search(query, page, filters) {
var categories;
for (const filter of filters) {
if (filter["type"] == "categories") {
categories = filter["values"][filter["state"]]["value"];
}
}
const res = JSON.parse(await this.request(`&wd=${query}&t=${categories ?? ""}&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getDetail(url) {
let desc = "无";
const anime = JSON.parse(await this.request(url)).list[0];
const blurb = this.text(anime.vod_blurb);
const content = this.text(anime.vod_content);
desc = desc.length < blurb?.length ? blurb : desc;
desc = desc.length < content.length ? content : desc;
const playLists = anime.vod_play_url.split("$$$");
const urls = [];
for (const playList of playLists) {
const episodes = playList.split("#").filter(e => e);
for (const episode of episodes) {
const parts = episode.split("$");
const name = parts[0];
const episodeUrl = parts[1];
if (episodeUrl.includes("m3u8")) {
urls.push({ name, url: episodeUrl });
}
}
}
return {
name: anime.vod_name,
imageUrl: anime.vod_pic,
description: desc,
episodes: urls
};
}
// For anime episode video list
async getVideoList(url) {
return [
{
url: url,
originalUrl: url,
quality: "HLS"
}
];
}
// For manga chapter pages
async getPageList() {
throw new Error("getPageList not implemented");
}
getFilterList() {
return [{
type: "categories",
name: "影片類型",
type_name: "SelectFilter",
values: [
// { type_name: "SelectOption", value: "1", name: "电影" },
// { type_name: "SelectOption", value: "2", name: "连续剧" },
// { type_name: "SelectOption", value: "3", name: "综艺" },
{ type_name: "SelectOption", value: "", name: "全部" },
{ type_name: "SelectOption", value: "1", name: "电视剧" },
{ type_name: "SelectOption", value: "2", name: "电影" },
{ type_name: "SelectOption", value: "3", name: "欧美剧" },
{ type_name: "SelectOption", value: "4", name: "香港剧" },
{ type_name: "SelectOption", value: "5", name: "韩剧" },
{ type_name: "SelectOption", value: "6", name: "日剧" },
{ type_name: "SelectOption", value: "7", name: "马泰剧" },
{ type_name: "SelectOption", value: "8", name: "伦理片" },
{ type_name: "SelectOption", value: "9", name: "动作片" },
{ type_name: "SelectOption", value: "10", name: "爱情片" },
{ type_name: "SelectOption", value: "11", name: "喜剧片" },
{ type_name: "SelectOption", value: "12", name: "科幻片" },
{ type_name: "SelectOption", value: "13", name: "恐怖片" },
{ type_name: "SelectOption", value: "14", name: "剧情片" },
{ type_name: "SelectOption", value: "15", name: "战争片" },
{ type_name: "SelectOption", value: "16", name: "记录片" },
{ type_name: "SelectOption", value: "17", name: "动漫" },
{ type_name: "SelectOption", value: "20", name: "内地剧" },
{ type_name: "SelectOption", value: "23", name: "动画片" },
{ type_name: "SelectOption", value: "24", name: "中国动漫" },
{ type_name: "SelectOption", value: "25", name: "日本动漫" },
{ type_name: "SelectOption", value: "26", name: "欧美动漫" },
{ type_name: "SelectOption", value: "27", name: "综艺" },
{ type_name: "SelectOption", value: "28", name: "台湾剧" },
{ type_name: "SelectOption", value: "29", name: "体育赛事" },
{ type_name: "SelectOption", value: "30", name: "大陆综艺" },
{ type_name: "SelectOption", value: "31", name: "日韩综艺" },
{ type_name: "SelectOption", value: "32", name: "港台综艺" },
{ type_name: "SelectOption", value: "33", name: "欧美综艺" },
{ type_name: "SelectOption", value: "34", name: "灾难片" },
{ type_name: "SelectOption", value: "35", name: "悬疑片" },
{ type_name: "SelectOption", value: "36", name: "犯罪片" },
{ type_name: "SelectOption", value: "37", name: "奇幻片" },
{ type_name: "SelectOption", value: "38", name: "短剧" }
]
}];
}
getSourcePreferences() {
return [
{
"key": "url",
"listPreference": {
"title": "Website Url",
"summary": "",
"valueIndex": 0,
"entries": ["jisuzy.com", "jisuzy.tv", "jisuziyuan.com"],
"entryValues": ["https://www.jisuzy.com", "https://www.jisuzy.tv", "https://www.jisuziyuan.com"],
}
}
];
}
}

View File

@@ -1,216 +0,0 @@
const mangayomiSources = [{
"name": "至臻视觉",
"lang": "zh",
"baseUrl": "https://mihdr.top",
"apiUrl": "",
"iconUrl": "https://mihdr.top/upload/dycms/20240708-1/9150a85ec594c1e7b14619e6570d7ee4.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.3",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/mihdr.js"
}];
class DefaultExtension extends MProvider {
patternQuark = /(https:\/\/pan\.quark\.cn\/s\/[^"]+)/;
patternUc = /(https:\/\/drive\.uc\.cn\/s\/[^"]+)/;
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getPopular(page) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
async getLatestUpdates(page) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/index.php/vod/show/id/1/page/${page}.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
}
async search(query, page, filters) {
const baseUrl = new SharedPreferences().get("url");
if (query == "") {
var categories;
for (const filter of filters) {
if (filter["type"] == "categories") {
categories = filter["values"][filter["state"]]["value"];
}
}
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/index.php/vod/show/id/${categories}/page/${page}.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
} else {
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/index.php/vod/search/page/${page}/wd/${query}.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select(".module-search-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.video-info .video-info-header a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".video-cover .module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
}
}
async getDetail(url) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + url, { "Referer": baseUrl });
const document = new Document(response.body);
const imageUrl = document.selectFirst("div.video-cover .module-item-cover .module-item-pic img").attr("data-src");
const name = document.selectFirst("div.video-info .video-info-header h1").text;
const description = document.selectFirst("div.video-info .video-info-content").text.replace("[收起部分]", "").replace("[展开全部]", "");
const type_name = "电影";
let quark_share_url_list = [], uc_share_url_list = []
const share_url_list = document.select("div.module-row-one .module-row-info")
.map(e => {
const url = e.selectFirst(".module-row-title p").text;
const quarkMatches = url.match(this.patternQuark);
if (quarkMatches && quarkMatches[1]) {
quark_share_url_list.push(quarkMatches[1]);
}
const ucMatches = url.match(this.patternUc);
if (ucMatches && ucMatches[1]) {
uc_share_url_list.push(ucMatches[1]);
}
return null;
})
.filter(url => url !== null);
let quark_episodes = await quarkFilesExtractor(quark_share_url_list, new SharedPreferences().get("quarkCookie"));
let uc_episodes = await ucFilesExtractor(uc_share_url_list, new SharedPreferences().get("ucCookie"));
let episodes = [...quark_episodes, ...uc_episodes];
return {
name, imageUrl, description, episodes
};
}
// For anime episode video list
async getVideoList(url) {
const videos = [];
const parts = url.split('++');
const type = parts[0].toLowerCase();
let vids;
if (type === 'quark') {
let cookie = new SharedPreferences().get("quarkCookie");
if (cookie == "") {
throw new Error("请先在本扩展设置中填写夸克Cookies, 需要夸克VIP账号 \n Please fill in the Quark Cookies in this extension settings first, you need a Quark VIP account");
} else {
vids = await quarkVideosExtractor(url, cookie);
}
} else if (type === 'uc') {
let cookie = new SharedPreferences().get("ucCookie");
if (cookie == "") {
throw new Error("请先在本扩展设置中填写UC云盘Cookies \n Please fill in the UC Cloud Cookies in this extension settings first");
} else {
vids = await ucVideosExtractor(url, cookie);
}
} else {
throw new Error("不支持的链接类型");
}
for (const vid of vids) {
videos.push(vid);
}
return videos;
}
getFilterList() {
return [{
type: "categories",
name: "影片類型",
type_name: "SelectFilter",
values: [
{ type_name: "SelectOption", value: "1", name: "电影" },
{ type_name: "SelectOption", value: "2", name: "剧集" },
{ type_name: "SelectOption", value: "3", name: "动漫" },
{ type_name: "SelectOption", value: "4", name: "综艺" },
{ type_name: "SelectOption", value: "5", name: "短剧" },
{ type_name: "SelectOption", value: "24", name: "老剧" },
{ type_name: "SelectOption", value: "26", name: "臻彩" }
]
}];
}
getSourcePreferences() {
return [
{
"key": "quarkCookie",
"editTextPreference": {
"title": "夸克Cookies",
"summary": "填写获取到的夸克Cookies",
"value": "",
"dialogTitle": "Cookies",
"dialogMessage": "",
}
},
{
"key": "ucCookie",
"editTextPreference": {
"title": "UC云盘Cookies",
"summary": "填写获取到的UC云盘Cookies",
"value": "",
"dialogTitle": "Cookies",
"dialogMessage": "",
}
},
{
"key": "url",
"listPreference": {
"title": "Website Url",
"summary": "",
"valueIndex": 0,
"entries": [
"https://mihdr.top",
"http://xiaomi666.fun",
"http://www.miqk.cc",
"https://xiaomiai.site"
],
"entryValues": [
"https://mihdr.top",
"http://xiaomi666.fun",
"http://www.miqk.cc",
"https://xiaomiai.site"
],
}
}
];
}
}

View File

@@ -1,163 +0,0 @@
const mangayomiSources = [{
"name": "蜜柑计划",
"lang": "zh",
"baseUrl": "https://mikanani.me",
"apiUrl": "",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/zh.mikan.png",
"typeSource": "torrent",
"itemType": 1,
"isNsfw": false,
"version": "0.0.3",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/mikan.js"
}];
class DefaultExtension extends MProvider {
dateStringToTimestamp(dateString) {
var parts = dateString.split('/');
var year = parseInt(parts[2]);
var month = parseInt(parts[0]) - 1;
var day = parseInt(parts[1]);
var date = new Date(year, month, day);
var timestamp = date.getTime();
return timestamp;
}
baseURL () {
const preference = new SharedPreferences();
var base_url = preference.get("domain_url");
if (base_url.endsWith("/")) {
base_url = base_url.slice(0, -1);
}
return base_url;
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getItems(url, cookies) {
var res;
if (cookies) {
const identity = new SharedPreferences().get("cookies");
res = await new Client().get(this.baseURL() + url, {
Cookie: `.AspNetCore.Identity.Application=${identity}`
});
if (res.body.search("退出登录") == -1) {
return {
list: [{name: "请设置Cookies", link: "", imageUrl: "https://mikan.tangbai.cc/images/mikan-pic.png"}],
hasNextPage: false
}
}
} else {
res = await new Client().get(this.baseURL() + url);
}
const doc = new Document(res.body);
const items = [];
const elements = doc.select("div.m-week-square");
for (const element of elements) {
const url = element.selectFirst("a").attr("href");
if (url == "javascript:void(0);") {
continue;
}
const title = element.selectFirst("a").attr("title");
const cover = this.baseURL() + element.selectFirst("img").attr("data-src");
items.push({
name: title,
imageUrl: cover,
link: url
});
}
return {
list: items,
hasNextPage: false
};
}
async getPopular(page) {
return await this.getItems("/Home/MyBangumi", true);
}
async getLatestUpdates(page) {
return await this.getItems("", false);
}
async search(query, page, filters) {
const res = await new Client().get(this.baseURL() + `/Home/Search?searchstr=${query}`);
const doc = new Document(res.body);
const items = [];
const elements = doc.select("div.central-container ul.list-inline li");
for (const element of elements) {
const title = element.selectFirst("div.an-text").text;
const cover = this.baseURL() + element.selectFirst("span").attr("data-src");
const url = element.selectFirst("a").attr("href");
items.push({
name: title,
imageUrl: cover,
link: url
});
}
return {
list: items,
hasNextPage: false
};
}
async getDetail(url) {
const res = await new Client().get(this.baseURL() + url);
const doc = new Document(res.body);
const cover = this.baseURL() + doc.selectFirst("div.content img").attr("src");
const title = doc.selectFirst("p.title").text;
const desc = doc.selectFirst("div.info").text;
const eps = [];
const lists = doc.select("div.m-bangumi-list-content");
for (const list of lists) {
//const header = list.selectFirst("span.title").text;
for (const item of list.select("div.m-bangumi-item")) {
const title = item.selectFirst("div.text").text;
const url = this.baseURL() + item.selectFirst("div.right a").attr("href");
const date = this.dateStringToTimestamp(item.selectFirst("div.date").text.split(" ")[0]);
eps.push({
name: title,
url: url,
dateUpload: date.toString()
});
}
}
//eps.reverse();
return {
name: title,
imageUrl: cover,
description: desc,
episodes: eps
};
}
getFilterList() {
throw new Error("getFilterList not implemented");
}
getSourcePreferences() {
return [{
"key": "domain_url",
"editTextPreference": {
"title": "Url",
"summary": "蜜柑计划网址",
"value": "https://mikanani.me",
"dialogTitle": "URL",
"dialogMessage": "",
}
},{
"key": "cookies",
"editTextPreference": {
"title": "用户Cookies在webview中登陆则可不设",
"summary": "用于读取用户订阅的Cookies.AspNetCore.Identity.Application",
"value": "",
"dialogTitle": "Cookies",
"dialogMessage": "",
}
}];
}
}

View File

@@ -1,214 +0,0 @@
const mangayomiSources = [{
"name": "小米UC资源站",
"lang": "zh",
"baseUrl": "http://www.mucpan.cc",
"apiUrl": "",
"iconUrl": "http://www.mucpan.cc/template/DYXS2/static/picture/index_logo.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.3",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/mucpan.js"
}];
class DefaultExtension extends MProvider {
patternQuark = /(https:\/\/pan\.quark\.cn\/s\/[^"]+)/;
patternUc = /(https:\/\/drive\.uc\.cn\/s\/[^"]+)/;
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getPopular(page) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
async getLatestUpdates(page) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/index.php/vod/show/id/20/page/${page}.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
}
async search(query, page, filters) {
const baseUrl = new SharedPreferences().get("url");
if (query == "") {
var categories;
for (const filter of filters) {
if (filter["type"] == "categories") {
categories = filter["values"][filter["state"]]["value"];
}
}
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/index.php/vod/show/id/${categories}/page/${page}.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
} else {
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/index.php/vod/search/page/${page}/wd/${query}.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select(".module-search-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.video-info .video-info-header a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".video-cover .module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
}
}
async getDetail(url) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + url, { "Referer": baseUrl });
const document = new Document(response.body);
const imageUrl = document.selectFirst("div.video-cover .module-item-cover .module-item-pic img").attr("data-src");
const name = document.selectFirst("div.video-info .video-info-header h1").text;
const description = document.selectFirst("div.video-info .video-info-content").text.replace("[收起部分]", "").replace("[展开全部]", "");
const type_name = "电影";
let quark_share_url_list = [], uc_share_url_list = []
const share_url_list = document.select("div.module-row-one .module-row-info")
.map(e => {
const url = e.selectFirst(".module-row-title p").text;
const quarkMatches = url.match(this.patternQuark);
if (quarkMatches && quarkMatches[1]) {
quark_share_url_list.push(quarkMatches[1]);
}
const ucMatches = url.match(this.patternUc);
if (ucMatches && ucMatches[1]) {
uc_share_url_list.push(ucMatches[1]);
}
return null;
})
.filter(url => url !== null);
let quark_episodes = await quarkFilesExtractor(quark_share_url_list, new SharedPreferences().get("quarkCookie"));
let uc_episodes = await ucFilesExtractor(uc_share_url_list, new SharedPreferences().get("ucCookie"));
let episodes = [...quark_episodes, ...uc_episodes];
return {
name, imageUrl, description, episodes
};
}
// For anime episode video list
async getVideoList(url) {
const videos = [];
const parts = url.split('++');
const type = parts[0].toLowerCase();
let vids;
if (type === 'quark') {
let cookie = new SharedPreferences().get("quarkCookie");
if (cookie == "") {
throw new Error("请先在本扩展设置中填写夸克Cookies, 需要夸克VIP账号 \n Please fill in the Quark Cookies in this extension settings first, you need a Quark VIP account");
} else {
vids = await quarkVideosExtractor(url, cookie);
}
} else if (type === 'uc') {
let cookie = new SharedPreferences().get("ucCookie");
if (cookie == "") {
throw new Error("请先在本扩展设置中填写UC云盘Cookies \n Please fill in the UC Cloud Cookies in this extension settings first");
} else {
vids = await ucVideosExtractor(url, cookie);
}
} else {
throw new Error("不支持的链接类型");
}
for (const vid of vids) {
videos.push(vid);
}
return videos;
}
getFilterList() {
return [{
type: "categories",
name: "影片類型",
type_name: "SelectFilter",
values: [
{ type_name: "SelectOption", value: "20", name: "电影" },
{ type_name: "SelectOption", value: "21", name: "剧集" },
{ type_name: "SelectOption", value: "22", name: "动漫" },
{ type_name: "SelectOption", value: "23", name: "综艺" },
{ type_name: "SelectOption", value: "24", name: "少儿" }
]
}];
}
getSourcePreferences() {
return [
{
"key": "quarkCookie",
"editTextPreference": {
"title": "夸克Cookies",
"summary": "填写获取到的夸克Cookies",
"value": "",
"dialogTitle": "Cookies",
"dialogMessage": "",
}
},
{
"key": "ucCookie",
"editTextPreference": {
"title": "UC云盘Cookies",
"summary": "填写获取到的UC云盘Cookies",
"value": "",
"dialogTitle": "Cookies",
"dialogMessage": "",
}
},
{
"key": "url",
"listPreference": {
"title": "Website Url",
"summary": "",
"valueIndex": 0,
"entries": [
"https://www.milvdou.fun",
"https://www.mucpan.cc",
"https://mucpan.cc",
"http://milvdou.fun"
],
"entryValues": [
"https://www.milvdou.fun",
"https://www.mucpan.cc",
"https://mucpan.cc",
"http://milvdou.fun"
],
}
}
];
}
}

View File

@@ -1,195 +0,0 @@
const mangayomiSources = [{
"name": "天空资源网",
"lang": "zh",
"baseUrl": "https://tiankongzy.com",
"apiUrl": "",
"iconUrl": "https://api.tiankongapi.com/template/v10012/images/logo.jpg",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.2",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/tiankongzy.js"
}];
class DefaultExtension extends MProvider {
dict = new Map([
["&nbsp;", " "],
["&quot;", '"'],
["&lt;", "<"],
["&gt;", ">"],
["&amp;", "&"],
["&sdot;", "·"],
]);
text(content) {
if (!content) return "";
const str =
[...content.matchAll(/>([^<]+?)</g)]
.map((m) => m[1])
.join("")
.trim() || content;
return str.replace(/&[a-z]+;/g, (c) => this.dict.get(c) || c);
}
async request(url) {
const preference = new SharedPreferences();
return (await new Client({ 'useDartHttpClient': true }).get(preference.get("url") + "/api.php/provide/vod?ac=detail" + url, { "Referer": preference.get("url") })).body;
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getPopular(page) {
// let genres = [];
// const gen = JSON.parse(await this.request("&ac=list"));
// gen.class.forEach((e) => {
// genres.push({
// type_name: "SelectOption",
// value: e.type_id,
// name: e.type_name
// });
// });
// console.log(genres)
const res = JSON.parse(await this.request(`&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getLatestUpdates(page) {
const h = (new Date().getUTCHours() + 9) % 24;
const res = JSON.parse(await this.request(`&pg=${page}&h=${h || 24}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async search(query, page, filters) {
var categories;
for (const filter of filters) {
if (filter["type"] == "categories") {
categories = filter["values"][filter["state"]]["value"];
}
}
const res = JSON.parse(await this.request(`&wd=${query}&t=${categories ?? ""}&pg=${page}`));
return {
list: res.list.map((e) => ({
link: "&ids=" + e.vod_id,
imageUrl: e.vod_pic,
name: e.vod_name
})),
hasNextPage: true
};
}
async getDetail(url) {
let desc = "无";
const anime = JSON.parse(await this.request(url)).list[0];
const blurb = this.text(anime.vod_blurb);
const content = this.text(anime.vod_content);
desc = desc.length < blurb?.length ? blurb : desc;
desc = desc.length < content.length ? content : desc;
const playLists = anime.vod_play_url.split("$$$");
const urls = [];
for (const playList of playLists) {
const episodes = playList.split("#").filter(e => e);
for (const episode of episodes) {
const parts = episode.split("$");
const name = parts[0];
const episodeUrl = parts[1];
if (episodeUrl.includes("m3u8")) {
urls.push({ name, url: episodeUrl });
}
}
}
return {
name: anime.vod_name,
imageUrl: anime.vod_pic,
description: desc,
episodes: urls
};
}
// For anime episode video list
async getVideoList(url) {
return [
{
url: url,
originalUrl: url,
quality: "HLS"
}
];
}
// For manga chapter pages
async getPageList() {
throw new Error("getPageList not implemented");
}
getFilterList() {
return [{
type: "categories",
name: "影片類型",
type_name: "SelectFilter",
values: [
{ type_name: "SelectOption", value: "", name: "全部" },
{ type_name: "SelectOption", value: "1", name: "电影" },
{ type_name: "SelectOption", value: "2", name: "纪录片" },
{ type_name: "SelectOption", value: "3", name: "连续剧" },
{ type_name: "SelectOption", value: "4", name: "欧美剧" },
{ type_name: "SelectOption", value: "5", name: "香港剧" },
{ type_name: "SelectOption", value: "6", name: "动作片" },
{ type_name: "SelectOption", value: "7", name: "爱情片" },
{ type_name: "SelectOption", value: "8", name: "科幻片" },
{ type_name: "SelectOption", value: "9", name: "恐怖片" },
{ type_name: "SelectOption", value: "10", name: "剧情片" },
{ type_name: "SelectOption", value: "11", name: "战争片" },
{ type_name: "SelectOption", value: "12", name: "喜剧片" },
{ type_name: "SelectOption", value: "20", name: "动画片" },
{ type_name: "SelectOption", value: "21", name: "犯罪片" },
{ type_name: "SelectOption", value: "22", name: "国产剧" },
{ type_name: "SelectOption", value: "23", name: "韩国剧" },
{ type_name: "SelectOption", value: "24", name: "动漫" },
{ type_name: "SelectOption", value: "25", name: "综艺" },
{ type_name: "SelectOption", value: "26", name: "大陆综艺" },
{ type_name: "SelectOption", value: "27", name: "港台综艺" },
{ type_name: "SelectOption", value: "28", name: "日韩综艺" },
{ type_name: "SelectOption", value: "29", name: "欧美综艺" },
{ type_name: "SelectOption", value: "30", name: "台湾剧" },
{ type_name: "SelectOption", value: "31", name: "国产动漫" },
{ type_name: "SelectOption", value: "32", name: "日本动漫" },
{ type_name: "SelectOption", value: "33", name: "欧美动漫" },
{ type_name: "SelectOption", value: "34", name: "海外动漫" },
{ type_name: "SelectOption", value: "35", name: "泰国剧" },
{ type_name: "SelectOption", value: "36", name: "日剧" },
{ type_name: "SelectOption", value: "37", name: "电影解说" },
{ type_name: "SelectOption", value: "38", name: "奇幻片" },
{ type_name: "SelectOption", value: "39", name: "灾难片" },
{ type_name: "SelectOption", value: "40", name: "悬疑片" },
{ type_name: "SelectOption", value: "41", name: "其他片" },
{ type_name: "SelectOption", value: "42", name: "体育赛事" },
{ type_name: "SelectOption", value: "44", name: "短剧" }
]
}];
}
getSourcePreferences() {
return [
{
"key": "url",
"listPreference": {
"title": "Website Url",
"summary": "",
"valueIndex": 0,
"entries": ["tiankongzy.com", "tkzy1", "tkzy2", "tkzy3", "tkzy4", "tkzy5", "tkzy6", "tkzy7", "tkzy8", "tkzy9"],
"entryValues": ["https://tiankongzy.com", "https://tkzy1.com", "https://tkzy2.com", "https://tkzy3.com", "https://tkzy4.com", "https://tkzy5.com", "https://tkzy6.com", "https://tkzy7.com", "https://tkzy8.com", "https://tkzy9.com"],
}
}
];
}
}

View File

@@ -1,220 +0,0 @@
const mangayomiSources = [{
"name": "玩偶哥哥",
"lang": "zh",
"baseUrl": "https://www.wogg.one",
"apiUrl": "",
"iconUrl": "https://imgsrc.baidu.com/forum/pic/item/4b90f603738da977d5da660af651f8198618e31f.jpg",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.3",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/wogg.js"
}];
class DefaultExtension extends MProvider {
patternQuark = /(https:\/\/pan\.quark\.cn\/s\/[^"]+)/;
patternUc = /(https:\/\/drive\.uc\.cn\/s\/[^"]+)/;
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getPopular(page) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
async getLatestUpdates(page) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/vodshow/1--------${page}---.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
}
async search(query, page, filters) {
const baseUrl = new SharedPreferences().get("url");
if (query == "") {
var categories;
for (const filter of filters) {
if (filter["type"] == "categories") {
categories = filter["values"][filter["state"]]["value"];
}
}
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/vodshow/${categories}--------${page}---.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
} else {
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/vodsearch/${query}----------${page}---.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select(".module-search-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.video-info .video-info-header a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".video-cover .module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
}
}
async getDetail(url) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + url, { "Referer": baseUrl });
const document = new Document(response.body);
const imageUrl = document.selectFirst("div.video-cover .module-item-cover .module-item-pic img").attr("data-src");
const name = document.selectFirst("div.video-info .video-info-header h1").text;
const description = document.selectFirst("div.video-info .video-info-content").text.replace("[收起部分]", "").replace("[展开全部]", "");
const type_name = "电影";
let quark_share_url_list = [], uc_share_url_list = []
const share_url_list = document.select("div.module-row-one .module-row-info")
.map(e => {
const url = e.selectFirst(".module-row-title p").text;
const quarkMatches = url.match(this.patternQuark);
if (quarkMatches && quarkMatches[1]) {
quark_share_url_list.push(quarkMatches[1]);
}
const ucMatches = url.match(this.patternUc);
if (ucMatches && ucMatches[1]) {
uc_share_url_list.push(ucMatches[1]);
}
return null;
})
.filter(url => url !== null);
let quark_episodes = await quarkFilesExtractor(quark_share_url_list, new SharedPreferences().get("quarkCookie"));
let uc_episodes = await ucFilesExtractor(uc_share_url_list, new SharedPreferences().get("ucCookie"));
let episodes = [...quark_episodes, ...uc_episodes];
return {
name, imageUrl, description, episodes
};
}
// For anime episode video list
async getVideoList(url) {
const videos = [];
const parts = url.split('++');
const type = parts[0].toLowerCase();
let vids;
if (type === 'quark') {
let cookie = new SharedPreferences().get("quarkCookie");
if (cookie == "") {
throw new Error("请先在本扩展设置中填写夸克Cookies, 需要夸克VIP账号 \n Please fill in the Quark Cookies in this extension settings first, you need a Quark VIP account");
} else {
vids = await quarkVideosExtractor(url, cookie);
}
} else if (type === 'uc') {
let cookie = new SharedPreferences().get("ucCookie");
if (cookie == "") {
throw new Error("请先在本扩展设置中填写UC云盘Cookies \n Please fill in the UC Cloud Cookies in this extension settings first");
} else {
vids = await ucVideosExtractor(url, cookie);
}
} else {
throw new Error("不支持的链接类型");
}
for (const vid of vids) {
videos.push(vid);
}
return videos;
}
getFilterList() {
return [{
type: "categories",
name: "影片類型",
type_name: "SelectFilter",
values: [
{ type_name: "SelectOption", value: "1", name: "电影" },
{ type_name: "SelectOption", value: "2", name: "剧集" },
{ type_name: "SelectOption", value: "3", name: "动漫" },
{ type_name: "SelectOption", value: "4", name: "综艺" },
{ type_name: "SelectOption", value: "5", name: "音乐" },
{ type_name: "SelectOption", value: "6", name: "短剧" },
{ type_name: "SelectOption", value: "44", name: "臻彩视界" }
]
}];
}
getSourcePreferences() {
return [
{
"key": "quarkCookie",
"editTextPreference": {
"title": "夸克Cookies",
"summary": "填写获取到的夸克Cookies",
"value": "",
"dialogTitle": "Cookies",
"dialogMessage": "",
}
},
{
"key": "ucCookie",
"editTextPreference": {
"title": "UC云盘Cookies",
"summary": "填写获取到的UC云盘Cookies",
"value": "",
"dialogTitle": "Cookies",
"dialogMessage": "",
}
},
{
"key": "url",
"listPreference": {
"title": "Website Url",
"summary": "",
"valueIndex": 0,
"entries": [
"https://wogg.xxooo.cf",
"https://wogg.333232.xyz",
"https://woggpan.333232.xyz",
"https://wogg.heshiheng.top",
"https://www.wogg.one",
"https://www.wogg.lol"
],
"entryValues": [
"https://wogg.xxooo.cf",
"https://wogg.333232.xyz",
"https://woggpan.333232.xyz",
"https://wogg.heshiheng.top",
"https://www.wogg.one",
"https://www.wogg.lol"
],
}
}
];
}
}

View File

@@ -1,378 +0,0 @@
const mangayomiSources = [{
"name": "樱花动漫",
"lang": "zh",
"baseUrl": "http://www.iyinghua.com",
"apiUrl": "",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/zh.yhdm.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.2",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/yhdm.js"
}];
class DefaultExtension extends MProvider {
stringUTF8(text, d) {
if (!d) {
return text;
}
var bytes = [];
for (var i = 0; i < text.length; i++) {
bytes.push(text.charCodeAt(i));
}
var charCodes = [];
var i = 0;
while (i < bytes.length) {
var byte1 = bytes[i];
var charCode;
if (byte1 < 0x80) {
charCode = byte1;
i += 1;
} else if (byte1 < 0xE0) {
var byte2 = bytes[i + 1];
charCode = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F);
i += 2;
} else if (byte1 < 0xF0) {
var byte2 = bytes[i + 1];
var byte3 = bytes[i + 2];
charCode = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
i += 3;
} else {
var byte2 = bytes[i + 1];
var byte3 = bytes[i + 2];
var byte4 = bytes[i + 3];
charCode = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
i += 4;
}
charCodes.push(charCode);
}
return String.fromCharCode.apply(null, charCodes);
}
getBaseUrl() {
const preference = new SharedPreferences();
var base_url = preference.get("domain_url");
if (base_url.endsWith("/")) {
return base_url.slice(0, -1);
}
return base_url;
}
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getItems(url, p, d) {
const res = await new Client().get(this.getBaseUrl() + url);
const doc = new Document(res.body);
const items = [];
const elements = doc.select(p);
for (const element of elements) {
items.push({
name: this.stringUTF8(element.selectFirst("img").attr("alt"), d),
imageUrl: element.selectFirst("img").attr("src"),
link: element.selectFirst("a").attr("href")
});
}
return {
list: items,
hasNextPage: true
};
}
async getPopular(page) {
const results = await this.getItems("", "div.img li", true);
results["hasNextPage"] = false;
return results;
}
async getLatestUpdates(page) {
return await this.getItems(`/${new Date().getFullYear()}/${(page == 1)? "":page.toString()+".html"}`, "div.lpic li", true);
}
async search(query, page, filters) {
if (query) {
return await this.getItems(`/search/${query}/?page=${page}`, "div.lpic li", false);
}
return await this.getItems(filters[0]["values"][filters[0]["state"]]["value"] + ((page == 1) ? "" : page.toString() + ".html"), "div.lpic li", true);
}
async getDetail(url) {
const res = await new Client().get(this.getBaseUrl() + url);
const doc = new Document(res.body);
const cover = doc.selectFirst("div.thumb img").attr("src");
const title = this.stringUTF8(doc.selectFirst("div.rate h1").text, true);
const genre = doc.select("div.sinfo a").map(e => this.stringUTF8(e.text, true));
genre.splice(-1);
const desc = this.stringUTF8(doc.selectFirst("div.info").text, true);
const eps = [];
const elements = doc.select("div.movurl a");
for (const element of elements) {
eps.push({
name: this.stringUTF8(element.text, true),
url: element.attr("href")
});
}
if (eps[0]["url"].search("-1.html") != -1) {
eps.reverse();
}
return {
name: title,
imageUrl: cover,
description: desc,
genre: genre,
episodes: eps
};
}
async getVideoList(url) {
const res = await new Client().get(this.getBaseUrl() + url);
const doc = new Document(res.body);
const video_url = this.stringUTF8(doc.selectFirst("div#playbox").attr("data-vid").split("$")[0], true);
return [{
url: video_url,
originalUrl: video_url,
quality: "Origin"
}];
}
getFilterList() {
return [{
type: "Select",
type_name: "SelectFilter",
name: "分类",
values: [{
name: "2024",
value: "/2024/",
type_name: "SelectOption"
},
{
name: "2023",
value: "/2023/",
type_name: "SelectOption"
},
{
name: "2022",
value: "/2022/",
type_name: "SelectOption"
},
{
name: "2021",
value: "/2021/",
type_name: "SelectOption"
},
{
name: "2020",
value: "/2020/",
type_name: "SelectOption"
},
{
name: "2019",
value: "/2019/",
type_name: "SelectOption"
},
{
name: "2018",
value: "/2018/",
type_name: "SelectOption"
},
{
name: "2017",
value: "/2017/",
type_name: "SelectOption"
},
{
name: "2016",
value: "/2016/",
type_name: "SelectOption"
},
{
name: "2015",
value: "/2015/",
type_name: "SelectOption"
},
{
name: "日本",
value: "/japan/",
type_name: "SelectOption"
},
{
name: "大陆",
value: "/china/",
type_name: "SelectOption"
},
{
name: "美国",
value: "/american/",
type_name: "SelectOption"
},
{
name: "英国",
value: "/england/",
type_name: "SelectOption"
},
{
name: "韩国",
value: "/korea/",
type_name: "SelectOption"
},
{
name: "日语",
value: "/29/",
type_name: "SelectOption"
},
{
name: "国语",
value: "/30/",
type_name: "SelectOption"
},
{
name: "粤语",
value: "/31/",
type_name: "SelectOption"
},
{
name: "英语",
value: "/32/",
type_name: "SelectOption"
},
{
name: "韩语",
value: "/33/",
type_name: "SelectOption"
},
{
name: "方言",
value: "/34/",
type_name: "SelectOption"
},
{
name: "热血",
value: "/66/",
type_name: "SelectOption"
},
{
name: "格斗",
value: "/64/",
type_name: "SelectOption"
},
{
name: "恋爱",
value: "/91/",
type_name: "SelectOption"
},
{
name: "校园",
value: "/70/",
type_name: "SelectOption"
},
{
name: "搞笑",
value: "/67/",
type_name: "SelectOption"
},
{
name: "LOLI",
value: "/111/",
type_name: "SelectOption"
},
{
name: "神魔",
value: "/83/",
type_name: "SelectOption"
},
{
name: "机战",
value: "/81/",
type_name: "SelectOption"
},
{
name: "科幻",
value: "/75/",
type_name: "SelectOption"
},
{
name: "真人",
value: "/74/",
type_name: "SelectOption"
},
{
name: "青春",
value: "/84/",
type_name: "SelectOption"
},
{
name: "魔法",
value: "/73/",
type_name: "SelectOption"
},
{
name: "美少女",
value: "/72/",
type_name: "SelectOption"
},
{
name: "神话",
value: "/102/",
type_name: "SelectOption"
},
{
name: "冒险",
value: "/61/",
type_name: "SelectOption"
},
{
name: "运动",
value: "/69/",
type_name: "SelectOption"
},
{
name: "竞技",
value: "/62/",
type_name: "SelectOption"
},
{
name: "童话",
value: "/103/",
type_name: "SelectOption"
},
{
name: "励志",
value: "/85/",
type_name: "SelectOption"
},
{
name: "后宫",
value: "/99/",
type_name: "SelectOption"
},
{
name: "战争",
value: "/80/",
type_name: "SelectOption"
},
{
name: "吸血鬼",
value: "/119/",
type_name: "SelectOption"
}
]
}];
}
getSourcePreferences() {
return [{
"key": "domain_url",
"editTextPreference": {
"title": "Url",
"summary": "樱花动漫网址",
"value": "http://www.iyinghua.com",
"dialogTitle": "URL",
"dialogMessage": "",
}
}
];
}
}

View File

@@ -1,213 +0,0 @@
const mangayomiSources = [{
"name": "多多影音",
"lang": "zh",
"baseUrl": "https://tv.yydsys.top",
"apiUrl": "",
"iconUrl": "https://tv.yydsys.top/template/DYXS2/static/picture/logo.png",
"typeSource": "single",
"itemType": 1,
"isNsfw": false,
"version": "0.0.3",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/zh/yydsys.js"
}];
class DefaultExtension extends MProvider {
patternQuark = /(https:\/\/pan\.quark\.cn\/s\/[^"]+)/;
patternUc = /(https:\/\/drive\.uc\.cn\/s\/[^"]+)/;
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
async getPopular(page) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: false
}
}
async getLatestUpdates(page) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/index.php/vod/show/id/1/page/${page}.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
}
async search(query, page, filters) {
const baseUrl = new SharedPreferences().get("url");
if (query == "") {
var categories;
for (const filter of filters) {
if (filter["type"] == "categories") {
categories = filter["values"][filter["state"]]["value"];
}
}
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/index.php/vod/show/id/${categories}/page/${page}.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select("div.module-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.module-item-cover .module-item-pic a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
} else {
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + `/index.php/vod/search/page/${page}/wd/${query}.html`, { "Referer": baseUrl });
const elements = new Document(response.body).select(".module-search-item");
const list = [];
for (const element of elements) {
let oneA = element.selectFirst('.video-info .video-info-header a');
const name = oneA.attr("title");
const imageUrl = element.selectFirst(".video-cover .module-item-cover .module-item-pic img").attr("data-src");
const link = oneA.attr("href");
list.push({ name, imageUrl, link });
}
return {
list: list,
hasNextPage: true
}
}
}
async getDetail(url) {
const baseUrl = new SharedPreferences().get("url");
const response = await new Client({ 'useDartHttpClient': true }).get(baseUrl + url, { "Referer": baseUrl });
const document = new Document(response.body);
const imageUrl = document.selectFirst("div.video-cover .module-item-cover .module-item-pic img").attr("data-src");
const name = document.selectFirst("div.video-info .video-info-header h1").text;
const description = document.selectFirst("div.video-info .video-info-content").text.replace("[收起部分]", "").replace("[展开全部]", "");
const type_name = "电影";
let quark_share_url_list = [], uc_share_url_list = []
const share_url_list = document.select("div.module-row-one .module-row-info")
.map(e => {
const url = e.selectFirst(".module-row-title p").text;
const quarkMatches = url.match(this.patternQuark);
if (quarkMatches && quarkMatches[1]) {
quark_share_url_list.push(quarkMatches[1]);
}
const ucMatches = url.match(this.patternUc);
if (ucMatches && ucMatches[1]) {
uc_share_url_list.push(ucMatches[1]);
}
return null;
})
.filter(url => url !== null);
let quark_episodes = await quarkFilesExtractor(quark_share_url_list, new SharedPreferences().get("quarkCookie"));
let uc_episodes = await ucFilesExtractor(uc_share_url_list, new SharedPreferences().get("ucCookie"));
let episodes = [...quark_episodes, ...uc_episodes];
return {
name, imageUrl, description, episodes
};
}
// For anime episode video list
async getVideoList(url) {
const videos = [];
const parts = url.split('++');
const type = parts[0].toLowerCase();
let vids;
if (type === 'quark') {
let cookie = new SharedPreferences().get("quarkCookie");
if (cookie == "") {
throw new Error("请先在本扩展设置中填写夸克Cookies, 需要夸克VIP账号 \n Please fill in the Quark Cookies in this extension settings first, you need a Quark VIP account");
} else {
vids = await quarkVideosExtractor(url, cookie);
}
} else if (type === 'uc') {
let cookie = new SharedPreferences().get("ucCookie");
if (cookie == "") {
throw new Error("请先在本扩展设置中填写UC云盘Cookies \n Please fill in the UC Cloud Cookies in this extension settings first");
} else {
vids = await ucVideosExtractor(url, cookie);
}
} else {
throw new Error("不支持的链接类型");
}
for (const vid of vids) {
videos.push(vid);
}
return videos;
}
getFilterList() {
return [{
type: "categories",
name: "影片類型",
type_name: "SelectFilter",
values: [
{ type_name: "SelectOption", value: "1", name: "电影" },
{ type_name: "SelectOption", value: "2", name: "剧集" },
{ type_name: "SelectOption", value: "4", name: "动漫" },
{ type_name: "SelectOption", value: "3", name: "综艺" },
{ type_name: "SelectOption", value: "5", name: "短剧" },
{ type_name: "SelectOption", value: "20", name: "纪录片" }
]
}];
}
getSourcePreferences() {
return [
{
"key": "quarkCookie",
"editTextPreference": {
"title": "夸克Cookies",
"summary": "填写获取到的夸克Cookies",
"value": "",
"dialogTitle": "Cookies",
"dialogMessage": "",
}
},
{
"key": "ucCookie",
"editTextPreference": {
"title": "UC云盘Cookies",
"summary": "填写获取到的UC云盘Cookies",
"value": "",
"dialogTitle": "Cookies",
"dialogMessage": "",
}
},
{
"key": "url",
"listPreference": {
"title": "Website Url",
"summary": "",
"valueIndex": 0,
"entries": [
"https://tv.yydsys.top",
"https://tv.yydsys.cc",
"https://tv.214521.xyz"
],
"entryValues": [
"https://tv.yydsys.top",
"https://tv.yydsys.cc",
"https://tv.214521.xyz"
],
}
}
];
}
}