208 lines
7.1 KiB
JavaScript
Executable File
208 lines
7.1 KiB
JavaScript
Executable File
#!/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)));
|