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(); 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(); const aniListToken = c.req.header("X-AniList-Token"); const client = new Client({ token: readEnvVariable(c.env, "QSTASH_TOKEN") }); if (isRetrying) { if (!(await verifyQstashHeader(env(c, "workerd"), c.req))) { return c.json(ErrorResponse, { status: 401 }); } } else { try { const { wasAdded, wasDeleted } = await setWatchStatus( env(c, "workerd"), deviceId, Number(titleId), watchStatus, ); if (wasAdded) { await maybeScheduleNextAiringEpisode( env(c, "workerd"), c.req, titleId, ); } else if (wasDeleted) { const messageId = await getTitleMessage( env(c, "workerd"), titleId, ); if (messageId) { await client.messages.delete(messageId); await deleteTitleMessage(env(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;