diff --git a/api-prox/.dockerignore b/api-prox/.dockerignore new file mode 100644 index 0000000..2ec179f --- /dev/null +++ b/api-prox/.dockerignore @@ -0,0 +1,11 @@ +.vercel +vercel.json +.wrangler +wrangler.json +node_modules +deno.ts +deno.lock +bun.ts +bun.lock +README.md +README.RU.md diff --git a/api-prox/Dockerfile b/api-prox/Dockerfile new file mode 100644 index 0000000..3797e6f --- /dev/null +++ b/api-prox/Dockerfile @@ -0,0 +1,16 @@ +FROM node:23-alpine + +LABEL org.opencontainers.image.source=https://github.com/radiquum/anix + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +ADD src ./src +COPY node.ts ./ +COPY tsconfig.json ./ + +EXPOSE 7001 + +CMD ["npm", "run", "node-run"] \ No newline at end of file diff --git a/api-prox/src/hooks/enabledHooks.ts b/api-prox/src/hooks/enabledHooks.ts new file mode 100644 index 0000000..f99b0d6 --- /dev/null +++ b/api-prox/src/hooks/enabledHooks.ts @@ -0,0 +1,5 @@ +import { Hook } from "."; +import thirdPartyReleaseRatingHook from "./show3rdPartyReleaseRating.js"; // Импортирует .ts как .js + +export const enabledHooks: Hook[] = [thirdPartyReleaseRatingHook]; +export default enabledHooks; diff --git a/api-prox/src/hooks/index.ts b/api-prox/src/hooks/index.ts index 25103ce..c6bf2bc 100644 --- a/api-prox/src/hooks/index.ts +++ b/api-prox/src/hooks/index.ts @@ -1,21 +1,28 @@ +import { enabledHooks } from "./enabledHooks"; + export type Hook = { + title: string; + description: string | null; priority: number; match: (url: URL, method: "GET" | "POST") => boolean; - hook: (url: URL, data: any, method: "GET" | "POST") => any; + hook: (url: URL, data: any, method: "GET" | "POST") => Promise; }; -import testHook from "./test.js"; - -export const hookList: Hook[] = sortHooks([testHook]); +export const hookList: Hook[] = sortHooks(enabledHooks); export function sortHooks(hooks: Hook[]) { return hooks.sort((a, b) => b.priority - a.priority); } -export function runHooks(hooks: Hook[], url: URL, data: any, method: "GET" | "POST") { +export async function runHooks( + hooks: Hook[], + url: URL, + data: any, + method: "GET" | "POST" +) { for (const hook of hooks) { if (hook.match(url, method)) { - data = hook.hook(data, url, method); + data = await hook.hook(data, url, method); } } return data; diff --git a/api-prox/src/hooks/show3rdPartyReleaseRating.ts b/api-prox/src/hooks/show3rdPartyReleaseRating.ts new file mode 100644 index 0000000..03719ba --- /dev/null +++ b/api-prox/src/hooks/show3rdPartyReleaseRating.ts @@ -0,0 +1,85 @@ +// хук добавляет рейтинг Shikimori и MAL в поле note релиза +import { InfoLogger } from "../utils/logger"; +import { tryCatch } from "../utils/tryCatch"; + +const title = "show3rdPartyReleaseRating.ts"; +const description = "Добавление рейтингов от Shikimori и MAL на страницу релиза"; +const priority = 0; + +function match(url: URL, method: "GET" | "POST"): boolean { + const pathRe = /\/release\/\d+/; + return pathRe.test(url.pathname) && method == "GET"; +} + +const timeout = 5000; // таймаут в миллисекундах, если запрос рейтинга происходит слишком долго + +async function getShikimoriRating(title: string) { + // ищём аниме на шикимори по названию, т.к. ид аниме аниксарт и шикимори разные и нет никакого референса друг на друга + const { data, error } = await tryCatch( + fetch(`https://shikimori.one/api/animes?search=${title}`, { + signal: AbortSignal.timeout(timeout), + }) + ); + + if (error || !data.ok) return null; // если при поиске произошла ошибка, то возвращаем null + const searchJson: any = await data.json(); + if (searchJson.length == 0) return null; // если нет результатов, то возвращаем null + + // берём ид от первого результата + const shikiId = searchJson[0]["id"]; + + // повторяем процесс, уже с ид от шикимори + const { data: shikiData, error: shikiError } = await tryCatch( + fetch(`https://shikimori.one/api/animes/${shikiId}`, { + signal: AbortSignal.timeout(timeout), + }) + ); + + if (shikiError || !shikiData.ok) return null; // если при произошла ошибка, то возвращаем null + + // возвращаем рейтинг + const shikiJson: any = await shikiData.json(); + return Number(shikiJson.score); +} + +async function getMALRating(title: string) { + // ищём аниме на MAL по названию, через API Jikan, т.к. ид аниме аниксарт и шикимори разные и нет никакого референса друг на друга + const { data, error } = await tryCatch( + fetch(`https://api.jikan.moe/v4/anime?q=${title}`, { + signal: AbortSignal.timeout(timeout), + }) + ); + + if (error || !data.ok) return null; // если при поиске произошла ошибка, то возвращаем null + const malJson: any = await data.json(); + if (malJson.data.length == 0) return null; // если нет результатов, то возвращаем null + + // возвращаем рейтинг от первого результата + return Number(malJson.data[0].score); +} + +async function hook(data: any, _: URL, __: "GET" | "POST") { + // проверяем что есть поле 'release' + // иначе возвращаем оригинальные данные + if (!data.hasOwnProperty("release")) return data; + + // ищем рейтинг + const shikiRating = await getShikimoriRating(data.release.title_original); + const malRating = await getMALRating(data.release.title_original); + + // добавляем рейтинг + // пушим строки в список, что-бы было легче их объединить + const noteBuilder = []; + if (data.release.note) noteBuilder.push(`${data.release.note}`); // первым добавляем оригинальное значение примечания, если оно есть + data.release.note && (shikiRating || malRating) && noteBuilder.push("-".repeat(100)); // добавляем разделитель, если есть рейтинг и оригинальное примечание + shikiRating && noteBuilder.push(`Рейтинг Shikimori: ${shikiRating}★`); // добавляем рейтинг от шикимори + malRating && noteBuilder.push(`Рейтинг My Anime List: ${malRating}★`); // добавляем рейтинг от MAL + data.release.note = noteBuilder.join("
"); // заменяем оригинальное поле нашей строкой + + InfoLogger(title, "Fetched and set 3rd party rating as note"); + // возвращаем изменённые данные + return data; +} + +const entrypoint = { title, description, priority, match, hook }; +export default entrypoint; diff --git a/api-prox/src/hooks/test.ts b/api-prox/src/hooks/test.ts deleted file mode 100644 index 9a0f2a6..0000000 --- a/api-prox/src/hooks/test.ts +++ /dev/null @@ -1,15 +0,0 @@ -const priority = 0; - -function match(url: URL, method: "GET" | "POST"): boolean { - return url.pathname == "/profile/1" && method == "GET"; -} - -function hook(data: any, _: URL, __: "GET" | "POST") { - const newUname = "Anixartiki"; - if (!data.hasOwnProperty("profile") || !data.profile) return data; - data["profile"]["login"] = newUname; - return data; -} - -const entrypoint = { priority, match, hook }; -export default entrypoint; diff --git a/api-prox/src/index.ts b/api-prox/src/index.ts index d854434..dd7f0e2 100644 --- a/api-prox/src/index.ts +++ b/api-prox/src/index.ts @@ -21,8 +21,8 @@ app.get("/", (c) => { @@ -49,9 +49,9 @@ app.get("/health", (c) => { ${asciiHTML()} ${separatorHTML()} @@ -59,6 +59,10 @@ app.get("/health", (c) => {

Request Time: ${new Date().toLocaleString("ru-RU")}

Version: ${appVersion}

Runner: ${getRunningEnvironment()}

+

Enabled Hooks:

+ `); @@ -70,6 +74,10 @@ app.get("/health/json", (c) => { time: new Date().getTime(), version: appVersion, runner: getRunningEnvironment(), + enabledHooks: hookList.map((hook) => ({ + title: hook.title, + description: hook.description, + })), }); });