add novel support

This commit is contained in:
Schnitzel5
2024-11-25 22:55:23 +01:00
parent d9a66b7651
commit 981833cac8
28 changed files with 5836 additions and 164 deletions

View File

@@ -0,0 +1,202 @@
const mangayomiSources = [{
"name": "NetflixMirror",
"lang": "all",
"baseUrl": "https://iosmirror.cc",
"apiUrl": "",
"iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.netflixmirror.png",
"typeSource": "single",
"isManga": false,
"version": "0.0.45",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/all/netflixmirror.js"
}];
class DefaultExtension extends MProvider {
async getCookie() {
const addhash = new Document((await new Client().get(`${this.source.baseUrl}/home`, { "cookie": "" })).body).selectFirst("body").attr("data-addhash");
await new Client().get(`https://userverify.netmirror.app/verify?dp1=${addhash}&a=y`);
let body;
let res;
do {
res = await new Client().post(`${this.source.baseUrl}/verify2.php`, { "cookie": "" }, { "verify": addhash });
body = res.body;
} while (!body.includes('"statusup":"All Done"'));
return res.headers["set-cookie"];
}
async request(url, cookie) {
cookie = cookie ?? await this.getCookie();
return (await new Client().get(this.source.baseUrl + url, { "cookie": `ott=nf; hd=on; ${cookie}` })).body;
}
async getPopular(page) {
return await this.getPages(await this.request("/home"), ".tray-container, #top10")
}
async getLatestUpdates(page) {
return await this.getPages(await this.request("/home"), ".inner-mob-tray-container")
}
async getPages(body, selector) {
const elements = new Document(body).select(selector);
const cookie = await this.getCookie();
const list = [];
for (const element of elements) {
const linkElement = element.selectFirst("article, .top10-post");
const id = linkElement.selectFirst("a").attr("data-post");
if (id.length > 0) {
const imageUrl = linkElement.selectFirst(".card-img-container img, .top10-img img").attr("data-src");
list.push({ name: JSON.parse(await this.request(`/post.php?id=${id}`, cookie)).title, imageUrl, link: id });
}
}
return {
list: list,
hasNextPage: false
}
}
async search(query, page, filters) {
const data = JSON.parse(await this.request(`/search.php?s=${query}`));
const list = [];
data.searchResult.map(async (res) => {
const id = res.id;
list.push({ name: res.t, imageUrl: `https://img.nfmirrorcdn.top/poster/v/${id}.jpg`, link: id });
})
return {
list: list,
hasNextPage: false
}
}
async getDetail(url) {
const cookie = await this.getCookie();
const data = JSON.parse(await this.request(`/post.php?id=${url}`, cookie));
const name = data.title;
const genre = [data.ua, ...(data.genre || '').split(',').map(g => g.trim())];
const description = data.desc;
let episodes = [];
if (data.episodes[0] === null) {
episodes.push({ name, url: JSON.stringify({ id: url, name }) });
} else {
episodes = data.episodes.map(ep => ({
name: `${ep.s.replace('S', 'Season ')} ${ep.ep.replace('E', 'Episode ')} : ${ep.t}`,
url: JSON.stringify({ id: ep.id, name })
}));
}
if (data.nextPageShow === 1) {
const eps = await this.getEpisodes(name, url, data.nextPageSeason, 2, cookie);
episodes.push(...eps);
}
episodes.reverse();
if (data.season && data.season.length > 1) {
let newEpisodes = [];
const seasonsToProcess = data.season.slice(0, -1);
await Promise.all(seasonsToProcess.map(async (season) => {
const eps = await this.getEpisodes(name, url, season.id, 1, cookie);
newEpisodes.push(...eps);
}));
newEpisodes.reverse();
episodes.push(...newEpisodes);
}
return {
description, status: 1, genre, episodes
};
}
async getEpisodes(name, eid, sid, page, cookie) {
const episodes = [];
let pg = page;
while (true) {
try {
const data = JSON.parse(await this.request(`/episodes.php?s=${sid}&series=${eid}&page=${pg}`, cookie));
data.episodes?.forEach(ep => {
episodes.push({
name: `${ep.s.replace('S', 'Season ')} ${ep.ep.replace('E', 'Episode ')} : ${ep.t}`,
url: JSON.stringify({ id: ep.id, name })
});
});
if (data.nextPageShow === 0) break;
pg++;
} catch (_) {
break;
}
}
return episodes;
}
async getVideoList(url) {
const baseUrl = this.source.baseUrl;
const urlData = JSON.parse(url);
const data = JSON.parse(await this.request(`/playlist.php?id=${urlData.id}&t=${urlData.name}`));
const videoList = [];
for (const playlist of data) {
for (const source of playlist.sources) {
try {
const subtitles = [];
playlist.tracks.filter(track => track.kind === 'captions').forEach(track => {
subtitles.push({
label: track.label,
file: track.file
});
});
const link = baseUrl + source.file;
const headers =
{
'Host': link.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/)[1],
'Origin': baseUrl,
'Referer': `${baseUrl}/`
};
const resp = await new Client().get(link, headers);
if (resp.statusCode === 200) {
const masterPlaylist = resp.body;
const audios = [];
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 });
}
});
if (!masterPlaylist.includes('#EXT-X-STREAM-INF:')) {
if (audios.length === 0) {
videoList.push({ url: link, quality: source.label, originalUrl: link, subtitles, headers });
} else {
videoList.push({ url: link, quality: source.label, originalUrl: link, subtitles, audios, headers });
}
} else {
masterPlaylist.substringAfter('#EXT-X-STREAM-INF:').split('#EXT-X-STREAM-INF:').forEach(it => {
const quality = `${it.substringAfter('RESOLUTION=').substringAfter('x').substringBefore(',')}p (${source.label})`;
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': baseUrl,
'Referer': `${baseUrl}/`
};
if (audios.length === 0) {
videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, subtitles, headers });
} else {
videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, subtitles, audios, headers });
}
});
}
}
} catch (_) {
}
}
}
return videoList;
}
}

View File

@@ -7,16 +7,20 @@ const mangayomiSources = [{
"typeSource": "single",
"itemType": "anime",
"isNsfw": false,
"version": "0.0.15",
"version": "0.0.28",
"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 new Client().get(`${baseUrl}/beliebte-animes`);
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) {
@@ -33,7 +37,7 @@ class DefaultExtension extends MProvider {
}
async getLatestUpdates(page) {
const baseUrl = this.source.baseUrl;
const res = await new Client().get(`${baseUrl}/neu`);
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) {
@@ -50,13 +54,13 @@ class DefaultExtension extends MProvider {
}
async search(query, page, filters) {
const baseUrl = this.source.baseUrl;
const res = await new Client().get(`${baseUrl}/animes`);
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 new Client().get(baseUrl + link)).body).selectFirst("div.seriesCoverBox img").attr("data-src");
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 });
}
@@ -67,7 +71,7 @@ class DefaultExtension extends MProvider {
}
async getDetail(url) {
const baseUrl = this.source.baseUrl;
const res = await new Client().get(baseUrl + url);
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");
@@ -81,22 +85,23 @@ class DefaultExtension extends MProvider {
author = produzent[0].select("li").map(e => e.text).join(", ");
}
const seasonsElements = document.select("#stream > ul:nth-child(1) > li > a");
let episodes = [];
const promises = [];
const episodes = [];
for (const element of seasonsElements) {
const eps = await this.parseEpisodesFromSeries(element);
for (const ep of eps) {
episodes.push(ep);
promises.push(this.parseEpisodesFromSeries(element));
}
for (const p of (await Promise.allSettled(promises))) {
if (p.status == 'fulfilled') {
episodes.push(...p.value);
}
}
episodes.reverse();
return {
name, imageUrl, description, author, status: 5, genre, episodes
};
return { name, imageUrl, description, author, status: 5, genre, episodes };
}
async parseEpisodesFromSeries(element) {
const seasonId = element.getHref;
const res = await new Client().get(this.source.baseUrl + seasonId);
const res = await this.client.get(this.source.baseUrl + seasonId);
const episodeElements = new Document(res.body).select("table.seasonEpisodesList tbody tr");
const list = [];
for (const episodeElement of episodeElements) {
@@ -105,94 +110,71 @@ class DefaultExtension extends MProvider {
return list;
}
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 = episodeSpan.text.replace(/'/g, "'");
let name = "";
let url = "";
if (element.selectFirst("td.seasonEpisodeTitle a").attr("href").includes("/film")) {
const num = element.attr("data-episode-season-id");
name = `Film ${num}` + " : " + element.selectFirst("td.seasonEpisodeTitle a span").text;
url = element.selectFirst("td.seasonEpisodeTitle a").attr("href");
if (url.includes("/film")) {
name = `Film ${episodeSeasonId} : ${episode}`;
} else {
const season =
element.selectFirst("td.seasonEpisodeTitle a").attr("href").substringAfter("staffel-").substringBefore("/episode");;
const num = element.attr("data-episode-season-id");
name = `Staffel ${season} Folge ${num}` + " : " + element.selectFirst("td.seasonEpisodeTitle a span").text;
url = element.selectFirst("td.seasonEpisodeTitle a").attr("href");
const seasonMatch = url.match(/staffel-(\d+)\/episode/);
name = `Staffel ${seasonMatch[1]} Folge ${episodeSeasonId} : ${episode}`;
}
if (name.length > 0 && url.length > 0) {
return { name, url }
}
return {}
return name && url ? { name, url } : {};
}
async getVideoList(url) {
const baseUrl = this.source.baseUrl;
const res = await new Client().get(baseUrl + url);
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);
const redirectlink = document.select("ul.row li");
const preference = new SharedPreferences();
const hosterSelection = preference.get("hoster_selection");
let promises = [];
const videos = [];
for (const element of redirectlink) {
try {
const redirectsElements = document.select("ul.row li");
const hosterSelection = new SharedPreferences().get("hoster_selection_new");
const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false });
for (const element of redirectsElements) {
const host = element.selectFirst("a h4").text;
if (hosterSelection.includes(host)) {
const langkey = element.attr("data-lang-key");
let language = "";
if (langkey.includes("3")) {
language = "Deutscher Sub";
} else if (langkey.includes("1")) {
language = "Deutscher Dub";
} else if (langkey.includes("2")) {
language = "Englischer Sub";
}
const redirectgs = baseUrl + element.selectFirst("a.watchEpisode").attr("href");
const hoster = element.selectFirst("a h4").text;
if (hoster == "Streamtape" && hosterSelection.includes("Streamtape")) {
const body = (await new Client().get(redirectgs)).body;
const quality = `Streamtape ${language}`;
const vids = await streamTapeExtractor(body.match(/https:\/\/streamtape\.com\/e\/[a-zA-Z0-9]+/g)[0], quality);
for (const vid of vids) {
videos.push(vid);
}
} else if (hoster == "VOE" && hosterSelection.includes("VOE")) {
const body = (await new Client().get(redirectgs)).body;
const quality = `VOE ${language}`;
const vids = await voeExtractor(body.match(/https:\/\/voe\.sx\/e\/[a-zA-Z0-9]+/g)[0], quality);
for (const vid of vids) {
videos.push(vid);
}
} else if (hoster == "Vidoza" && hosterSelection.includes("Vidoza")) {
const body = (await new Client().get(redirectgs)).body;
const quality = `Vidoza ${language}`;
const match = body.match(/https:\/\/[^\s]*\.vidoza\.net\/[^\s]*\.mp4/g);
if (match.length > 0) {
videos.push({ url: match[0], originalUrl: match[0], quality });
}
}
} catch (_) {
const lang = (langkey == 1 || langkey == 3) ? 'Deutscher' : 'Englischer';
const type = (langkey == 1) ? 'Dub' : 'Sub';
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);
})(redirect, lang, type, host));
}
}
for (const p of (await Promise.allSettled(promises))) {
if (p.status == 'fulfilled') {
videos.push.apply(videos, p.value);
}
}
return this.sortVideos(videos);
}
sortVideos(videos) {
const preference = new SharedPreferences();
const hoster = preference.get("preferred_hoster");
const subPreference = preference.get("preferred_lang");
const hoster = RegExp(preference.get("preferred_hoster_new"));
const lang = RegExp(preference.get("preferred_lang"));
videos.sort((a, b) => {
let qualityMatchA = 0;
if (a.quality.includes(hoster) &&
a.quality.includes(subPreference)) {
qualityMatchA = 1;
}
let qualityMatchB = 0;
if (b.quality.includes(hoster) &&
b.quality.includes(subPreference)) {
qualityMatchB = 1;
}
let qualityMatchA = hoster.test(a.quality) * lang.test(a.quality);
let qualityMatchB = hoster.test(b.quality) * lang.test(b.quality);
return qualityMatchB - qualityMatchA;
});
return videos;
}
getSourcePreferences() {
const hosterOptions = ["Streamtape", "VOE", "Vidoza", "Doodstream"];
const languageOptions = ["Deutscher Sub", "Deutscher Dub", "Englischer Sub"];
return [
{
"key": "preferred_lang",
@@ -200,58 +182,92 @@ class DefaultExtension extends MProvider {
"title": "Bevorzugte Sprache",
"summary": "",
"valueIndex": 0,
"entries": [
"Deutscher Sub",
"Deutscher Dub",
"Englischer Sub"
],
"entryValues": [
"Deutscher Sub",
"Deutscher Dub",
"Englischer Sub"
]
"entries": languageOptions,
"entryValues": languageOptions
}
},
{
"key": "preferred_hoster",
"key": "preferred_hoster_new",
"listPreference": {
"title": "Standard-Hoster",
"summary": "",
"valueIndex": 0,
"entries": [
"Streamtape",
"VOE",
"Vidoza"
],
"entryValues": [
"Streamtape",
"VOE",
"Vidoza"
]
"entries": hosterOptions,
"entryValues": hosterOptions
}
},
{
"key": "hoster_selection",
"key": "hoster_selection_new",
"multiSelectListPreference": {
"title": "Hoster auswählen",
"summary": "",
"entries": [
"Streamtape",
"VOE",
"Vidoza"
],
"entryValues": [
"Streamtape",
"VOE",
"Vidoza"
],
"values": [
"Streamtape",
"VOE",
"Vidoza"
]
"entries": hosterOptions,
"entryValues": hosterOptions,
"values": hosterOptions
}
}
];
}
}
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: '' }];
}
_streamTapeExtractor = streamTapeExtractor;
streamTapeExtractor = async (url) => {
return await _streamTapeExtractor(url, '');
}
_voeExtractor = voeExtractor;
voeExtractor = async (url) => {
return (await _voeExtractor(url, '')).map(v => {
v.quality = v.quality.replace(/Voe: (\d+p?)/i, '$1');
return v;
});
}
async function extractAny(link, method, lang, type, host) {
const m = extractAny.methods[method];
return (!m) ? [] : (await m(link)).map(v => {
v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`;
return v;
});
};
extractAny.methods = {
'doodstream': doodExtractor,
'streamtape': streamTapeExtractor,
'vidoza': vidozaExtractor,
'voe': voeExtractor
};
function getRandomString(length) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
const charArray = new Array(length);
for (let i = 0; i < length; i++) {
charArray[i] = chars[Math.floor(Math.random() * chars.length)];
}
return charArray.join("");
}

View File

@@ -0,0 +1,273 @@
const mangayomiSources = [{
"name": "SerienStream",
"lang": "de",
"baseUrl": "https://s.to",
"apiUrl": "",
"iconUrl": "https://s.to/favicon.ico",
"typeSource": "single",
"isManga": false,
"isNsfw": false,
"version": "0.0.2",
"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
}
}
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);
const description = 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).join(", ");
}
const seasonsElements = document.select("#stream > ul:nth-child(1) > li > a");
const promises = [];
const episodes = [];
for (const element of seasonsElements) {
promises.push(this.parseEpisodesFromSeries(element));
}
for (const p of (await Promise.allSettled(promises))) {
if (p.status == 'fulfilled') {
episodes.push(...p.value);
}
}
episodes.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");
const list = [];
for (const episodeElement of episodeElements) {
list.push(this.episodeFromElement(episodeElement));
}
return list;
}
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 = episodeSpan.text.replace(/&#039;/g, "'");
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 redirectsElements = document.select("ul.row li");
const hosterSelection = new SharedPreferences().get("hoster_selection_new");
const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false });
for (const element of redirectsElements) {
const host = element.selectFirst("a h4").text;
if (hosterSelection.includes(host)) {
const langkey = element.attr("data-lang-key");
const lang = (langkey == 1 || langkey == 3) ? 'Deutscher' : 'Englischer';
const type = (langkey == 1) ? 'Dub' : 'Sub';
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);
})(redirect, lang, type, host));
}
}
for (const p of (await Promise.allSettled(promises))) {
if (p.status == 'fulfilled') {
videos.push.apply(videos, p.value);
}
}
return this.sortVideos(videos);
}
sortVideos(videos) {
const preference = new SharedPreferences();
const hoster = RegExp(preference.get("preferred_hoster_new"));
const lang = RegExp(preference.get("preferred_lang"));
videos.sort((a, b) => {
let qualityMatchA = hoster.test(a.quality) * lang.test(a.quality);
let qualityMatchB = hoster.test(b.quality) * lang.test(b.quality);
return qualityMatchB - qualityMatchA;
});
return videos;
}
getSourcePreferences() {
const hosterOptions = ["Streamtape", "VOE", "Vidoza", "Doodstream"];
const languageOptions = ["Deutscher Sub", "Deutscher Dub", "Englischer Sub"];
return [
{
"key": "preferred_lang",
"listPreference": {
"title": "Bevorzugte Sprache",
"summary": "",
"valueIndex": 0,
"entries": languageOptions,
"entryValues": languageOptions
}
},
{
"key": "preferred_hoster_new",
"listPreference": {
"title": "Standard-Hoster",
"summary": "",
"valueIndex": 0,
"entries": hosterOptions,
"entryValues": hosterOptions
}
},
{
"key": "hoster_selection_new",
"multiSelectListPreference": {
"title": "Hoster auswählen",
"summary": "",
"entries": hosterOptions,
"entryValues": hosterOptions,
"values": hosterOptions
}
}
];
}
}
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: '' }];
}
_streamTapeExtractor = streamTapeExtractor;
streamTapeExtractor = async (url) => {
return await _streamTapeExtractor(url, '');
}
_voeExtractor = voeExtractor;
voeExtractor = async (url) => {
return (await _voeExtractor(url, '')).map(v => {
v.quality = v.quality.replace(/Voe: (\d+p?)/i, '$1');
return v;
});
}
async function extractAny(link, method, lang, type, host) {
const m = extractAny.methods[method];
return (!m) ? [] : (await m(link)).map(v => {
v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`;
return v;
});
};
extractAny.methods = {
'doodstream': doodExtractor,
'streamtape': streamTapeExtractor,
'vidoza': vidozaExtractor,
'voe': voeExtractor
};
function getRandomString(length) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
const charArray = new Array(length);
for (let i = 0; i < length; i++) {
charArray[i] = chars[Math.floor(Math.random() * chars.length)];
}
return charArray.join("");
}

View File

@@ -7,7 +7,7 @@ const mangayomiSources = [{
"typeSource": "single",
"itemType": "anime",
"isNsfw": false,
"version": "0.0.25",
"version": "0.0.35",
"dateFormat": "",
"dateFormatLocale": "",
"pkgPath": "anime/src/en/allanime.js"
@@ -162,7 +162,7 @@ class DefaultExtension extends MProvider {
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_selection');
const altHosterSelection = preferences.get('alt_hoster_selection1');
for (const video of videoJson.data.episode.sourceUrls) {
const videoUrl = this.decryptSource(video.sourceUrl);
let quality = "";
@@ -193,7 +193,7 @@ class DefaultExtension extends MProvider {
videos.push(vid);
}
} else if (videoUrl.includes("streamlare.com") && altHosterSelection.some(element => 'streamlare' === element)) {
const vids = await streamlareExtractor(videoUrl);
const vids = await streamlareExtractor(videoUrl, 'Streamlare ');
for (const vid of vids) {
videos.push(vid);
}
@@ -203,7 +203,7 @@ class DefaultExtension extends MProvider {
videos.push(vid);
}
} else if (videoUrl.includes("wish") && altHosterSelection.some(element => 'streamwish' === element)) {
const vids = await streamwishExtractor(videoUrl);
const vids = await streamWishExtractor(videoUrl, 'StreamWish ');
for (const vid of vids) {
videos.push(vid);
}
@@ -213,7 +213,7 @@ class DefaultExtension extends MProvider {
}
sortVideos(videos) {
const preferences = new SharedPreferences();
const hoster = preferences.get("preferred_hoster");
const hoster = preferences.get("preferred_hoster1");
const quality = preferences.get("preferred_quality");
videos.sort((a, b) => {
let qualityMatchA = 0;
@@ -290,7 +290,7 @@ class DefaultExtension extends MProvider {
}
},
{
"key": "preferred_hoster_",
"key": "preferred_hoster1",
"listPreference": {
"title": "Preferred Video Server",
"summary": "",
@@ -302,7 +302,7 @@ class DefaultExtension extends MProvider {
"filemoon",
"streamwish"
],
"entryValues_": [
"entryValues": [
"Ac", "Ak", "Kir", "Rab", "Luf-mp4",
"Si-Hls", "S-mp4", "Ac-Hls", "Uv-mp4", "Pn-Hls",
"vidstreaming", "okru", "mp4upload", "streamlare", "doodstream",
@@ -312,7 +312,7 @@ class DefaultExtension extends MProvider {
}
},
{
"key": "alt_hoster_selection_",
"key": "alt_hoster_selection1",
"multiSelectListPreference": {
"title": "Enable/Disable Alternative Hosts",
"summary": "",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,717 @@
const mangayomiSources = [{
"name": "TioAnime",
"lang": "es",
"baseUrl": "https://tioanime.com",
"apiUrl": "",
"iconUrl": "https://tioanime.com/assets/img/tio_fb.jpg",
"typeSource": "single",
"isManga": false,
"version": "0.1.0",
"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 hasNextPage = doc.selectFirst("li.page-item.active + li").text == "»";
return { "list": list, "hasNextPage": hasNextPage };
}
statusFromString(status) {
return {
"En emision": 0,
"Finalizado": 1
}[status] ?? 5;
}
async getPopular(page) {
return this.parseAnimeList(`${this.source.baseUrl}/directorio?p=${page}`);
}
async getLatestUpdates(page) {
return 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 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() {
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: [
'Español'
],
entryValues: [
'Español'
]
}
},
{
key: 'type',
listPreference: {
title: 'Preferred Type',
summary: 'If available, this type will be chosen by default. Priority = 1 (lower is better)',
valueIndex: 0,
entries: [
'Sub'
],
entryValues: [
'Sub'
]
}
},
{
key: 'res',
listPreference: {
title: 'Preferred Resolution',
summary: 'If available, this resolution will be chosen by default. Priority = 2 (lower is better)',
valueIndex: 0,
entries: [
'1080p',
'720p',
'480p'
],
entryValues: [
'1080p',
'720p',
'480p'
]
}
},
{
key: 'host',
listPreference: {
title: 'Preferred Hoster',
summary: 'If available, this hoster will be chosen by default. Priority = 3 (lower is better)',
valueIndex: 0,
entries: [
'Okru',
'VidGuard',
'Voe',
'YourUpload'
],
entryValues: [
'Okru',
'VidGuard',
'Voe',
'YourUpload'
]
}
}
];
}
}
/***************************************************************************************************
*
* mangayomi-js-helpers v1.0
*
* # Video Extractors
* - vidGuardExtractor
* - doodExtractor
* - vidozaExtractor
* - okruExtractor
* - amazonExtractor
* - vidHideExtractor
* - filemoonExtractor
* - mixdropExtractor
* - burstcloudExtractor (not working, see description)
*
* # Video Extractor Format Wrappers
* - streamWishExtractor
* - voeExtractor
* - mp4UploadExtractor
* - yourUploadExtractor
* - streamTapeExtractor
* - sendVidExtractor
*
* # Video Extractor helpers
* - extractAny
*
* # Playlist Extractors
* - m3u8Extractor
* - jwplayerExtractor
*
* # Extension
* - sortVideos()
*
* # Encoding/Decoding
* - Uint8Array.fromBase64()
* - Uint8Array.prototype.toBase64()
* - Uint8Array.prototype.decode()
* - String.prototype.encode()
* - String.prototype.decode()
*
* # Random string
* - getRandomString()
*
* # URL
* - absUrl()
*
***************************************************************************************************/
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) {
let res = await new Client().get(url);
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'
});
}
return await jwplayerExtractor(res.body);
}
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}];
}
/** 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: ''
}];
}
_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}];
}
async function extractAny(url, method, lang, type, host) {
const m = extractAny.methods[method];
return (!m) ? [] : (await m(url)).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,
'mixdrop': mixdropExtractor,
'mp4upload': mp4UploadExtractor,
'okru': okruExtractor,
'sendvid': sendVidExtractor,
'streamtape': streamTapeExtractor,
'streamwish': vidHideExtractor,
'vidguard': vidGuardExtractor,
'vidhide': vidHideExtractor,
'vidoza': vidozaExtractor,
'voe': voeExtractor,
'yourupload': yourUploadExtractor
};
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;
// 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;
});
}
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.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.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;
}
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);
}
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;
}
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

@@ -0,0 +1,218 @@
const mangayomiSources = [{
"name": "玩偶哥哥",
"lang": "zh",
"baseUrl": "https://www.wogg.net",
"apiUrl": "",
"iconUrl": "https://imgsrc.baidu.com/forum/pic/item/4b90f603738da977d5da660af651f8198618e31f.jpg",
"typeSource": "single",
"isManga": false,
"isNsfw": false,
"version": "0.0.2",
"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": [
"wogg.net",
"wogg.xxooo.cf",
"wogg.888484.xyz",
"wogg.bf",
"wogg.333232.xyz"
],
"entryValues": [
"https://www.wogg.net",
"https://wogg.xxooo.cf",
"https://wogg.888484.xyz",
"https://www.wogg.bf",
"https://wogg.333232.xyz"
],
}
}
];
}
}

View File

@@ -0,0 +1,211 @@
const mangayomiSources = [{
"name": "多多影音",
"lang": "zh",
"baseUrl": "https://tv.yydsys.top",
"apiUrl": "",
"iconUrl": "https://tv.yydsys.top/template/DYXS2/static/picture/logo.png",
"typeSource": "single",
"isManga": false,
"isNsfw": false,
"version": "0.0.2",
"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": [
"tv.yydsys.top",
"tv.yydsys.cc",
],
"entryValues": [
"https://tv.yydsys.top",
"https://tv.yydsys.cc",
],
}
}
];
}
}