feat: add user profile fetch in middleware
This commit is contained in:
@@ -2,6 +2,7 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
|||||||
|
|
||||||
import { fetchTitleFromAnilist } from "~/libs/anilist/getTitle";
|
import { fetchTitleFromAnilist } from "~/libs/anilist/getTitle";
|
||||||
import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources";
|
import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources";
|
||||||
|
import { userProfileMiddleware } from "~/middleware/userProfile";
|
||||||
import {
|
import {
|
||||||
AniListIdQuerySchema,
|
AniListIdQuerySchema,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
SuccessResponseSchema,
|
SuccessResponseSchema,
|
||||||
} from "~/types/schema";
|
} from "~/types/schema";
|
||||||
import { Title } from "~/types/title";
|
import { Title } from "~/types/title";
|
||||||
|
import type { User } from "~/types/user";
|
||||||
|
|
||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ const route = createRoute({
|
|||||||
description: "Title could not be found",
|
description: "Title could not be found",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
middleware: [userProfileMiddleware],
|
||||||
});
|
});
|
||||||
|
|
||||||
app.openapi(route, async (c) => {
|
app.openapi(route, async (c) => {
|
||||||
@@ -55,7 +58,12 @@ app.openapi(route, async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { result: title, errorOccurred } = await fetchFromMultipleSources([
|
const { result: title, errorOccurred } = await fetchFromMultipleSources([
|
||||||
() => fetchTitleFromAnilist(aniListId, aniListToken ?? undefined),
|
() =>
|
||||||
|
fetchTitleFromAnilist(
|
||||||
|
aniListId,
|
||||||
|
(c.get("user") as User)?.id,
|
||||||
|
aniListToken ?? undefined,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (errorOccurred) {
|
if (errorOccurred) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { OpenAPIHono } from "@hono/zod-openapi";
|
|||||||
import { Duration, type DurationLike } from "luxon";
|
import { Duration, type DurationLike } from "luxon";
|
||||||
|
|
||||||
import { onNewEpisode } from "~/controllers/internal/new-episode";
|
import { onNewEpisode } from "~/controllers/internal/new-episode";
|
||||||
import { maybeUpdateLastConnectedAt } from "~/controllers/maybeUpdateLastConnectedAt";
|
|
||||||
import { AnilistUpdateType } from "~/libs/anilist/updateType";
|
import { AnilistUpdateType } from "~/libs/anilist/updateType";
|
||||||
import { calculateExponentialBackoff } from "~/libs/calculateExponentialBackoff";
|
import { calculateExponentialBackoff } from "~/libs/calculateExponentialBackoff";
|
||||||
import type { QueueName } from "~/libs/tasks/queueName.ts";
|
import type { QueueName } from "~/libs/tasks/queueName.ts";
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
MAX_QUEUE_DELAY_SECONDS,
|
MAX_QUEUE_DELAY_SECONDS,
|
||||||
type QueueBody,
|
type QueueBody,
|
||||||
} from "~/libs/tasks/queueTask";
|
} from "~/libs/tasks/queueTask";
|
||||||
|
import { maybeUpdateLastConnectedAt } from "~/middleware/maybeUpdateLastConnectedAt";
|
||||||
|
|
||||||
export const app = new OpenAPIHono<{ Bindings: Env }>();
|
export const app = new OpenAPIHono<{ Bindings: Env }>();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
GetNextEpisodeAiringAtQuery,
|
GetNextEpisodeAiringAtQuery,
|
||||||
GetPopularTitlesQuery,
|
GetPopularTitlesQuery,
|
||||||
GetTitleQuery,
|
GetTitleQuery,
|
||||||
|
GetTitleUserDataQuery,
|
||||||
GetTrendingTitlesQuery,
|
GetTrendingTitlesQuery,
|
||||||
GetUpcomingTitlesQuery,
|
GetUpcomingTitlesQuery,
|
||||||
GetUserProfileQuery,
|
GetUserProfileQuery,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
SearchQuery,
|
SearchQuery,
|
||||||
} from "~/libs/anilist/queries";
|
} from "~/libs/anilist/queries";
|
||||||
import { sleep } from "~/libs/sleep.ts";
|
import { sleep } from "~/libs/sleep.ts";
|
||||||
|
import type { Title } from "~/types/title";
|
||||||
|
|
||||||
const nextAiringEpisodeSchema = z.nullable(
|
const nextAiringEpisodeSchema = z.nullable(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -38,30 +40,54 @@ export class AnilistDurableObject extends DurableObject {
|
|||||||
return new Response("Not found", { status: 404 });
|
return new Response("Not found", { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTitle(id: number, token?: string) {
|
async getTitle(
|
||||||
return this.handleCachedRequest(
|
id: number,
|
||||||
`title:${id}`,
|
userId?: string,
|
||||||
async () => {
|
token?: string,
|
||||||
const anilistResponse = await this.fetchFromAnilist(
|
): Promise<Title | null> {
|
||||||
GetTitleQuery,
|
const promises: Promise<any>[] = [
|
||||||
{ id },
|
this.handleCachedRequest(
|
||||||
token,
|
`title:${id}`,
|
||||||
);
|
async () => {
|
||||||
return anilistResponse?.Media ?? null;
|
const anilistResponse = await this.fetchFromAnilist(GetTitleQuery, {
|
||||||
},
|
id,
|
||||||
(media) => {
|
});
|
||||||
if (!media) return undefined;
|
return anilistResponse?.Media ?? null;
|
||||||
// Cast to any to access fragment fields without unmasking
|
},
|
||||||
const nextAiringEpisode = nextAiringEpisodeSchema.parse(
|
(media) => {
|
||||||
(media as any)?.nextAiringEpisode,
|
if (!media) return undefined;
|
||||||
);
|
|
||||||
const airingAt = (nextAiringEpisode?.airingAt ?? 0) * 1000;
|
// Cast to any to access fragment fields without unmasking
|
||||||
if (airingAt) {
|
const nextAiringEpisode = nextAiringEpisodeSchema.parse(
|
||||||
return airingAt - Date.now();
|
(media as any)?.nextAiringEpisode,
|
||||||
}
|
);
|
||||||
return undefined;
|
return nextAiringEpisode?.airingAt
|
||||||
},
|
? DateTime.fromMillis(nextAiringEpisode?.airingAt)
|
||||||
|
: undefined;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
promises.push(
|
||||||
|
userId
|
||||||
|
? this.handleCachedRequest(
|
||||||
|
`title:${id}:${userId}`,
|
||||||
|
async () => {
|
||||||
|
const anilistResponse = await this.fetchFromAnilist(
|
||||||
|
GetTitleUserDataQuery,
|
||||||
|
{ id },
|
||||||
|
{ token },
|
||||||
|
);
|
||||||
|
return anilistResponse?.Media ?? null;
|
||||||
|
},
|
||||||
|
DateTime.now().plus({ days: 1 }),
|
||||||
|
)
|
||||||
|
: Promise.resolve({ mediaListEntry: null }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return Promise.all(promises).then(([title, userTitle]) => ({
|
||||||
|
...title,
|
||||||
|
...userTitle,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextEpisodeAiringAt(id: number) {
|
async getNextEpisodeAiringAt(id: number) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Title } from "~/types/title";
|
|||||||
|
|
||||||
export async function fetchTitleFromAnilist(
|
export async function fetchTitleFromAnilist(
|
||||||
id: number,
|
id: number,
|
||||||
|
userId?: number | undefined,
|
||||||
token?: string | undefined,
|
token?: string | undefined,
|
||||||
): Promise<Title | undefined> {
|
): Promise<Title | undefined> {
|
||||||
if (useMockData()) {
|
if (useMockData()) {
|
||||||
@@ -17,8 +18,7 @@ export async function fetchTitleFromAnilist(
|
|||||||
);
|
);
|
||||||
const stub = env.ANILIST_DO.get(durableObjectId);
|
const stub = env.ANILIST_DO.get(durableObjectId);
|
||||||
|
|
||||||
const data = await stub.getTitle(id, token);
|
const data = await stub.getTitle(id, userId, token);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ export const GetTitleQuery = graphql(
|
|||||||
[MediaFragment],
|
[MediaFragment],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const GetTitleUserDataQuery = graphql(`
|
||||||
|
query GetTitleUserData($id: Int!) {
|
||||||
|
Media(id: $id) {
|
||||||
|
mediaListEntry {
|
||||||
|
id
|
||||||
|
progress
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
export const SearchQuery = graphql(
|
export const SearchQuery = graphql(
|
||||||
`
|
`
|
||||||
query Search($query: String!, $page: Int!, $limit: Int!) {
|
query Search($query: String!, $page: Int!, $limit: Int!) {
|
||||||
|
|||||||
25
src/middleware/userProfile.ts
Normal file
25
src/middleware/userProfile.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createMiddleware } from "hono/factory";
|
||||||
|
|
||||||
|
import type { User } from "~/types/user";
|
||||||
|
|
||||||
|
export const userProfileMiddleware = createMiddleware<
|
||||||
|
Cloudflare.Env & {
|
||||||
|
Variables: {
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
Bindings: Env;
|
||||||
|
}
|
||||||
|
>(async (c, next) => {
|
||||||
|
const aniListToken = await c.req.header("X-AniList-Token");
|
||||||
|
if (!aniListToken) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await c.env.ANILIST_DO.getByName("GLOBAL").getUser(aniListToken);
|
||||||
|
if (!user) {
|
||||||
|
return c.json({ error: "User not found" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.set("user", user);
|
||||||
|
return next();
|
||||||
|
});
|
||||||
@@ -21,11 +21,6 @@ export const MediaFragment = graphql(`
|
|||||||
medium
|
medium
|
||||||
}
|
}
|
||||||
countryOfOrigin
|
countryOfOrigin
|
||||||
mediaListEntry {
|
|
||||||
id
|
|
||||||
progress
|
|
||||||
status
|
|
||||||
}
|
|
||||||
nextAiringEpisode {
|
nextAiringEpisode {
|
||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
airingAt
|
airingAt
|
||||||
|
|||||||
@@ -3,20 +3,24 @@ import { z } from "zod";
|
|||||||
export type User = z.infer<typeof User>;
|
export type User = z.infer<typeof User>;
|
||||||
export const User = z
|
export const User = z
|
||||||
.object({
|
.object({
|
||||||
statistics: z.object({
|
|
||||||
minutesWatched: z.number().openapi({ type: "integer", format: "int64" }),
|
|
||||||
episodesWatched: z.number().openapi({ type: "integer", format: "int64" }),
|
|
||||||
count: z
|
|
||||||
.number()
|
|
||||||
.int() /* .openapi({ type: "integer", format: "int64" }) */,
|
|
||||||
meanScore: z.number().openapi({ type: "number", format: "float" }),
|
|
||||||
}),
|
|
||||||
id: z.number().openapi({ type: "integer", format: "int64" }),
|
id: z.number().openapi({ type: "integer", format: "int64" }),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
avatar: z.object({
|
|
||||||
medium: z.string(),
|
|
||||||
large: z.string(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.nullable();
|
.nullable();
|
||||||
|
|
||||||
|
export type UserProfile = z.infer<typeof UserProfile>;
|
||||||
|
export const UserProfile = z.object({
|
||||||
|
statistics: z.object({
|
||||||
|
minutesWatched: z.number().openapi({ type: "integer", format: "int64" }),
|
||||||
|
episodesWatched: z.number().openapi({ type: "integer", format: "int64" }),
|
||||||
|
count: z.number().int(),
|
||||||
|
meanScore: z.number().openapi({ type: "number", format: "float" }),
|
||||||
|
}),
|
||||||
|
id: z.number().openapi({ type: "integer", format: "int64" }),
|
||||||
|
name: z.string(),
|
||||||
|
avatar: z.object({
|
||||||
|
medium: z.string(),
|
||||||
|
large: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user