Add modules by emp0ry
This commit is contained in:
@@ -0,0 +1,460 @@
|
||||
const API_BASE = "https://onwavedub.org/api/v1";
|
||||
const SITE_BASE = "https://onwavedub.org";
|
||||
const DEFAULT_SUBTITLE = "https://none.com";
|
||||
// Set to "voiceover-only" or "server-only" depending on preferred app UI grouping.
|
||||
const STREAM_PICKER_MODE = "voiceover-only";
|
||||
|
||||
function _streamTitle(kind, quality) {
|
||||
const q = Number.isFinite(quality) && quality > 0 ? `${quality}p` : "";
|
||||
|
||||
if (STREAM_PICKER_MODE === "server-only") {
|
||||
if (kind === "onwave") return "OnWave - Main";
|
||||
if (kind === "mirror") return "OnWave - Mirror";
|
||||
if (kind === "kodik") return q ? `OnWave - Kodik ${q}` : "OnWave - Kodik";
|
||||
}
|
||||
|
||||
if (kind === "onwave") return "OnWave";
|
||||
if (kind === "mirror") return "OnWave зеркало";
|
||||
if (kind === "kodik") return q ? `Kodik ${q}` : "Kodik";
|
||||
return "Stream";
|
||||
}
|
||||
|
||||
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 _absUrl(url, base) {
|
||||
if (!url) return "";
|
||||
const raw = String(url).trim();
|
||||
if (!raw) return "";
|
||||
if (raw.startsWith("http://") || raw.startsWith("https://")) return raw;
|
||||
if (raw.startsWith("//")) return "https:" + raw;
|
||||
|
||||
const origin = String(base || SITE_BASE).replace(/\/+$/, "");
|
||||
return origin + "/" + raw.replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
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 _packEpisode(payload) {
|
||||
return "onwave:" + encodeURIComponent(JSON.stringify(payload || {}));
|
||||
}
|
||||
|
||||
function _unpackEpisode(href) {
|
||||
const raw = String(href || "");
|
||||
if (!raw.startsWith("onwave:")) return null;
|
||||
return _safeJsonParse(decodeURIComponent(raw.slice("onwave:".length)), null);
|
||||
}
|
||||
|
||||
function _extractAnimeId(input) {
|
||||
const raw = String(input || "").trim();
|
||||
if (!raw) return "";
|
||||
|
||||
const packed = _unpackEpisode(raw);
|
||||
if (packed && packed.animeId != null) return String(packed.animeId);
|
||||
|
||||
if (/^\d+$/.test(raw)) return raw;
|
||||
|
||||
const m = raw.match(/\/anime\/(\d+)/i);
|
||||
if (m && m[1]) return m[1];
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
function _playerPriority(player) {
|
||||
return Number.isFinite(player?.priority) ? player.priority : 999;
|
||||
}
|
||||
|
||||
function _pickPlayer(players, predicate) {
|
||||
const list = (Array.isArray(players) ? players : []).filter(predicate);
|
||||
if (!list.length) return null;
|
||||
list.sort((a, b) => _playerPriority(a) - _playerPriority(b));
|
||||
return list[0];
|
||||
}
|
||||
|
||||
async function _apiGet(url) {
|
||||
return fetchv2(url, {
|
||||
"User-Agent": _ua(),
|
||||
"Accept": "application/json",
|
||||
"Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
|
||||
"Referer": SITE_BASE + "/",
|
||||
"Origin": SITE_BASE
|
||||
});
|
||||
}
|
||||
|
||||
function _buildOnWaveMp4(playerLink) {
|
||||
const abs = _absUrl(playerLink, "https://anisurf.site");
|
||||
if (!abs) return "";
|
||||
|
||||
if (/\/videos\/[^/]+\/streams\/source\.mp4/i.test(abs)) {
|
||||
return abs.split("?")[0];
|
||||
}
|
||||
|
||||
try {
|
||||
const u = new URL(abs);
|
||||
const id = u.searchParams.get("id");
|
||||
if (!id) return "";
|
||||
|
||||
const origin =
|
||||
u.hostname.toLowerCase().includes("anisurf.ru")
|
||||
? "https://anisurf.ru"
|
||||
: "https://anisurf.site";
|
||||
|
||||
return `${origin}/videos/${id}/streams/source.mp4`;
|
||||
} catch (_) {
|
||||
const m = abs.match(/[?&]id=([a-z0-9-]+)/i);
|
||||
if (!m || !m[1]) return "";
|
||||
|
||||
const isRu = /anisurf\.ru/i.test(abs);
|
||||
const origin = isRu ? "https://anisurf.ru" : "https://anisurf.site";
|
||||
return `${origin}/videos/${m[1]}/streams/source.mp4`;
|
||||
}
|
||||
}
|
||||
|
||||
function _appendBestKodikStream(streams, qualities) {
|
||||
let bestUrl = "";
|
||||
let bestQuality = 0;
|
||||
let url1080 = null;
|
||||
let url720 = null;
|
||||
let url480 = null;
|
||||
|
||||
for (const quality in qualities || {}) {
|
||||
const srcRaw = qualities?.[quality]?.src;
|
||||
const src = srcRaw ? (String(srcRaw).startsWith("//") ? "https:" + String(srcRaw) : String(srcRaw)) : "";
|
||||
if (!src) continue;
|
||||
|
||||
const numeric = parseInt(String(quality).replace(/[^\d]/g, ""), 10) || 0;
|
||||
if (numeric >= 1080 && !url1080) url1080 = src;
|
||||
if (numeric === 720 && !url720) url720 = src;
|
||||
if (numeric === 480 && !url480) url480 = src;
|
||||
|
||||
if (numeric > bestQuality) {
|
||||
bestQuality = numeric;
|
||||
bestUrl = src;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bestUrl) return;
|
||||
|
||||
const finalUrl = bestUrl;
|
||||
if (!url1080 && bestQuality >= 1080) url1080 = finalUrl;
|
||||
if (!url720 && bestQuality >= 720 && bestQuality < 1080) url720 = finalUrl;
|
||||
if (!url480 && bestQuality >= 480 && bestQuality < 720) url480 = finalUrl;
|
||||
|
||||
streams.push({
|
||||
title: _streamTitle("kodik", bestQuality),
|
||||
streamUrl: finalUrl,
|
||||
url1080,
|
||||
url720,
|
||||
url480,
|
||||
headers: {
|
||||
"User-Agent": _ua(),
|
||||
"Referer": SITE_BASE + "/"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function searchResults(keyword) {
|
||||
try {
|
||||
const query = String(keyword || "").trim();
|
||||
if (!query) return JSON.stringify([]);
|
||||
|
||||
const url = `${API_BASE}/catalog?query=${encodeURIComponent(query)}`;
|
||||
const response = await _apiGet(url);
|
||||
const data = await response.json();
|
||||
|
||||
const items = Array.isArray(data?.items) ? data.items : [];
|
||||
const out = items.map(item => ({
|
||||
title: String(item?.title || "Unknown"),
|
||||
image: _absUrl(item?.poster || "", SITE_BASE),
|
||||
href: String(item?.id || ""),
|
||||
_score: _scoreTitle(item?.title || "", query)
|
||||
}));
|
||||
|
||||
out.sort((a, b) => a._score - b._score);
|
||||
return JSON.stringify(out.map(({ _score, ...rest }) => rest));
|
||||
} catch (_) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractDetails(animeIdOrUrl) {
|
||||
try {
|
||||
const animeId = _extractAnimeId(animeIdOrUrl);
|
||||
if (!animeId) return JSON.stringify([]);
|
||||
|
||||
const response = await _apiGet(`${API_BASE}/anime/${encodeURIComponent(animeId)}`);
|
||||
const data = await response.json();
|
||||
|
||||
const aliases = [];
|
||||
if (data?.originalTitle) aliases.push(`Original: ${data.originalTitle}`);
|
||||
if (data?.alternateTitle) aliases.push(`Alt: ${data.alternateTitle}`);
|
||||
if (Number.isFinite(data?.length)) aliases.push(`Duration: ${data.length}m`);
|
||||
if (Number.isFinite(data?.episodesTotal)) aliases.push(`Episodes: ${data.episodesTotal}`);
|
||||
if (Array.isArray(data?.studios) && data.studios.length) {
|
||||
aliases.push(`Studios: ${data.studios.join(", ")}`);
|
||||
}
|
||||
|
||||
return JSON.stringify([
|
||||
{
|
||||
description: data?.description || "No description available",
|
||||
aliases: aliases.join(" | "),
|
||||
airdate: data?.airedAt ? String(data.airedAt).slice(0, 10) : "Unknown"
|
||||
}
|
||||
]);
|
||||
} catch (_) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractEpisodes(animeIdOrUrl) {
|
||||
try {
|
||||
const animeId = _extractAnimeId(animeIdOrUrl);
|
||||
if (!animeId) return JSON.stringify([]);
|
||||
|
||||
const response = await _apiGet(`${API_BASE}/anime/${encodeURIComponent(animeId)}`);
|
||||
const data = await response.json();
|
||||
const episodes = Array.isArray(data?.episodes) ? data.episodes : [];
|
||||
|
||||
const out = episodes
|
||||
.map((episode, index) => {
|
||||
const players = Array.isArray(episode?.players) ? episode.players : [];
|
||||
|
||||
const onwave = _pickPlayer(players, player => {
|
||||
const name = String(player?.name || "").toLowerCase();
|
||||
return name === "onwave" || (name.includes("onwave") && !name.includes("зеркало"));
|
||||
});
|
||||
|
||||
const mirror = _pickPlayer(players, player => {
|
||||
const name = String(player?.name || "").toLowerCase();
|
||||
return name.includes("зеркало") || name.includes("mirror");
|
||||
});
|
||||
|
||||
const kodik = _pickPlayer(players, player => {
|
||||
const name = String(player?.name || "").toLowerCase();
|
||||
const link = String(player?.link || "").toLowerCase();
|
||||
return name.includes("kodik") || link.includes("kodik");
|
||||
});
|
||||
|
||||
if (!onwave && !mirror && !kodik) return null;
|
||||
|
||||
const number = Number.isFinite(episode?.episode)
|
||||
? episode.episode
|
||||
: (Number.isFinite(episode?.index) ? episode.index : index + 1);
|
||||
|
||||
const payload = {
|
||||
animeId: String(animeId),
|
||||
season: Number.isFinite(episode?.season) ? episode.season : 1,
|
||||
episode: number,
|
||||
label: String(episode?.label || ""),
|
||||
players: {
|
||||
onwave: onwave?.link ? String(onwave.link) : "",
|
||||
onwaveMirror: mirror?.link ? String(mirror.link) : "",
|
||||
kodik: kodik?.link ? String(kodik.link) : ""
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
href: _packEpisode(payload),
|
||||
number,
|
||||
title: episode?.label ? String(episode.label) : `Episode ${number}`
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => Number(a.number) - Number(b.number));
|
||||
|
||||
return JSON.stringify(out);
|
||||
} catch (_) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStreamUrl(href) {
|
||||
try {
|
||||
const payload = _unpackEpisode(href);
|
||||
if (!payload) {
|
||||
return JSON.stringify({ streams: [], subtitle: DEFAULT_SUBTITLE });
|
||||
}
|
||||
|
||||
const streams = [];
|
||||
|
||||
const onwaveMp4 = _buildOnWaveMp4(payload?.players?.onwave);
|
||||
if (onwaveMp4) {
|
||||
streams.push({
|
||||
title: _streamTitle("onwave"),
|
||||
streamUrl: onwaveMp4,
|
||||
headers: {
|
||||
"User-Agent": _ua(),
|
||||
"Referer": "https://anisurf.site/"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mirrorMp4 = _buildOnWaveMp4(payload?.players?.onwaveMirror);
|
||||
if (mirrorMp4) {
|
||||
streams.push({
|
||||
title: _streamTitle("mirror"),
|
||||
streamUrl: mirrorMp4,
|
||||
headers: {
|
||||
"User-Agent": _ua(),
|
||||
"Referer": "https://anisurf.ru/"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const kodikLink = _absUrl(payload?.players?.kodik || "", SITE_BASE);
|
||||
if (kodikLink && (kodikLink.includes("kodikplayer.com") || kodikLink.includes("kodik.info"))) {
|
||||
try {
|
||||
const qualitiesJson = await kodikParser(kodikLink);
|
||||
const qualities = _safeJsonParse(qualitiesJson, {});
|
||||
_appendBestKodikStream(streams, qualities);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
streams,
|
||||
subtitle: DEFAULT_SUBTITLE
|
||||
});
|
||||
} catch (_) {
|
||||
return JSON.stringify({ streams: [], subtitle: DEFAULT_SUBTITLE });
|
||||
}
|
||||
}
|
||||
|
||||
// Kodik parser adapted from yummyanime.js approach.
|
||||
async function kodikParser(url) {
|
||||
try {
|
||||
const headers = {
|
||||
"Referer": SITE_BASE + "/",
|
||||
"User-Agent": _ua()
|
||||
};
|
||||
|
||||
const response = await fetchv2(url, headers);
|
||||
const htmlText = await response.text();
|
||||
|
||||
const urlParamsMatch = htmlText.match(/var\s+urlParams\s*=\s*'([^']+)'/);
|
||||
const videoInfoTypeMatch = htmlText.match(/vInfo\.type\s*=\s*'([^']+)'/);
|
||||
const videoInfoHashMatch = htmlText.match(/vInfo\.hash\s*=\s*'([^']+)'/);
|
||||
const videoInfoIdMatch = htmlText.match(/vInfo\.id\s*=\s*'([^']+)'/);
|
||||
|
||||
const urlParams = urlParamsMatch ? _safeJsonParse(urlParamsMatch[1], {}) : {};
|
||||
const videoInfoType = videoInfoTypeMatch ? videoInfoTypeMatch[1] : "";
|
||||
const videoInfoHash = videoInfoHashMatch ? videoInfoHashMatch[1] : "";
|
||||
const videoInfoId = videoInfoIdMatch ? videoInfoIdMatch[1] : "";
|
||||
|
||||
const finalData =
|
||||
`d=${urlParams.d || ""}` +
|
||||
`&d_sign=${urlParams.d_sign || ""}` +
|
||||
`&pd=${urlParams.pd || ""}` +
|
||||
`&pd_sign=${urlParams.pd_sign || ""}` +
|
||||
`&ref=${urlParams.ref || ""}` +
|
||||
`&ref_sign=${urlParams.ref_sign || ""}` +
|
||||
`&bad_user=false&cdn_is_working=true` +
|
||||
`&type=${videoInfoType}&hash=${videoInfoHash}&id=${videoInfoId}&info=%7B%7D`;
|
||||
|
||||
const headers2 = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"Referer": SITE_BASE + "/",
|
||||
"User-Agent": _ua(),
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
};
|
||||
|
||||
const apiResponse = await fetchv2("https://kodikplayer.com/ftor", headers2, "POST", finalData);
|
||||
const apiJson = await apiResponse.json();
|
||||
|
||||
const qualities = {};
|
||||
if (apiJson?.links) {
|
||||
for (const quality in apiJson.links) {
|
||||
const qualityArray = apiJson.links[quality];
|
||||
const first = Array.isArray(qualityArray) ? qualityArray[0] : null;
|
||||
if (!first?.src) continue;
|
||||
|
||||
qualities[quality] = {
|
||||
src: decode(first.src),
|
||||
type: first.type || "application/x-mpegURL"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(qualities, null, 2);
|
||||
} catch (_) {
|
||||
return JSON.stringify({ error: "kodik_parse_failed" });
|
||||
}
|
||||
}
|
||||
|
||||
function decode(input) {
|
||||
const map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
let out = "";
|
||||
let buffer = 0;
|
||||
let count = 0;
|
||||
|
||||
const rotated = [];
|
||||
const source = String(input || "");
|
||||
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const ch = source[i];
|
||||
if (/[a-zA-Z]/.test(ch)) {
|
||||
const code = ch.charCodeAt(0);
|
||||
const max = ch <= "Z" ? 90 : 122;
|
||||
const shifted = code + 18;
|
||||
rotated.push(String.fromCharCode(shifted <= max ? shifted : shifted - 26));
|
||||
} else {
|
||||
rotated.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
const rot = rotated.join("");
|
||||
for (let j = 0; j < rot.length; j++) {
|
||||
const ch = rot[j];
|
||||
if (ch === "=") break;
|
||||
const val = map.indexOf(ch);
|
||||
if (val === -1) continue;
|
||||
|
||||
buffer = (buffer << 6) | val;
|
||||
count += 6;
|
||||
|
||||
if (count >= 8) {
|
||||
count -= 8;
|
||||
out += String.fromCharCode((buffer >> count) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
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 (_) {}
|
||||
Reference in New Issue
Block a user