mirror of
https://github.com/Radiquum/AniX.git
synced 2025-09-05 14:05:36 +05:00
feat: start implementing api proxy with ability to use hooks: #8
This commit is contained in:
parent
9931962a6b
commit
6f45876240
6 changed files with 1720 additions and 0 deletions
28
api-prox/hooks/profile.example.ts
Normal file
28
api-prox/hooks/profile.example.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// хук меняет юзернейм 'Anixart' на 'Anixartiki'
|
||||||
|
|
||||||
|
import { logger } from "../shared";
|
||||||
|
|
||||||
|
export function match(path: string): boolean {
|
||||||
|
// id профиля 1, это профиль Anixart (разработчиков)
|
||||||
|
if (path == "/profile/1") return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(data: any, url: URL) {
|
||||||
|
const newUname = "Anixartiki";
|
||||||
|
|
||||||
|
// проверяем что есть поле 'profile' и оно не равно 'null', что значит что мы получили данные с апи и можно двигаться дальше
|
||||||
|
// иначе возвращаем оригинальные данные
|
||||||
|
if (!data.hasOwnProperty("profile") || !data.profile) return data;
|
||||||
|
|
||||||
|
// выводим сообщение в лог, если уровень логгера 'debug'
|
||||||
|
logger.debugHook(
|
||||||
|
`Changed username of '${data["profile"]["login"]}' (${data["profile"]["id"]}) to ${newUname}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// меняем поле на новый юзернейм
|
||||||
|
data["profile"]["login"] = newUname;
|
||||||
|
|
||||||
|
// возвращаем изменённые данные
|
||||||
|
return data;
|
||||||
|
}
|
41
api-prox/hooks/release.ts
Normal file
41
api-prox/hooks/release.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// хук добавляет рейтинг шикимори в поле note релиза
|
||||||
|
|
||||||
|
export function match(path: string): boolean {
|
||||||
|
// используем только страницы с путём /release/<id>
|
||||||
|
const pathRe = /\/release\/\d+/
|
||||||
|
if (pathRe.test(path)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(data: any, url: URL) {
|
||||||
|
// проверяем что есть поле 'release'
|
||||||
|
// иначе возвращаем оригинальные данные
|
||||||
|
if (!data.hasOwnProperty("release")) return data;
|
||||||
|
|
||||||
|
// ищём аниме на шикимори по названию, т.к. ид аниме аниксарт и шикимори разные и нет никакого референса друг на друга
|
||||||
|
const shikiIdRes = await fetch(
|
||||||
|
`https://shikimori.one/api/animes?search=${data["release"]["title_original"]}`
|
||||||
|
);
|
||||||
|
if (!shikiIdRes.ok) return data; // если при поиске произошла ошибка, то возвращаем оригинальные данные
|
||||||
|
const shikiIdJson = await shikiIdRes.json();
|
||||||
|
if (shikiIdJson.length == 0) return data; // если нет результатов, то возвращаем оригинальные данные
|
||||||
|
|
||||||
|
const shikiId = shikiIdJson[0]["id"]; // берём ид от первого результата
|
||||||
|
|
||||||
|
// повторяем процесс, уже с ид от шикимори
|
||||||
|
const shikiAnimRes = await fetch(
|
||||||
|
`https://shikimori.one/api/animes/${shikiId}}`
|
||||||
|
);
|
||||||
|
if (!shikiAnimRes.ok) return data;
|
||||||
|
const shikiAnimJson = await shikiAnimRes.json();
|
||||||
|
|
||||||
|
// пушим строки в список, что-бы было легче их объединить
|
||||||
|
const noteBuilder = [];
|
||||||
|
if (data["release"]["note"] != null) noteBuilder.push(`${data.note}<hr/>`); // если в поле note уже что-то есть, разделяем значение и рейтинг
|
||||||
|
noteBuilder.push(`<b>Рейтинг Shikimori:</b> ${shikiAnimJson.score}★<br>`); // добавляем рейтинг от шикимори
|
||||||
|
data["release"]["note"] = noteBuilder.toString(); // заменяем оригинальное поле нашей строкой
|
||||||
|
data["release"]["id_shikimori"] = shikiId; // добавляем айди шикимори в ответ, потому что почему нет
|
||||||
|
|
||||||
|
// возвращаем изменённые данные
|
||||||
|
return data;
|
||||||
|
}
|
92
api-prox/index.ts
Normal file
92
api-prox/index.ts
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import { ANIXART_API, ANIXART_HEADERS, asJSON, logger } from "./shared";
|
||||||
|
import express from "express";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const host = "0.0.0.0";
|
||||||
|
const port = 7001;
|
||||||
|
|
||||||
|
const loadedHooks = [];
|
||||||
|
|
||||||
|
app.get("/*path", async (req, res) => {
|
||||||
|
const url = new URL(`${ANIXART_API}${req.url}`);
|
||||||
|
logger.debug(`Fetching ${url.protocol}//${url.hostname}${url.pathname}`);
|
||||||
|
|
||||||
|
const isApiV2 = url.searchParams.get("API-Version") == "v2" || false;
|
||||||
|
if (isApiV2) {
|
||||||
|
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.ok ||
|
||||||
|
apiResponse.headers.get("content-type") != "application/json"
|
||||||
|
) {
|
||||||
|
logger.error(`Failed to fetch: ${url.protocol}//${url.hostname}${url.pathname}`)
|
||||||
|
asJSON(
|
||||||
|
res,
|
||||||
|
{
|
||||||
|
code: 99,
|
||||||
|
returned_value: {
|
||||||
|
request_status: apiResponse.status,
|
||||||
|
request_content_type: apiResponse.headers.get("content-type"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = await apiResponse.json();
|
||||||
|
let hooks = [];
|
||||||
|
|
||||||
|
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")) continue;
|
||||||
|
if (name.includes("example")) 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 = 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.listen(port, host, function () {
|
||||||
|
logger.info(`Server listen: http://${host}:${port}`);
|
||||||
|
});
|
1453
api-prox/package-lock.json
generated
Normal file
1453
api-prox/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
20
api-prox/package.json
Normal file
20
api-prox/package.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "anix-api-proxy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Proxy and Hook requests from anix to anixart api",
|
||||||
|
"main": "index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"serve": "npx tsx ./index.ts"
|
||||||
|
},
|
||||||
|
"author": "Radiquum",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.3",
|
||||||
|
"@types/node": "^24.0.4",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"tsx": "^4.20.3"
|
||||||
|
}
|
||||||
|
}
|
86
api-prox/shared.ts
Normal file
86
api-prox/shared.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
export const corsHeaders = {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resHeaders = {
|
||||||
|
...corsHeaders,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function asJSON(res, object: any, status: number) {
|
||||||
|
res.status(status).type("application/json");
|
||||||
|
res.set(corsHeaders);
|
||||||
|
res.send(JSON.stringify(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ANIXART_UA =
|
||||||
|
"AnixartApp/8.2.1-23121216 (Android 9; SDK 28; arm64-v8a; samsung SM-G975N; en)";
|
||||||
|
export const ANIXART_API = "https://api.anixart.app";
|
||||||
|
export const ANIXART_HEADERS = {
|
||||||
|
"User-Agent": ANIXART_UA,
|
||||||
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogLevel = "debug" | "info" | "warn" | "error" | "disable";
|
||||||
|
export class Log {
|
||||||
|
level: LogLevel;
|
||||||
|
levelInt = {
|
||||||
|
debug: 0,
|
||||||
|
info: 1,
|
||||||
|
warn: 2,
|
||||||
|
error: 3,
|
||||||
|
disable: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(level: LogLevel = "info") {
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
getString(...args: string[]): string {
|
||||||
|
return args.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTime(): string {
|
||||||
|
const datetime = new Date();
|
||||||
|
return `${datetime.getHours().toString().padStart(2, "0")}:${datetime.getMinutes().toString().padStart(2, "0")}:${datetime.getSeconds().toString().padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(...msg: string[]) {
|
||||||
|
if (this.levelInt[this.level] <= 0)
|
||||||
|
console.log(`[DEBUG](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||||
|
}
|
||||||
|
info(...msg: string[]) {
|
||||||
|
if (this.levelInt[this.level] <= 1)
|
||||||
|
console.log(`[INFO](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||||
|
}
|
||||||
|
warn(...msg: string[]) {
|
||||||
|
if (this.levelInt[this.level] <= 2)
|
||||||
|
console.log(`[WARN](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||||
|
}
|
||||||
|
error(...msg: string[]) {
|
||||||
|
if (this.levelInt[this.level] <= 3)
|
||||||
|
console.log(`[ERROR](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugHook(...msg: string[]) {
|
||||||
|
if (this.levelInt[this.level] <= 0)
|
||||||
|
console.log(`[DEBUG|HOOK](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||||
|
}
|
||||||
|
infoHook(...msg: string[]) {
|
||||||
|
if (this.levelInt[this.level] <= 1)
|
||||||
|
console.log(`[INFO|HOOK](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||||
|
}
|
||||||
|
warnHook(...msg: string[]) {
|
||||||
|
if (this.levelInt[this.level] <= 2)
|
||||||
|
console.log(`[WARN|HOOK](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||||
|
}
|
||||||
|
errorHook(...msg: string[]) {
|
||||||
|
if (this.levelInt[this.level] <= 3)
|
||||||
|
console.log(`[ERROR|HOOK](${this.getTime()}) -> ${this.getString(...msg)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = new Log((process.env.LOG_LEVEL as LogLevel) || "info");
|
Loading…
Add table
Add a link
Reference in a new issue