refactor: move getWatchingTitles to AniList Durable Object

introduces caching to that method
This commit is contained in:
2025-12-06 08:08:14 -05:00
parent c24ff62b30
commit 9b5cc7ea62
4 changed files with 71 additions and 148 deletions

View File

@@ -1,142 +0,0 @@
import { graphql } from "gql.tada";
import { GraphQLClient } from "graphql-request";
import { sleep } from "~/libs/sleep";
const GetWatchingTitlesQuery = graphql(`
query GetWatchingTitles($userName: String!, $page: Int!) {
Page(page: $page, perPage: 50) {
mediaList(
userName: $userName
type: ANIME
sort: UPDATED_TIME_DESC
status_in: [CURRENT, REPEATING, PLANNING]
) {
media {
id
idMal
title {
english
userPreferred
}
description
episodes
genres
status
bannerImage
averageScore
coverImage {
extraLarge
large
medium
}
countryOfOrigin
mediaListEntry {
id
progress
status
updatedAt
}
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
}
pageInfo {
currentPage
hasNextPage
perPage
total
}
}
}
`);
export function getWatchingTitles(
username: string,
page: number,
aniListToken: string,
): Promise<GetWatchingTitles> {
const client = new GraphQLClient("https://graphql.anilist.co/");
return client
.request(
GetWatchingTitlesQuery,
{ userName: username, page },
{ Authorization: `Bearer ${aniListToken}` },
)
.then((data) => data?.Page!)
.catch((err) => {
console.error("Failed to get watching titles");
console.error(err);
const response = err.response;
if (response.status === 429) {
console.log("429, retrying in", response.headers.get("Retry-After"));
return sleep(Number(response.headers.get("Retry-After")!) * 1000).then(
() => getWatchingTitles(username, page, aniListToken),
);
}
throw err;
});
}
type GetWatchingTitles = {
mediaList:
| ({
media: {
id: number;
idMal: number | null;
title: {
english: string | null;
userPreferred: string | null;
} | null;
description: string | null;
episodes: number | null;
genres: (string | null)[] | null;
status:
| "FINISHED"
| "RELEASING"
| "NOT_YET_RELEASED"
| "CANCELLED"
| "HIATUS"
| null;
bannerImage: string | null;
averageScore: number | null;
coverImage: {
extraLarge: string | null;
large: string | null;
medium: string | null;
} | null;
countryOfOrigin: unknown;
mediaListEntry: {
id: number;
progress: number | null;
status:
| "CURRENT"
| "REPEATING"
| "PLANNING"
| "COMPLETED"
| "DROPPED"
| "PAUSED"
| null;
updatedAt: number;
} | null;
nextAiringEpisode: {
timeUntilAiring: number;
airingAt: number;
episode: number;
} | null;
} | null;
} | null)[]
| null;
pageInfo: {
currentPage: number | null;
hasNextPage: boolean | null;
perPage: number | null;
total: number | null;
} | null;
};

View File

@@ -1,4 +1,5 @@
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { env } from "cloudflare:workers";
import { streamSSE } from "hono/streaming"; import { streamSSE } from "hono/streaming";
import { fetchEpisodes } from "~/controllers/episodes/getByAniListId"; import { fetchEpisodes } from "~/controllers/episodes/getByAniListId";
@@ -10,7 +11,6 @@ import { ErrorResponse, ErrorResponseSchema } from "~/types/schema";
import { Title } from "~/types/title"; import { Title } from "~/types/title";
import { getUser } from "./getUser"; import { getUser } from "./getUser";
import { getWatchingTitles } from "./getWatchingTitles";
const UserSchema = z.object({ const UserSchema = z.object({
name: z.string(), name: z.string(),
@@ -129,11 +129,15 @@ app.openapi(route, async (c) => {
let hasNextPage = true; let hasNextPage = true;
do { do {
const { mediaList, pageInfo } = await getWatchingTitles( const stub = env.ANILIST_DO.getByName(user.name!);
user.name!, const { mediaList, pageInfo } = await stub
currentPage++, .getTitles(
aniListToken, user.name!,
); currentPage,
["CURRENT", "PLANNING", "PAUSED", "REPEATING"],
aniListToken,
)
.then((data) => data!);
if (!mediaList) { if (!mediaList) {
break; break;
} }

View File

@@ -12,6 +12,7 @@ import {
GetUpcomingTitlesQuery, GetUpcomingTitlesQuery,
GetUserProfileQuery, GetUserProfileQuery,
GetUserQuery, GetUserQuery,
GetWatchingTitlesQuery,
MarkEpisodeAsWatchedMutation, MarkEpisodeAsWatchedMutation,
MarkTitleAsWatchedMutation, MarkTitleAsWatchedMutation,
NextSeasonPopularQuery, NextSeasonPopularQuery,
@@ -260,6 +261,33 @@ export class AnilistDurableObject extends DurableObject {
return data?.SaveMediaListEntry; return data?.SaveMediaListEntry;
} }
async getTitles(
userName: string,
page: number,
statusFilters: (
| "CURRENT"
| "COMPLETED"
| "PLANNING"
| "DROPPED"
| "PAUSED"
| "REPEATING"
)[],
aniListToken: string,
) {
return await this.handleCachedRequest(
`titles:${JSON.stringify({ page, statusFilters })}`,
async () => {
const data = await this.fetchFromAnilist(
GetWatchingTitlesQuery,
{ userName, page, statusFilters },
aniListToken,
);
return data?.Page;
},
60 * 60 * 1000,
);
}
// Helper to handle caching logic // Helper to handle caching logic
async handleCachedRequest( async handleCachedRequest(
key: string, key: string,

View File

@@ -266,3 +266,36 @@ export const NextSeasonPopularQuery = graphql(
`, `,
[HomeTitleFragment], [HomeTitleFragment],
); );
export const GetWatchingTitlesQuery = graphql(
`
query GetWatchingTitles(
$userName: String!
$page: Int!
$statusFilters: [MediaListStatus!]
) {
Page(page: $page, perPage: 50) {
mediaList(
userName: $userName
type: ANIME
sort: UPDATED_TIME_DESC
status_in: $statusFilters
) {
media {
...Media
mediaListEntry {
updatedAt
}
}
}
pageInfo {
currentPage
hasNextPage
perPage
total
}
}
}
`,
[MediaFragment],
);