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 (_) {}