AniX/api-prox/index.ts

372 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
ANIXART_API,
ANIXART_HEADERS,
ANIXART_HEADERST,
asJSON,
GetHook,
LoadedHook,
logger,
PostHook,
} from "./shared";
import express from "express";
import fs from "fs/promises";
import { MediaChromeTheme } from "./media-chrome";
import { Iframe } from "./iframe";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const host = "0.0.0.0";
const port = 7001;
const loadedHooks: LoadedHook[] = [];
app.get("/player", async (req, res) => {
let url = req.query.url || null;
res.status(200);
res.set({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
"Cache-Control": "no-cache",
"Content-Type": "text/html; charset=utf-8",
});
if (!url) {
res.send("<h1>No url query found!</h1>");
return;
}
let player = "";
let poster = "";
const CUSTOM_PLAYER_DOMAINS = [
"video.sibnet.ru",
"anixart.libria.fun",
"kodik.info",
"aniqit.com",
"kodik.cc",
"kodik.biz",
];
const urlDomain = new URL(url.toString());
const PLAYER_PARSER_URL = process.env.PLAYER_PARSER_URL || null;
if (CUSTOM_PLAYER_DOMAINS.includes(urlDomain.hostname)) {
try {
if (!PLAYER_PARSER_URL) throw new Error();
if (
["kodik.info", "aniqit.com", "kodik.cc", "kodik.biz"].includes(
urlDomain.hostname
)
) {
player = "kodik";
}
if ("anixart.libria.fun" == urlDomain.hostname) {
player = "libria";
}
if ("video.sibnet.ru" == urlDomain.hostname) {
player = "sibnet";
}
const playerParserRes = await fetch(
`${PLAYER_PARSER_URL}?url=${encodeURIComponent(url.toString())}&player=${player}`
);
if (!playerParserRes.ok) throw new Error();
const playerParserData: { manifest: string; poster: string } =
await playerParserRes.json();
poster = playerParserData.poster;
if (playerParserData.manifest.startsWith("#EXTM3U")) {
const playerUrlArray = playerParserData.manifest.split("\n");
url = playerUrlArray.join("\\n");
} else {
url = playerParserData.manifest;
}
} catch {
res.send(Iframe(url.toString()));
return;
}
} else if (url.toString().endsWith("mp4")) {
player = "mp4";
} else if (url.toString().endsWith(".m3u8")) {
player = "hls";
} else {
res.send(Iframe(url.toString()));
return;
}
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Веб-плеер</title>
<meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=yes' />
<style>body, html {height: 100%; width: 100%; margin: 0px;padding: 0px;border: 0px;}</style>
${["kodik", "libria", "hls"].includes(player) ? '<script type="module" src="https://cdn.jsdelivr.net/npm/hls-video-element@1.2/+esm"></script>' : ""}
<script>
window.onload = () => {
const url = "${url}";
const poster = "${poster}";
const element = document.querySelector("#video-element");
element.poster = poster;
if (url.startsWith("http")) {
element.src = url;
} else {
let file = new File([url], "manifest.m3u8", {
type: "application/x-mpegURL",
});
element.src = URL.createObjectURL(file);
};
};
</script>
</head>
<body>
${MediaChromeTheme()}
<media-theme
template="media-theme-sutro"
style="width:100%;height:100%;">
${
["kodik", "libria", "hls"].includes(player) ?
`<hls-video slot="media" playsinline id="video-element"></hls-video>`
: `<video slot="media" playsinline id="video-element"></video>`
}
</media-theme>
</body>
</html>
`);
});
app.get("/*path", async (req, res) => {
if (req.path == "/favicon.ico") return asJSON(res, {}, 404);
const url = new URL(`${ANIXART_API}${req.url}`);
logger.debug(
`[${req.method}] ${url.protocol}//${url.hostname}${url.pathname}`
);
// logger.debug(` ↳ [QUERY] ${url.searchParams.toString()}`);
if (
url.searchParams.get("API-Version") == "v2" ||
req.headers["api-version"] == "v2"
) {
// logger.debug(` ↳ Force API V2`);
ANIXART_HEADERS["Api-Version"] = "v2";
url.searchParams.delete("API-Version");
}
const apiResponse = await fetch(url.toString(), {
method: "GET",
headers: ANIXART_HEADERS,
});
if (
!apiResponse ||
!apiResponse.ok ||
apiResponse.headers.get("content-type") != "application/json"
) {
logger.error(
`Failed to fetch: '${url.protocol}//${url.hostname}${url.pathname}', Path probably doesn't exist`
);
asJSON(
res,
{
code: 99,
returned_value: {
request_status: apiResponse ? apiResponse.status : null,
request_content_type:
apiResponse ? apiResponse.headers.get("content-type") : null,
},
reason: "Path probably doesn't exist",
},
500
);
return;
}
let data = await apiResponse.json();
let hooks: string[] = [];
try {
hooks = await fs.readdir("./hooks");
} catch (err) {
logger.error("'hooks' directory not found");
}
for (let i = 0; i < hooks.length; i++) {
const name = hooks[i];
if (
!name.endsWith(".ts") ||
name.includes("example") ||
name.includes("disabled")
)
continue;
const isHookLoaded = loadedHooks.find(
(item) => item.path == `./hooks/${name}`
);
const stat = await fs.stat(`./hooks/${name}`);
if (isHookLoaded && isHookLoaded.mtime != stat.mtime.toISOString()) {
logger.infoHook(`Updated "./hooks/${name}"`);
delete require.cache[require.resolve(`./hooks/${name}`)];
isHookLoaded.mtime = stat.mtime.toISOString();
}
const hook: GetHook = require(`./hooks/${name}`);
if (!isHookLoaded) {
logger.infoHook(`Loaded "./hooks/${name}"`);
loadedHooks.push({
path: `./hooks/${name}`,
mtime: stat.mtime.toISOString(),
});
}
if (!hook.hasOwnProperty("match") || !hook.hasOwnProperty("get")) continue;
if (!hook.match(req.path)) continue;
data = await hook.get(data, url);
}
asJSON(res, data, 200);
return;
});
app.post("/*path", async (req, res) => {
const url = new URL(`${ANIXART_API}${req.url}`);
logger.debug(
`[${req.method}] ${url.protocol}//${url.hostname}${url.pathname}`
);
// logger.debug(` ↳ [QUERY] ${url.searchParams.toString()}`);
let apiResponse: null | Response = null;
const apiHeaders: ANIXART_HEADERST = {
"User-Agent": ANIXART_HEADERS["User-Agent"],
"Content-Type": req.headers["content-type"] || "application/json",
};
if (
url.searchParams.get("API-Version") == "v2" ||
req.headers["api-version"] == "v2"
) {
// logger.debug(` ↳ Force API V2`);
apiHeaders["Api-Version"] = "v2";
url.searchParams.delete("API-Version");
}
const reqContentType =
req.headers["content-type"] ?
req.headers["content-type"].split(";")[0]
: "application/json";
switch (reqContentType) {
case "multipart/form-data":
const formData = new FormData();
for (const name in req.body) {
formData.append(name, req.body[name]);
}
apiResponse = await fetch(url.toString(), {
method: "POST",
headers: apiHeaders,
body: formData,
});
break;
case "application/x-www-form-urlencoded":
apiResponse = await fetch(url.toString(), {
method: "POST",
headers: apiHeaders,
body: new URLSearchParams(req.body),
});
break;
case "application/json":
apiResponse = await fetch(url.toString(), {
method: "POST",
headers: apiHeaders,
body: JSON.stringify(req.body),
});
break;
}
// logger.console("debug", ` ↳ [REQ BODY]`, req.body);
// logger.console("debug", ` ↳ [REQ HEADERS]`, req.headers);
// logger.console("debug", " ↳ [RES TEXT]", await apiResponse.text());
// logger.console("debug", " ↳ [RES HEADERS]", apiResponse.headers);
if (
!apiResponse ||
!apiResponse.ok ||
apiResponse.headers.get("content-type") != "application/json"
) {
logger.error(
`Failed to post: '${url.protocol}//${url.hostname}${url.pathname}', Path probably doesn't exist`
);
asJSON(
res,
{
code: 99,
returned_value: {
request_status: apiResponse ? apiResponse.status : null,
request_content_type:
apiResponse ? apiResponse.headers.get("content-type") : null,
},
reason: "Path probably doesn't exist",
},
500
);
return;
}
let data = await apiResponse.json();
let hooks: string[] = [];
try {
hooks = await fs.readdir("./hooks");
} catch (err) {
logger.error("'hooks' directory not found");
}
for (let i = 0; i < hooks.length; i++) {
const name = hooks[i];
if (
!name.endsWith(".ts") ||
name.includes("example") ||
name.includes("disabled")
)
continue;
const isHookLoaded = loadedHooks.find(
(item) => item.path == `./hooks/${name}`
);
const stat = await fs.stat(`./hooks/${name}`);
if (isHookLoaded && isHookLoaded.mtime != stat.mtime.toISOString()) {
logger.infoHook(`Updated "./hooks/${name}"`);
delete require.cache[require.resolve(`./hooks/${name}`)];
isHookLoaded.mtime = stat.mtime.toISOString();
}
const hook: PostHook = require(`./hooks/${name}`);
if (!isHookLoaded) {
logger.infoHook(`Loaded "./hooks/${name}"`);
loadedHooks.push({
path: `./hooks/${name}`,
mtime: stat.mtime.toISOString(),
});
}
if (!hook.hasOwnProperty("match") || !hook.hasOwnProperty("post")) continue;
if (!hook.match(req.path)) continue;
data = await hook.post(data, url);
}
asJSON(res, data, 200);
return;
});
app.listen(port, host, function () {
logger.info(`Server listen: http://${host}:${port}`);
});