#!/usr/bin/env node "use strict"; const path = require("path"); const axios = require("axios"); const PROVIDERS = { anikoto: "./extensions/Anime/anikoto.js", anineko: "./extensions/Anime/anineko.js", pahe: "./extensions/Anime/pahe.js", }; const dynamicReferers = new Map(); let fallbackReferer = ""; global.axios = axios.create({ timeout: 30000, headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", Accept: "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", }, }); global.setDynamicReferer = (domain, referer) => { if (domain && referer) dynamicReferers.set(String(domain), String(referer)); }; global.setFallbackReferer = (referer) => { if (referer) fallbackReferer = String(referer); }; function fail(message, code = 1) { process.stderr.write(`${message}\n`); process.exit(code); } function providerName(raw) { const value = String(raw || "").trim().toLowerCase(); if (!value || !PROVIDERS[value]) fail(`Unknown provider: ${raw || ""}`); return value; } function loadProvider(name) { return require(path.resolve(__dirname, PROVIDERS[name])); } function parseId(value) { const text = String(value || "").trim(); const match = text.match(/^([a-z0-9_-]+):(.+)$/i); if (match && PROVIDERS[match[1].toLowerCase()]) { return { provider: match[1].toLowerCase(), id: match[2] }; } return { provider: "", id: text }; } async function collectEpisodes(provider, id) { const first = await provider.fetchEpisode(id, 1); const episodes = Array.isArray(first?.episodes) ? [...first.episodes] : []; const totalPages = Number(first?.totalPages || 1); for (let page = 2; page <= totalPages; page += 1) { const payload = await provider.fetchEpisode(id, page); if (Array.isArray(payload?.episodes)) episodes.push(...payload.episodes); } return episodes; } function qualityScore(source, wanted) { const text = `${source.quality || ""} ${source.name || ""}`; const match = text.match(/(\d{3,4})p?/); const value = match ? Number(match[1]) : 0; const normalized = String(wanted || "best").replace(/p$/i, "").toLowerCase(); if (normalized === "worst") return value ? -value : 0; if (normalized === "best" || !/^\d+$/.test(normalized)) return value; const target = Number(normalized); if (!value) return -9999; return -Math.abs(target - value); } function sourceMatchesMode(source, mode) { const requested = String(mode || "sub").toLowerCase(); const type = String(source?.type || source?.lang || requested).toLowerCase(); if (requested === "dub") return type === "dub"; if (requested === "sub") return type === "sub" || type === "softsub" || type === "hsub" || type === "hardsub"; return type === requested; } async function resolve(provider, episodeId, mode, quality) { const candidates = await resolveAll(provider, episodeId, mode, quality); if (candidates.length) return candidates[0]; throw new Error("Could not resolve a playable source."); } async function resolveAll(provider, episodeId, mode, quality) { const sourcesPayload = await provider.fetchEpisodeSources(episodeId, mode); const sources = Array.isArray(sourcesPayload?.sources) ? sourcesPayload.sources : []; if (!sources.length) throw new Error("No episode sources were returned."); sources.sort((a, b) => qualityScore(b, quality) - qualityScore(a, quality)); const resolvedSources = []; const resolvedSourceKeys = new Set(); for (const source of sources) { let resolved = null; try { resolved = source.isUnresolved && provider.processServer ? await provider.processServer(source.rawServer || source) : source; } catch (err) { process.stderr.write(`Source ${source.name || source.quality || "server"} failed to resolve: ${err?.message || err}\n`); continue; } if (resolved?.url) { const sourceType = resolved.type || resolved.lang || source.type || source.lang || mode; if (!sourceMatchesMode({ type: sourceType }, mode)) continue; const headers = Object.assign({}, resolved.headers || {}); try { const host = new URL(resolved.url).hostname; if (!headers.Referer && dynamicReferers.has(host)) { headers.Referer = dynamicReferers.get(host); } } catch (_) {} if (!headers.Referer && fallbackReferer) headers.Referer = fallbackReferer; const headerKey = Object.entries(headers) .map(([key, value]) => [String(key).toLowerCase(), String(value)]) .sort(([left], [right]) => left.localeCompare(right)); const sourceKey = `${resolved.url}\n${JSON.stringify(headerKey)}`; if (resolvedSourceKeys.has(sourceKey)) continue; resolvedSourceKeys.add(sourceKey); resolvedSources.push({ url: resolved.url, isM3U8: Boolean(resolved.isM3U8 || String(resolved.url).includes(".m3u8")), quality: resolved.quality || source.quality || source.name || "auto", type: sourceType, headers, subtitles: resolved.subtitles || sourcesPayload.subtitles || [], server: source.name || resolved.name || resolved.quality || "server", }); } } return resolvedSources; } async function main() { const [command, rawProvider, ...rest] = process.argv.slice(2); if (!command) fail("Missing command."); if (command === "providers") { console.log(JSON.stringify(Object.keys(PROVIDERS))); return; } const name = providerName(rawProvider); const provider = loadProvider(name); if (command === "search") { const query = rest[0] || ""; const page = Number(rest[1] || 1); const data = await provider.SearchAnime(query, { page }); const results = (data.results || []).map((item, index) => ({ id: `${name}:${item.id}`, provider: name, provider_id: item.id, title: item.title || item.name || item.id, image: item.image || null, index: index + 1, })); console.log(JSON.stringify({ ...data, results })); return; } const parsed = parseId(rest[0]); if (parsed.provider && parsed.provider !== name) { fail(`Show id provider ${parsed.provider} does not match requested ${name}.`); } const id = parsed.id; if (!id) fail("Missing id."); if (command === "info") { const data = await provider.AnimeInfo(id); console.log(JSON.stringify({ ...data, id: `${name}:${data.id || id}`, provider: name, provider_id: data.id || id })); return; } if (command === "episodes") { const episodes = await collectEpisodes(provider, id); console.log(JSON.stringify({ episodes })); return; } if (command === "resolve") { const mode = rest[1] || "sub"; const quality = rest[2] || "best"; const data = await resolve(provider, id, mode, quality); console.log(JSON.stringify(data)); return; } if (command === "resolve-all") { const mode = rest[1] || "sub"; const quality = rest[2] || "best"; const data = await resolveAll(provider, id, mode, quality); console.log(JSON.stringify({ sources: data })); return; } fail(`Unknown command: ${command}`); } main().catch((err) => fail(err?.stack || err?.message || String(err)));