removal
This commit is contained in:
@@ -0,0 +1,697 @@
|
||||
// AniDub module for Sora (AsyncJS)
|
||||
// Author: emp0ry
|
||||
// Version: 1.0.0
|
||||
|
||||
const BASE_URL = "https://anidub.org";
|
||||
const SEARCH_URL = BASE_URL + "/index.php?do=search&subaction=search";
|
||||
|
||||
const PLAYLIST_API = "https://plapi.cdnvideohub.com/api/v1/player/sv/playlist";
|
||||
const VIDEO_API = "https://plapi.cdnvideohub.com/api/v1/player/sv/video/";
|
||||
|
||||
const DEFAULT_PUB = "19";
|
||||
const DEFAULT_AGGR = "mali";
|
||||
const DEFAULT_SUBTITLE = "https://none.com";
|
||||
|
||||
function _ua() {
|
||||
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||
}
|
||||
|
||||
function _safeJsonParse(value, fallback) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (_) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function _htmlDecode(value) {
|
||||
const s = String(value || "");
|
||||
if (!s) return "";
|
||||
|
||||
const named = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
""": '"',
|
||||
"'": "'",
|
||||
"'": "'",
|
||||
" ": " ",
|
||||
"«": "«",
|
||||
"»": "»"
|
||||
};
|
||||
|
||||
const withNamed = s.replace(/&(amp|lt|gt|quot|#39|apos|nbsp|laquo|raquo);/g, m => named[m] || m);
|
||||
|
||||
const withDec = withNamed.replace(/&#(\d+);/g, (_, d) => {
|
||||
const code = parseInt(d, 10);
|
||||
return Number.isFinite(code) ? String.fromCharCode(code) : "";
|
||||
});
|
||||
|
||||
return withDec.replace(/&#x([0-9a-f]+);/gi, (_, h) => {
|
||||
const code = parseInt(h, 16);
|
||||
return Number.isFinite(code) ? String.fromCharCode(code) : "";
|
||||
});
|
||||
}
|
||||
|
||||
function _stripTags(value) {
|
||||
return _htmlDecode(String(value || "")
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/<\/p>/gi, "\n")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim());
|
||||
}
|
||||
|
||||
function _attr(tag, name) {
|
||||
const block = String(tag || "");
|
||||
if (!block) return "";
|
||||
|
||||
const quoted = new RegExp(name + "\\s*=\\s*(['\"])(.*?)\\1", "i").exec(block);
|
||||
if (quoted && quoted[2]) return _htmlDecode(quoted[2]);
|
||||
|
||||
const plain = new RegExp(name + "\\s*=\\s*([^\\s>]+)", "i").exec(block);
|
||||
return plain && plain[1] ? _htmlDecode(plain[1]) : "";
|
||||
}
|
||||
|
||||
function _extractMeta(html, attrName, attrValue) {
|
||||
const src = String(html || "");
|
||||
|
||||
const re = new RegExp(
|
||||
"<meta[^>]*" + attrName + "=['\"]" + attrValue + "['\"][^>]*content=['\"]([^'\"]+)['\"][^>]*>",
|
||||
"i"
|
||||
);
|
||||
|
||||
const m = src.match(re);
|
||||
return m && m[1] ? _htmlDecode(m[1]).trim() : "";
|
||||
}
|
||||
|
||||
function _absUrl(url, base) {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
if (raw.startsWith("http://") || raw.startsWith("https://")) return raw;
|
||||
if (raw.startsWith("//")) return "https:" + raw;
|
||||
|
||||
const root = String(base || BASE_URL).replace(/\/+$/, "");
|
||||
return root + "/" + raw.replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
function _cleanUrl(url) {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return null;
|
||||
if (raw.startsWith("//")) return "https:" + raw;
|
||||
return raw;
|
||||
}
|
||||
|
||||
function _scoreTitle(title, keyword) {
|
||||
const t = String(title || "").toLowerCase().trim();
|
||||
const k = String(keyword || "").toLowerCase().trim();
|
||||
|
||||
if (!t || !k) return 99;
|
||||
if (t === k) return 0;
|
||||
if (t.startsWith(k)) return 1;
|
||||
if (t.includes(k)) return 2;
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
function _voiceRank(name) {
|
||||
const s = String(name || "").toLowerCase();
|
||||
|
||||
const order = [
|
||||
"anidub online",
|
||||
"anidub",
|
||||
"aniliberty",
|
||||
"anilibria",
|
||||
"shiza",
|
||||
"onwave",
|
||||
"anistar",
|
||||
"animevost",
|
||||
"студийная банда",
|
||||
"studio band",
|
||||
"dream cast",
|
||||
"dreamcast",
|
||||
"jam club",
|
||||
"jam",
|
||||
"субтит",
|
||||
"sub"
|
||||
];
|
||||
|
||||
for (let i = 0; i < order.length; i++) {
|
||||
if (s.includes(order[i])) return i;
|
||||
}
|
||||
|
||||
return 999;
|
||||
}
|
||||
|
||||
function _packAnime(payload) {
|
||||
return "anidub-release:" + encodeURIComponent(JSON.stringify(payload || {}));
|
||||
}
|
||||
|
||||
function _unpackAnime(href) {
|
||||
const raw = String(href || "");
|
||||
if (!raw.startsWith("anidub-release:")) return null;
|
||||
|
||||
return _safeJsonParse(decodeURIComponent(raw.slice("anidub-release:".length)), null);
|
||||
}
|
||||
|
||||
function _packEpisode(payload) {
|
||||
return "anidub:" + encodeURIComponent(JSON.stringify(payload || {}));
|
||||
}
|
||||
|
||||
function _unpackEpisode(href) {
|
||||
const raw = String(href || "");
|
||||
if (!raw.startsWith("anidub:")) return null;
|
||||
|
||||
return _safeJsonParse(decodeURIComponent(raw.slice("anidub:".length)), null);
|
||||
}
|
||||
|
||||
function _searchHeaders() {
|
||||
return {
|
||||
"User-Agent": _ua(),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"Origin": BASE_URL,
|
||||
"Referer": BASE_URL + "/"
|
||||
};
|
||||
}
|
||||
|
||||
function _htmlHeaders(referer) {
|
||||
return {
|
||||
"User-Agent": _ua(),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
|
||||
"Referer": referer || BASE_URL + "/",
|
||||
"Origin": BASE_URL
|
||||
};
|
||||
}
|
||||
|
||||
function _jsonHeaders(referer) {
|
||||
return {
|
||||
"User-Agent": _ua(),
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
|
||||
"Referer": referer || BASE_URL + "/"
|
||||
};
|
||||
}
|
||||
|
||||
function _streamHeaders() {
|
||||
return {
|
||||
"User-Agent": _ua(),
|
||||
"Referer": BASE_URL + "/"
|
||||
};
|
||||
}
|
||||
|
||||
async function _postSearch(query) {
|
||||
const q = encodeURIComponent(String(query || ""));
|
||||
const withParams = `${SEARCH_URL}&story=${q}`;
|
||||
|
||||
try {
|
||||
const r = await fetchv2(withParams, _searchHeaders(), "POST", "");
|
||||
const txt = await r.text();
|
||||
|
||||
if (txt && txt.includes("all__item")) return txt;
|
||||
} catch (_) {}
|
||||
|
||||
const body = "do=search&subaction=search&story=" + q;
|
||||
const fallback = await fetchv2(BASE_URL + "/index.php?do=search", _searchHeaders(), "POST", body);
|
||||
return fallback.text();
|
||||
}
|
||||
|
||||
function _parseSearchResults(html) {
|
||||
const src = String(html || "");
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
|
||||
const blocks = src.match(/<a\b[^>]*class=["'][^"']*all__item[^"']*["'][^>]*>[\s\S]*?<\/a>/gi) || [];
|
||||
|
||||
for (const block of blocks) {
|
||||
const openTag = (block.match(/<a\b[^>]*>/i) || [""])[0];
|
||||
const imgTag = (block.match(/<img\b[^>]*>/i) || [""])[0];
|
||||
|
||||
const titleMatch = block.match(
|
||||
/<span\b[^>]*class=["'][^"']*all__item-title[^"']*["'][^>]*>([\s\S]*?)<\/span>/i
|
||||
);
|
||||
|
||||
const href = _absUrl(_attr(openTag, "href"), BASE_URL);
|
||||
const title = _stripTags((titleMatch && titleMatch[1]) || _attr(openTag, "title") || "Unknown title");
|
||||
const image = _absUrl(_attr(imgTag, "src") || _attr(imgTag, "data-src"), BASE_URL);
|
||||
|
||||
if (!href || !title || seen.has(href)) continue;
|
||||
seen.add(href);
|
||||
|
||||
out.push({
|
||||
title,
|
||||
image,
|
||||
href: _packAnime({
|
||||
animeUrl: href,
|
||||
title,
|
||||
image
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function _extractAnimeUrl(input) {
|
||||
const packed = _unpackAnime(input);
|
||||
if (packed?.animeUrl) return _absUrl(packed.animeUrl, BASE_URL);
|
||||
|
||||
const ep = _unpackEpisode(input);
|
||||
if (ep?.animeUrl) return _absUrl(ep.animeUrl, BASE_URL);
|
||||
|
||||
return _absUrl(input, BASE_URL);
|
||||
}
|
||||
|
||||
function _extractTitleId(html) {
|
||||
const src = String(html || "");
|
||||
|
||||
const m1 = src.match(/data-title-id=["'](\d+)["']/i);
|
||||
if (m1 && m1[1]) return m1[1];
|
||||
|
||||
const m2 = src.match(/[?&]titleId=(\d+)/i);
|
||||
if (m2 && m2[1]) return m2[1];
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function _extractPublisherId(html) {
|
||||
const src = String(html || "");
|
||||
|
||||
const m = src.match(/data-publisher-id=["'](\d+)["']/i);
|
||||
return m && m[1] ? m[1] : DEFAULT_PUB;
|
||||
}
|
||||
|
||||
function _extractAggregator(html) {
|
||||
const src = String(html || "");
|
||||
|
||||
const m1 = src.match(/data-aggregator=["']([^"']+)["']/i);
|
||||
if (m1 && m1[1]) return m1[1];
|
||||
|
||||
const m2 = src.match(/[?&]aggr=([^"'&<>\s]+)/i);
|
||||
return m2 && m2[1] ? decodeURIComponent(m2[1]) : DEFAULT_AGGR;
|
||||
}
|
||||
|
||||
async function _getAnimePage(animeUrl) {
|
||||
const r = await fetchv2(animeUrl, _htmlHeaders(BASE_URL + "/"));
|
||||
return r.text();
|
||||
}
|
||||
|
||||
async function _getPlaylist(titleId, pub, aggr, referer) {
|
||||
const url =
|
||||
`${PLAYLIST_API}?pub=${encodeURIComponent(pub || DEFAULT_PUB)}` +
|
||||
`&aggr=${encodeURIComponent(aggr || DEFAULT_AGGR)}` +
|
||||
`&id=${encodeURIComponent(titleId)}`;
|
||||
|
||||
const r = await fetchv2(url, _jsonHeaders(referer || BASE_URL + "/"));
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async function _getVideo(vkId, referer) {
|
||||
const url = VIDEO_API + encodeURIComponent(String(vkId || ""));
|
||||
const r = await fetchv2(url, _jsonHeaders(referer || BASE_URL + "/"));
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function _detailsDescription(html) {
|
||||
const title =
|
||||
_stripTags((html.match(/<h1\b[^>]*class=["'][^"']*movie__title[^"']*["'][^>]*>([\s\S]*?)<\/h1>/i) || [null, ""])[1]) ||
|
||||
_extractMeta(html, "property", "og:title") ||
|
||||
_extractMeta(html, "name", "title") ||
|
||||
"AniDub";
|
||||
|
||||
const subtitle = _stripTags(
|
||||
(html.match(/<span\b[^>]*class=["'][^"']*movie__subtitle[^"']*["'][^>]*>([\s\S]*?)<\/span>/i) || [null, ""])[1]
|
||||
);
|
||||
|
||||
const desc =
|
||||
_stripTags((html.match(/<div\b[^>]*class=["'][^"']*movie__description[^"']*["'][^>]*>([\s\S]*?)<\/div>/i) || [null, ""])[1]) ||
|
||||
_extractMeta(html, "property", "og:description") ||
|
||||
_extractMeta(html, "name", "description") ||
|
||||
"No description available.";
|
||||
|
||||
return [title, subtitle, desc].filter(Boolean).join("\n\n");
|
||||
}
|
||||
|
||||
function _detailsAliases(html, titleId, pub, aggr) {
|
||||
const parts = [];
|
||||
|
||||
const addField = (display, pattern) => {
|
||||
const re = new RegExp(
|
||||
"<div[^>]*class=[\"'][^\"']*movie__type[^\"']*[\"'][^>]*>\\s*<span>" +
|
||||
pattern +
|
||||
":<\\/span>\\s*([\\s\\S]*?)<\\/div>",
|
||||
"i"
|
||||
);
|
||||
|
||||
const m = html.match(re);
|
||||
const val = m && m[1] ? _stripTags(m[1]) : "";
|
||||
|
||||
if (val) parts.push(`${display}: ${val}`);
|
||||
};
|
||||
|
||||
addField("Жанр", "Жанр");
|
||||
addField("Тип", "Тип");
|
||||
addField("Статус", "Статус");
|
||||
addField("Год выхода", "Год выхода");
|
||||
addField("Длительность", "Длительность \\(мин\\.\\)");
|
||||
addField("Кол-во эпизодов", "Кол-во эпизодов");
|
||||
addField("Другие названия", "Другие названия");
|
||||
|
||||
if (titleId) parts.push(`titleId: ${titleId}`);
|
||||
if (pub) parts.push(`pub: ${pub}`);
|
||||
if (aggr) parts.push(`aggr: ${aggr}`);
|
||||
|
||||
return parts.join(" | ");
|
||||
}
|
||||
|
||||
function _airdate(html) {
|
||||
const premiere = html.match(
|
||||
/<div[^>]*class=["'][^"']*movie__type[^"']*["'][^>]*>\s*<span>Премьера:<\/span>\s*([\s\S]*?)<\/div>/i
|
||||
);
|
||||
|
||||
const added = html.match(
|
||||
/<div[^>]*class=["'][^"']*movie__added[^"']*["'][^>]*>[\s\S]*?<span>Добавлено<\/span>[\s\S]*?<span>([\s\S]*?)<\/span>/i
|
||||
);
|
||||
|
||||
return _stripTags((premiere && premiere[1]) || (added && added[1]) || "Unknown");
|
||||
}
|
||||
|
||||
function _episodeKey(season, episode) {
|
||||
return `${season || 1}:${episode || 0}`;
|
||||
}
|
||||
|
||||
function _episodeTitle(season, episode) {
|
||||
if (season && season > 1) return `S${season}E${episode}`;
|
||||
return `Episode ${episode}`;
|
||||
}
|
||||
|
||||
function _voiceName(item) {
|
||||
const studio = String(item?.voiceStudio || "").trim();
|
||||
const type = String(item?.voiceType || "").trim();
|
||||
|
||||
if (studio && type) return `${studio} · ${type}`;
|
||||
if (studio) return studio;
|
||||
if (type) return type;
|
||||
|
||||
return "Unknown voiceover";
|
||||
}
|
||||
|
||||
function _sourceUrl(sources, key) {
|
||||
return _cleanUrl(sources?.[key] || "");
|
||||
}
|
||||
|
||||
function _buildQualityUrls(videoJson) {
|
||||
const sources = videoJson?.sources || {};
|
||||
|
||||
const url2160 = _sourceUrl(sources, "mpeg4kUrl");
|
||||
const url1440 = _sourceUrl(sources, "mpeg2kUrl") || _sourceUrl(sources, "mpegQhdUrl");
|
||||
const url1080 = _sourceUrl(sources, "mpegFullHdUrl");
|
||||
const url720 = _sourceUrl(sources, "mpegHighUrl");
|
||||
const url480 = _sourceUrl(sources, "mpegMediumUrl");
|
||||
const url360 = _sourceUrl(sources, "mpegLowUrl");
|
||||
const url240 = _sourceUrl(sources, "mpegLowestUrl");
|
||||
const url144 = _sourceUrl(sources, "mpegTinyUrl");
|
||||
|
||||
return {
|
||||
url2160,
|
||||
url1440,
|
||||
url1080,
|
||||
url720,
|
||||
url480,
|
||||
url360,
|
||||
url240,
|
||||
url144
|
||||
};
|
||||
}
|
||||
|
||||
function _pickBestMp4(qualityUrls) {
|
||||
const ordered = [
|
||||
{ quality: 2160, url: qualityUrls.url2160 },
|
||||
{ quality: 1440, url: qualityUrls.url1440 },
|
||||
{ quality: 1080, url: qualityUrls.url1080 },
|
||||
{ quality: 720, url: qualityUrls.url720 },
|
||||
{ quality: 480, url: qualityUrls.url480 },
|
||||
{ quality: 360, url: qualityUrls.url360 },
|
||||
{ quality: 240, url: qualityUrls.url240 },
|
||||
{ quality: 144, url: qualityUrls.url144 }
|
||||
];
|
||||
|
||||
for (const item of ordered) {
|
||||
if (item.url) return item;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function _hasAnyQuality(qualityUrls) {
|
||||
return !!(
|
||||
qualityUrls.url2160 ||
|
||||
qualityUrls.url1440 ||
|
||||
qualityUrls.url1080 ||
|
||||
qualityUrls.url720 ||
|
||||
qualityUrls.url480 ||
|
||||
qualityUrls.url360 ||
|
||||
qualityUrls.url240 ||
|
||||
qualityUrls.url144
|
||||
);
|
||||
}
|
||||
|
||||
function _buildVoiceoverStream(voiceName, videoJson) {
|
||||
const qualityUrls = _buildQualityUrls(videoJson);
|
||||
|
||||
if (!_hasAnyQuality(qualityUrls)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const best = _pickBestMp4(qualityUrls);
|
||||
if (!best?.url) return null;
|
||||
|
||||
const headers = _streamHeaders();
|
||||
|
||||
return {
|
||||
title: voiceName || "Voiceover",
|
||||
streamUrl: best.url,
|
||||
|
||||
// Main Sora quality-picker fields.
|
||||
url1080: qualityUrls.url1080,
|
||||
url720: qualityUrls.url720,
|
||||
url480: qualityUrls.url480,
|
||||
|
||||
// Extra qualities. Harmless if Sora ignores them.
|
||||
url2160: qualityUrls.url2160,
|
||||
url1440: qualityUrls.url1440,
|
||||
url360: qualityUrls.url360,
|
||||
url240: qualityUrls.url240,
|
||||
url144: qualityUrls.url144,
|
||||
|
||||
headers
|
||||
};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Search
|
||||
// ------------------------------------------------------------
|
||||
async function searchResults(keyword) {
|
||||
try {
|
||||
const query = String(keyword || "").trim();
|
||||
if (!query) return JSON.stringify([]);
|
||||
|
||||
const html = await _postSearch(query);
|
||||
const results = _parseSearchResults(html);
|
||||
|
||||
for (const item of results) {
|
||||
item._score = _scoreTitle(item.title, query);
|
||||
}
|
||||
|
||||
results.sort((a, b) => a._score - b._score);
|
||||
|
||||
return JSON.stringify(results.map(({ _score, ...rest }) => rest));
|
||||
} catch (_) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Details
|
||||
// ------------------------------------------------------------
|
||||
async function extractDetails(href) {
|
||||
try {
|
||||
const animeUrl = _extractAnimeUrl(href);
|
||||
if (!animeUrl) return JSON.stringify([]);
|
||||
|
||||
const html = await _getAnimePage(animeUrl);
|
||||
|
||||
const titleId = _extractTitleId(html);
|
||||
const pub = _extractPublisherId(html);
|
||||
const aggr = _extractAggregator(html);
|
||||
|
||||
return JSON.stringify([{
|
||||
description: _detailsDescription(html),
|
||||
aliases: _detailsAliases(html, titleId, pub, aggr) || "AniDub",
|
||||
airdate: _airdate(html)
|
||||
}]);
|
||||
} catch (_) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Episodes
|
||||
// Unique episodes only.
|
||||
// Store all voiceovers in href payload.
|
||||
// ------------------------------------------------------------
|
||||
async function extractEpisodes(href) {
|
||||
try {
|
||||
const animeUrl = _extractAnimeUrl(href);
|
||||
if (!animeUrl) return JSON.stringify([]);
|
||||
|
||||
const html = await _getAnimePage(animeUrl);
|
||||
|
||||
const titleId = _extractTitleId(html);
|
||||
const pub = _extractPublisherId(html);
|
||||
const aggr = _extractAggregator(html);
|
||||
|
||||
if (!titleId) return JSON.stringify([]);
|
||||
|
||||
const playlist = await _getPlaylist(titleId, pub, aggr, animeUrl);
|
||||
const items = Array.isArray(playlist?.items) ? playlist.items : [];
|
||||
|
||||
const byEpisode = new Map();
|
||||
|
||||
for (const item of items) {
|
||||
const vkId = String(item?.vkId || "").trim();
|
||||
if (!vkId) continue;
|
||||
|
||||
const season = Number.isFinite(item?.season) ? item.season : 1;
|
||||
const episode = Number.isFinite(item?.episode) ? item.episode : 0;
|
||||
if (!episode) continue;
|
||||
|
||||
const key = _episodeKey(season, episode);
|
||||
|
||||
if (!byEpisode.has(key)) {
|
||||
byEpisode.set(key, {
|
||||
season,
|
||||
episode,
|
||||
options: []
|
||||
});
|
||||
}
|
||||
|
||||
const ep = byEpisode.get(key);
|
||||
|
||||
ep.options.push({
|
||||
vkId,
|
||||
cvhId: String(item?.cvhId || ""),
|
||||
voiceStudio: String(item?.voiceStudio || "").trim(),
|
||||
voiceType: String(item?.voiceType || "").trim(),
|
||||
voiceName: _voiceName(item),
|
||||
name: String(item?.name || "").trim(),
|
||||
season,
|
||||
episode
|
||||
});
|
||||
}
|
||||
|
||||
const out = Array.from(byEpisode.values())
|
||||
.sort((a, b) => {
|
||||
if (a.season !== b.season) return a.season - b.season;
|
||||
return a.episode - b.episode;
|
||||
})
|
||||
.map(ep => {
|
||||
ep.options.sort((a, b) => _voiceRank(a.voiceName) - _voiceRank(b.voiceName));
|
||||
|
||||
return {
|
||||
href: _packEpisode({
|
||||
animeUrl,
|
||||
titleId,
|
||||
pub,
|
||||
aggr,
|
||||
season: ep.season,
|
||||
episode: ep.episode,
|
||||
options: ep.options
|
||||
}),
|
||||
number: ep.episode,
|
||||
title: _episodeTitle(ep.season, ep.episode)
|
||||
};
|
||||
});
|
||||
|
||||
return JSON.stringify(out);
|
||||
} catch (_) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Stream
|
||||
// Correct voiceover + quality picker:
|
||||
// - one stream per voiceover
|
||||
// - each stream carries url1080/url720/url480/etc.
|
||||
// - HLS skipped.
|
||||
// ------------------------------------------------------------
|
||||
async function extractStreamUrl(href) {
|
||||
try {
|
||||
const payload = _unpackEpisode(href);
|
||||
const options = Array.isArray(payload?.options) ? payload.options : [];
|
||||
|
||||
if (!options.length) {
|
||||
return JSON.stringify({
|
||||
streams: [],
|
||||
subtitle: DEFAULT_SUBTITLE
|
||||
});
|
||||
}
|
||||
|
||||
options.sort((a, b) => _voiceRank(a.voiceName) - _voiceRank(b.voiceName));
|
||||
|
||||
const streams = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const opt of options) {
|
||||
const vkId = String(opt?.vkId || "").trim();
|
||||
if (!vkId || seen.has(vkId)) continue;
|
||||
seen.add(vkId);
|
||||
|
||||
try {
|
||||
const videoJson = await _getVideo(vkId, payload?.animeUrl || BASE_URL + "/");
|
||||
|
||||
const stream = _buildVoiceoverStream(
|
||||
String(opt?.voiceName || "Voiceover").trim(),
|
||||
videoJson
|
||||
);
|
||||
|
||||
if (stream) streams.push(stream);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
streams,
|
||||
subtitle: DEFAULT_SUBTITLE
|
||||
});
|
||||
} catch (_) {
|
||||
return JSON.stringify({
|
||||
streams: [],
|
||||
subtitle: DEFAULT_SUBTITLE
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _defaultExport() {
|
||||
return {
|
||||
searchResults,
|
||||
extractDetails,
|
||||
extractEpisodes,
|
||||
extractStreamUrl
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
globalThis.default = _defaultExport;
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
this.default = _defaultExport;
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
globalThis.module = globalThis.module || {};
|
||||
globalThis.module.exports = { default: _defaultExport };
|
||||
} catch (_) {}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"sourceName": "AniDub",
|
||||
"iconUrl": "https://anidub.org/templates/CinemaBlue/images/jutfun.png",
|
||||
"author": {
|
||||
"name": "emp0ry",
|
||||
"icon": "https://avatars.githubusercontent.com/u/64217088"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"language": "Russian",
|
||||
"streamType": "HLS",
|
||||
"quality": "1080p",
|
||||
"baseUrl": "https://anidub.org/",
|
||||
"searchBaseUrl": "https://anidub.org/index.php?do=search&subaction=search&story=%s",
|
||||
"scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/anidub/anidub.js",
|
||||
"asyncJS": true,
|
||||
"streamAsyncJS": true,
|
||||
"softsub": false,
|
||||
"type": "anime",
|
||||
"downloadSupport": false,
|
||||
"supportsMojuru": true,
|
||||
"supportsDartotsu": true,
|
||||
"supportsSora": true,
|
||||
"supportsLuna": true
|
||||
}
|
||||
Reference in New Issue
Block a user