From 91dd2508237af515fc5d3327c1fc75a7283026d8 Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Thu, 10 Oct 2024 12:52:22 +0200 Subject: [PATCH] feat: create route to be able to mark episode as watched --- .../episodes/getByAniListId/index.ts | 1 - src/controllers/episodes/index.ts | 6 + .../episodes/markEpisodeAsWatched/anilist.ts | 59 ++++++++++ .../episodes/markEpisodeAsWatched/index.ts | 106 ++++++++++++++++++ src/controllers/watch-status/index.ts | 41 ++++--- src/libs/anilist/getTitle.ts | 2 +- 6 files changed, 197 insertions(+), 18 deletions(-) create mode 100644 src/controllers/episodes/markEpisodeAsWatched/anilist.ts create mode 100644 src/controllers/episodes/markEpisodeAsWatched/index.ts diff --git a/src/controllers/episodes/getByAniListId/index.ts b/src/controllers/episodes/getByAniListId/index.ts index ff3963f..b2c0efc 100644 --- a/src/controllers/episodes/getByAniListId/index.ts +++ b/src/controllers/episodes/getByAniListId/index.ts @@ -2,7 +2,6 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { env } from "hono/adapter"; import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; -import { readEnvVariable } from "~/libs/readEnvVariable"; import type { Env } from "~/types/env"; import { EpisodesResponse, EpisodesResponseSchema } from "~/types/episode"; import { diff --git a/src/controllers/episodes/index.ts b/src/controllers/episodes/index.ts index ce33e0f..f373810 100644 --- a/src/controllers/episodes/index.ts +++ b/src/controllers/episodes/index.ts @@ -10,5 +10,11 @@ app.route( "/", await import("./getEpisodeUrl").then((controller) => controller.default), ); +app.route( + "/", + await import("./markEpisodeAsWatched").then( + (controller) => controller.default, + ), +); export default app; diff --git a/src/controllers/episodes/markEpisodeAsWatched/anilist.ts b/src/controllers/episodes/markEpisodeAsWatched/anilist.ts new file mode 100644 index 0000000..1cf55a6 --- /dev/null +++ b/src/controllers/episodes/markEpisodeAsWatched/anilist.ts @@ -0,0 +1,59 @@ +import { graphql } from "gql.tada"; +import { GraphQLClient } from "graphql-request"; + +const MarkEpisodeAsWatchedMutation = graphql(` + mutation MarkEpisodeAsWatched($titleId: Int!, $episodeNumber: Int!) { + SaveMediaListEntry( + mediaId: $titleId + status: CURRENT + progress: $episodeNumber + ) { + id + } + } +`); + +const MarkTitleAsWatchedMutation = graphql(` + mutation MarkTitleAsWatched($titleId: Int!) { + SaveMediaListEntry(mediaId: $titleId, status: COMPLETED) { + id + } + } +`); + +export async function markEpisodeAsWatched( + aniListToken: string, + titleId: number, + episodeNumber: number, + markTitleAsComplete: boolean, +) { + const client = new GraphQLClient("https://graphql.anilist.co/"); + + console.log(aniListToken); + console.log( + typeof aniListToken, + titleId, + typeof titleId, + episodeNumber, + typeof episodeNumber, + markTitleAsComplete, + ); + const mutation = markTitleAsComplete + ? client.request( + MarkTitleAsWatchedMutation, + { titleId }, + { Authorization: `Bearer ${aniListToken}` }, + ) + : client.request( + MarkEpisodeAsWatchedMutation, + { titleId, episodeNumber }, + { Authorization: `Bearer ${aniListToken}` }, + ); + + return mutation + .then((data) => !!data?.SaveMediaListEntry?.id) + .catch(async (err) => { + console.error(await err.response); + throw err; + }); +} diff --git a/src/controllers/episodes/markEpisodeAsWatched/index.ts b/src/controllers/episodes/markEpisodeAsWatched/index.ts new file mode 100644 index 0000000..b1c66ec --- /dev/null +++ b/src/controllers/episodes/markEpisodeAsWatched/index.ts @@ -0,0 +1,106 @@ +import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; +import { env } from "hono/adapter"; + +import { updateWatchStatus } from "~/controllers/watch-status"; +import type { Env } from "~/types/env"; +import { + AniListIdQuerySchema, + EpisodeNumberSchema, + ErrorResponse, + ErrorResponseSchema, + SuccessResponse, + SuccessResponseSchema, +} from "~/types/schema"; + +import { markEpisodeAsWatched } from "./anilist"; + +const MarkEpisodeAsWatchedRequest = z.object({ + episodeNumber: EpisodeNumberSchema, + isComplete: z.boolean(), +}); + +const route = createRoute({ + tags: ["aniplay", "episodes"], + summary: "Mark episode as watched", + operationId: "markEpisodeAsWatched", + method: "post", + path: "/{aniListId}/watched", + request: { + params: z.object({ aniListId: AniListIdQuerySchema }), + body: { + content: { + "application/json": { + schema: MarkEpisodeAsWatchedRequest, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: SuccessResponseSchema(), + }, + }, + description: "Returns whether the episode was marked as watched", + }, + 401: { + content: { + "application/json": { + schema: ErrorResponseSchema, + }, + }, + description: "Unauthorized to mark the episode as watched", + }, + 500: { + content: { + "application/json": { + schema: ErrorResponseSchema, + }, + }, + description: "Error marking episode as watched", + }, + }, +}); + +const app = new OpenAPIHono(); + +app.openapi(route, async (c) => { + const aniListToken = c.req.header("X-AniList-Token"); + + if (!aniListToken) { + return c.json(ErrorResponse, { status: 401 }); + } + + const deviceId = c.req.header("X-Aniplay-DeviceId")!; + const aniListId = Number(c.req.param("aniListId")); + const { episodeNumber, isComplete } = + await c.req.json(); + + try { + await markEpisodeAsWatched( + aniListToken, + aniListId, + episodeNumber, + isComplete, + ); + if (isComplete) { + await updateWatchStatus( + env(c, "workerd"), + c.req, + deviceId, + aniListId, + "COMPLETED", + ); + } + } catch (error) { + console.error( + new Error("Failed to mark episode as watched", { cause: error }), + ); + return c.json(ErrorResponse, { status: 500 }); + } + + return c.json(SuccessResponse, 200); +}); + +export default app; diff --git a/src/controllers/watch-status/index.ts b/src/controllers/watch-status/index.ts index b58d9a5..27174ca 100644 --- a/src/controllers/watch-status/index.ts +++ b/src/controllers/watch-status/index.ts @@ -1,4 +1,5 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; +import type { HonoRequest } from "hono"; import { env } from "hono/adapter"; import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; @@ -65,6 +66,26 @@ const route = createRoute({ }, }); +export async function updateWatchStatus( + env: Env, + req: HonoRequest, + deviceId: string, + titleId: number, + watchStatus: WatchStatus | null, +) { + const { wasAdded, wasDeleted } = await setWatchStatus( + env, + deviceId, + Number(titleId), + watchStatus, + ); + if (wasAdded) { + await maybeScheduleNextAiringEpisode(env, req, titleId); + } else if (wasDeleted) { + await removeTask(env, "new-episode", buildNewEpisodeTaskId(titleId)); + } +} + app.openapi(route, async (c) => { const { deviceId, @@ -76,25 +97,13 @@ app.openapi(route, async (c) => { if (!isRetrying) { try { - const { wasAdded, wasDeleted } = await setWatchStatus( - env(c, "workerd"), + await updateWatchStatus( + env(c, "workerd"), + c.req, deviceId, - Number(titleId), + titleId, watchStatus, ); - if (wasAdded) { - await maybeScheduleNextAiringEpisode( - env(c, "workerd"), - c.req, - titleId, - ); - } else if (wasDeleted) { - await removeTask( - env(c, "workerd"), - "new-episode", - buildNewEpisodeTaskId(titleId), - ); - } } catch (error) { console.error(new Error("Error setting watch status", { cause: error })); console.error(error); diff --git a/src/libs/anilist/getTitle.ts b/src/libs/anilist/getTitle.ts index 64e88d8..badc125 100644 --- a/src/libs/anilist/getTitle.ts +++ b/src/libs/anilist/getTitle.ts @@ -34,7 +34,7 @@ export async function fetchTitleFromAnilist( if (error.message.includes("Not Found")) { return undefined; } - if (error.response.status === 429) { + if (error.response?.status === 429) { console.log( "429, retrying in", error.response.headers.get("Retry-After"),