feat: create route to be able to mark episode as watched

This commit is contained in:
2024-10-10 12:52:22 +02:00
parent 223c2f1e4c
commit 91dd250823
6 changed files with 197 additions and 18 deletions

View File

@@ -2,7 +2,6 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { env } from "hono/adapter"; import { env } from "hono/adapter";
import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources";
import { readEnvVariable } from "~/libs/readEnvVariable";
import type { Env } from "~/types/env"; import type { Env } from "~/types/env";
import { EpisodesResponse, EpisodesResponseSchema } from "~/types/episode"; import { EpisodesResponse, EpisodesResponseSchema } from "~/types/episode";
import { import {

View File

@@ -10,5 +10,11 @@ app.route(
"/", "/",
await import("./getEpisodeUrl").then((controller) => controller.default), await import("./getEpisodeUrl").then((controller) => controller.default),
); );
app.route(
"/",
await import("./markEpisodeAsWatched").then(
(controller) => controller.default,
),
);
export default app; export default app;

View File

@@ -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;
});
}

View File

@@ -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<Env>();
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<typeof MarkEpisodeAsWatchedRequest._type>();
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;

View File

@@ -1,4 +1,5 @@
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import type { HonoRequest } from "hono";
import { env } from "hono/adapter"; import { env } from "hono/adapter";
import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; 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) => { app.openapi(route, async (c) => {
const { const {
deviceId, deviceId,
@@ -76,25 +97,13 @@ app.openapi(route, async (c) => {
if (!isRetrying) { if (!isRetrying) {
try { try {
const { wasAdded, wasDeleted } = await setWatchStatus( await updateWatchStatus(
env<Env, typeof c>(c, "workerd"), env(c, "workerd"),
c.req,
deviceId, deviceId,
Number(titleId), titleId,
watchStatus, watchStatus,
); );
if (wasAdded) {
await maybeScheduleNextAiringEpisode(
env<Env, typeof c>(c, "workerd"),
c.req,
titleId,
);
} else if (wasDeleted) {
await removeTask(
env<Env, typeof c>(c, "workerd"),
"new-episode",
buildNewEpisodeTaskId(titleId),
);
}
} catch (error) { } catch (error) {
console.error(new Error("Error setting watch status", { cause: error })); console.error(new Error("Error setting watch status", { cause: error }));
console.error(error); console.error(error);

View File

@@ -34,7 +34,7 @@ export async function fetchTitleFromAnilist(
if (error.message.includes("Not Found")) { if (error.message.includes("Not Found")) {
return undefined; return undefined;
} }
if (error.response.status === 429) { if (error.response?.status === 429) {
console.log( console.log(
"429, retrying in", "429, retrying in",
error.response.headers.get("Retry-After"), error.response.headers.get("Retry-After"),