feat: Centralize Anilist GraphQL queries, generalize Durable Object for multiple operations with caching, and add new controllers for search, popular titles, user data, and episode tracking.

This commit is contained in:
2025-11-29 05:03:57 -05:00
parent a25111acbf
commit b1e46ad6eb
10 changed files with 726 additions and 520 deletions

View File

@@ -1,5 +1,4 @@
import { graphql } from "gql.tada";
import { GraphQLClient } from "graphql-request";
import { env } from "cloudflare:workers";
import type { HonoRequest } from "hono";
import { DateTime } from "luxon";
@@ -7,38 +6,6 @@ import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEp
import { getValue, setValue } from "~/models/kv";
import { filterUnreleasedTitles } from "~/models/unreleasedTitles";
import type { Title } from "~/types/title";
import { MediaFragment } from "~/types/title/mediaFragment";
const GetUpcomingTitlesQuery = graphql(
`
query GetUpcomingTitles(
$page: Int!
$airingAtLowerBound: Int!
$airingAtUpperBound: Int!
) {
Page(page: $page) {
airingSchedules(
notYetAired: true
sort: TIME
airingAt_lesser: $airingAtUpperBound
airingAt_greater: $airingAtLowerBound
) {
id
airingAt
timeUntilAiring
episode
media {
...Media
}
}
pageInfo {
hasNextPage
}
}
}
`,
[MediaFragment],
);
type AiringSchedule = {
media: Title;
@@ -49,7 +16,9 @@ type AiringSchedule = {
};
export async function getUpcomingTitlesFromAnilist(req: HonoRequest) {
const client = new GraphQLClient("https://graphql.anilist.co/");
const durableObjectId = env.ANILIST_DO.idFromName("GLOBAL");
const stub = env.ANILIST_DO.get(durableObjectId);
const lastCheckedScheduleAt = await getValue("schedule_last_checked_at").then(
(value) => (value ? Number(value) : DateTime.now().toUnixInteger()),
);
@@ -61,21 +30,38 @@ export async function getUpcomingTitlesFromAnilist(req: HonoRequest) {
let shouldContinue = true;
do {
const { Page } = await client.request(GetUpcomingTitlesQuery, {
page: currentPage++,
airingAtLowerBound: lastCheckedScheduleAt,
airingAtUpperBound: twoDaysFromNow,
const response = await stub.fetch("http://anilist-do/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
operationName: "GetUpcomingTitles",
variables: {
page: currentPage++,
airingAtLowerBound: lastCheckedScheduleAt,
airingAtUpperBound: twoDaysFromNow,
},
}),
});
const { airingSchedules, pageInfo } = Page!;
if (!response.ok) {
// If failed, break loop or handle error. For now, break.
break;
}
const Page = (await response.json()) as any;
if (!Page) break;
const { airingSchedules, pageInfo } = Page;
plannedToWatchTitles = plannedToWatchTitles.union(
await filterUnreleasedTitles(
airingSchedules!.map((schedule) => schedule!.media?.id!),
airingSchedules!.map((schedule: any) => schedule!.media?.id!),
),
);
scheduleList = scheduleList.concat(
airingSchedules!.filter(
(schedule): schedule is AiringSchedule =>
(schedule: any): schedule is AiringSchedule =>
!!schedule &&
!plannedToWatchTitles.has(schedule.media?.id) &&
schedule.media?.countryOfOrigin === "JP" &&