feat: create route to be able to mark episode as watched
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
59
src/controllers/episodes/markEpisodeAsWatched/anilist.ts
Normal file
59
src/controllers/episodes/markEpisodeAsWatched/anilist.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
106
src/controllers/episodes/markEpisodeAsWatched/index.ts
Normal file
106
src/controllers/episodes/markEpisodeAsWatched/index.ts
Normal 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;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user