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,69 +1,46 @@
import { graphql } from "gql.tada";
import { GraphQLClient } from "graphql-request";
import { sleep } from "~/libs/sleep";
import { HomeTitleFragment } from "~/types/title/homeTitle";
const SearchQuery = graphql(
`
query Search($query: String!, $page: Int!, $limit: Int!) {
Page(page: $page, perPage: $limit) {
media(
search: $query
type: ANIME
sort: [POPULARITY_DESC, SCORE_DESC]
) {
...HomeTitle
}
pageInfo {
hasNextPage
}
}
}
`,
[HomeTitleFragment],
);
import { env } from "cloudflare:workers";
export async function fetchSearchResultsFromAnilist(
query: string,
page: number,
limit: number,
): Promise<SearchResultsResponse | undefined> {
const client = new GraphQLClient("https://graphql.anilist.co/");
const durableObjectId = env.ANILIST_DO.idFromName("GLOBAL");
const stub = env.ANILIST_DO.get(durableObjectId);
return client
.request(SearchQuery, { page, query, limit })
.then((data) => data?.Page)
.then((page) => {
if (!page || page.media?.length === 0) {
return undefined;
}
const response = await stub.fetch("http://anilist-do/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
operationName: "Search",
variables: { query, page, limit },
}),
});
if (!response.ok) {
return undefined;
}
const data = (await response.json()) as any;
if (!data || data.media?.length === 0) {
return undefined;
}
const { media: results, pageInfo } = data;
return {
results: results?.map((result: any) => {
if (!result) return null;
const { media: results, pageInfo } = page;
return {
results: results?.map((result) => {
if (!result) return null;
return {
id: result.id,
title: result.title?.userPreferred ?? result.title?.english,
coverImage: result.coverImage,
};
}),
hasNextPage: pageInfo?.hasNextPage,
id: result.id,
title: result.title?.userPreferred ?? result.title?.english,
coverImage: result.coverImage,
};
})
.catch((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(
() => fetchSearchResultsFromAnilist(query, page, limit),
);
}
throw err;
});
}),
hasNextPage: pageInfo?.hasNextPage,
};
}
type SearchResultsResponse = {