Files
aniplay-api/src/controllers/watch-status/index.ts

136 lines
3.7 KiB
TypeScript

import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { Client } from "@upstash/qstash";
import { env } from "hono/adapter";
import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode";
import { verifyQstashHeader } from "~/libs/qstash/verifyQstashHeader";
import { readEnvVariable } from "~/libs/readEnvVariable";
import { deleteTitleMessage, getTitleMessage } from "~/models/titleMessages";
import { setWatchStatus } from "~/models/watchStatus";
import type { Env } from "~/types/env";
import {
AniListIdSchema,
ErrorResponse,
ErrorResponseSchema,
SuccessResponse,
SuccessResponseSchema,
} from "~/types/schema";
import { WatchStatus } from "~/types/title/watchStatus";
import { maybeUpdateWatchStatusOnAnilist } from "./anilist";
const app = new OpenAPIHono<Env>();
const UpdateWatchStatusRequest = z.object({
deviceId: z.string(),
watchStatus: WatchStatus.nullable(),
titleId: AniListIdSchema,
isRetrying: z.boolean().optional().default(false),
});
const route = createRoute({
tags: ["aniplay", "title"],
operationId: "updateWatchStatus",
summary: "Update watch status for a title",
description:
"Updates the watch status for a title. If the user sets the watch status to 'watching', they'll start getting notified about new episodes.",
method: "post",
path: "/",
request: {
body: {
content: {
"application/json": {
schema: UpdateWatchStatusRequest,
},
},
},
headers: z.object({ "x-anilist-token": z.string().nullish() }),
},
responses: {
200: {
content: {
"application/json": {
schema: SuccessResponseSchema(),
},
},
description: "Watch status was successfully updated",
},
500: {
content: {
"application/json": {
schema: ErrorResponseSchema,
},
},
description: "Failed to update watch status",
},
},
});
app.openapi(route, async (c) => {
const {
deviceId,
watchStatus,
titleId,
isRetrying = false,
} = await c.req.json<typeof UpdateWatchStatusRequest._type>();
const aniListToken = c.req.header("X-AniList-Token");
const client = new Client({ token: readEnvVariable(c.env, "QSTASH_TOKEN") });
if (isRetrying) {
if (!(await verifyQstashHeader(env<Env, typeof c>(c, "workerd"), c.req))) {
return c.json(ErrorResponse, { status: 401 });
}
} else {
try {
const { wasAdded, wasDeleted } = await setWatchStatus(
env<Env, typeof c>(c, "workerd"),
deviceId,
Number(titleId),
watchStatus,
);
if (wasAdded) {
await maybeScheduleNextAiringEpisode(
env<Env, typeof c>(c, "workerd"),
c.req,
titleId,
);
} else if (wasDeleted) {
const messageId = await getTitleMessage(
env<Env, typeof c>(c, "workerd"),
titleId,
);
if (messageId) {
await client.messages.delete(messageId);
await deleteTitleMessage(env<Env, typeof c>(c, "workerd"), titleId);
}
}
} catch (error) {
console.error(new Error("Error setting watch status", { cause: error }));
console.error(error);
return c.json(ErrorResponse, { status: 500 });
}
}
try {
await maybeUpdateWatchStatusOnAnilist(
Number(titleId),
watchStatus,
aniListToken,
);
} catch (error) {
console.error(
new Error("Failed to update watch status on Anilist", { cause: error }),
);
client.publishJSON({
url: c.req.url,
body: { deviceId, watchStatus, titleId, isRetrying: true },
retries: 0,
delay: 60,
});
}
return c.json(SuccessResponse, { status: 200 });
});
export default app;