feat: migrate to cloudflare d1 and queues
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
.mf
|
||||||
.wrangler
|
.wrangler
|
||||||
.dev.vars
|
.dev.vars
|
||||||
*.db
|
*.db
|
||||||
*.db-*
|
*.db-*
|
||||||
.env
|
.env
|
||||||
|
src/libs/anilist/anilist-do.ts
|
||||||
|
.idea/ChatHistory_schema_v3.xml
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
import { config } from "dotenv";
|
||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
config({ path: ".dev.vars" });
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: "./src/models/schema.ts",
|
schema: "./src/models/schema.ts",
|
||||||
out: "./drizzle",
|
out: "./drizzle",
|
||||||
driver: "turso",
|
driver: "d1-http",
|
||||||
dialect: "sqlite",
|
dialect: "sqlite",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.TURSO_URL,
|
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
|
||||||
authToken: process.env.TURSO_AUTH_TOKEN,
|
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
|
||||||
|
token: process.env.CLOUDFLARE_D1_TOKEN!,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
"@hono/swagger-ui": "^0.5.1",
|
"@hono/swagger-ui": "^0.5.1",
|
||||||
"@hono/zod-openapi": "^0.19.5",
|
"@hono/zod-openapi": "^0.19.5",
|
||||||
"@hono/zod-validator": "^0.2.2",
|
"@hono/zod-validator": "^0.2.2",
|
||||||
"@libsql/client": "0.15.4",
|
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"gql.tada": "^1.8.10",
|
"gql.tada": "^1.8.10",
|
||||||
"graphql": "^16.12.0",
|
"graphql": "^16.12.0",
|
||||||
|
|||||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -22,9 +22,6 @@ importers:
|
|||||||
"@hono/zod-validator":
|
"@hono/zod-validator":
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.2(hono@4.10.4)(zod@3.25.76)
|
version: 0.2.2(hono@4.10.4)(zod@3.25.76)
|
||||||
"@libsql/client":
|
|
||||||
specifier: 0.15.4
|
|
||||||
version: 0.15.4
|
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.44.7
|
specifier: ^0.44.7
|
||||||
version: 0.44.7(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2))
|
version: 0.44.7(@cloudflare/workers-types@4.20251014.0)(@libsql/client@0.15.4)(bun-types@1.3.1(@types/react@19.2.2))
|
||||||
@@ -4997,10 +4994,12 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
optional: true
|
||||||
|
|
||||||
"@libsql/core@0.15.15":
|
"@libsql/core@0.15.15":
|
||||||
dependencies:
|
dependencies:
|
||||||
js-base64: 3.7.8
|
js-base64: 3.7.8
|
||||||
|
optional: true
|
||||||
|
|
||||||
"@libsql/darwin-arm64@0.5.22":
|
"@libsql/darwin-arm64@0.5.22":
|
||||||
optional: true
|
optional: true
|
||||||
@@ -5017,8 +5016,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
optional: true
|
||||||
|
|
||||||
"@libsql/isomorphic-fetch@0.3.1": {}
|
"@libsql/isomorphic-fetch@0.3.1":
|
||||||
|
optional: true
|
||||||
|
|
||||||
"@libsql/isomorphic-ws@0.1.5":
|
"@libsql/isomorphic-ws@0.1.5":
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5027,6 +5028,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
optional: true
|
||||||
|
|
||||||
"@libsql/linux-arm-gnueabihf@0.5.22":
|
"@libsql/linux-arm-gnueabihf@0.5.22":
|
||||||
optional: true
|
optional: true
|
||||||
@@ -5058,7 +5060,8 @@ snapshots:
|
|||||||
outvariant: 1.4.3
|
outvariant: 1.4.3
|
||||||
strict-event-emitter: 0.5.1
|
strict-event-emitter: 0.5.1
|
||||||
|
|
||||||
"@neon-rs/load@0.0.4": {}
|
"@neon-rs/load@0.0.4":
|
||||||
|
optional: true
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5262,6 +5265,7 @@ snapshots:
|
|||||||
"@types/ws@8.18.1":
|
"@types/ws@8.18.1":
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node": 24.9.2
|
"@types/node": 24.9.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
"@vitest/expect@3.2.4":
|
"@vitest/expect@3.2.4":
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5520,7 +5524,8 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.2.2: {}
|
csstype@3.2.2: {}
|
||||||
|
|
||||||
data-uri-to-buffer@4.0.1: {}
|
data-uri-to-buffer@4.0.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5538,7 +5543,8 @@ snapshots:
|
|||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
detect-libc@2.0.2: {}
|
detect-libc@2.0.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
@@ -5762,6 +5768,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
node-domexception: 1.0.0
|
node-domexception: 1.0.0
|
||||||
web-streams-polyfill: 3.3.3
|
web-streams-polyfill: 3.3.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5791,6 +5798,7 @@ snapshots:
|
|||||||
formdata-polyfill@4.0.10:
|
formdata-polyfill@4.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
fetch-blob: 3.2.0
|
fetch-blob: 3.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
@@ -5979,7 +5987,8 @@ snapshots:
|
|||||||
|
|
||||||
jose@5.10.0: {}
|
jose@5.10.0: {}
|
||||||
|
|
||||||
js-base64@3.7.8: {}
|
js-base64@3.7.8:
|
||||||
|
optional: true
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
@@ -6016,6 +6025,7 @@ snapshots:
|
|||||||
"@libsql/linux-x64-gnu": 0.5.22
|
"@libsql/linux-x64-gnu": 0.5.22
|
||||||
"@libsql/linux-x64-musl": 0.5.22
|
"@libsql/linux-x64-musl": 0.5.22
|
||||||
"@libsql/win32-x64-msvc": 0.5.22
|
"@libsql/win32-x64-msvc": 0.5.22
|
||||||
|
optional: true
|
||||||
|
|
||||||
lilconfig@3.1.3: {}
|
lilconfig@3.1.3: {}
|
||||||
|
|
||||||
@@ -6169,6 +6179,7 @@ snapshots:
|
|||||||
data-uri-to-buffer: 4.0.1
|
data-uri-to-buffer: 4.0.1
|
||||||
fetch-blob: 3.2.0
|
fetch-blob: 3.2.0
|
||||||
formdata-polyfill: 4.0.10
|
formdata-polyfill: 4.0.10
|
||||||
|
optional: true
|
||||||
|
|
||||||
npm-run-path@5.3.0:
|
npm-run-path@5.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6242,7 +6253,8 @@ snapshots:
|
|||||||
|
|
||||||
prettier@3.6.2: {}
|
prettier@3.6.2: {}
|
||||||
|
|
||||||
promise-limit@2.7.0: {}
|
promise-limit@2.7.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
psl@1.15.0:
|
psl@1.15.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6595,7 +6607,8 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3: {}
|
web-streams-polyfill@3.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
web-streams-polyfill@4.0.0-beta.3: {}
|
web-streams-polyfill@4.0.0-beta.3: {}
|
||||||
|
|
||||||
@@ -6701,7 +6714,8 @@ snapshots:
|
|||||||
|
|
||||||
ws@8.18.0: {}
|
ws@8.18.0: {}
|
||||||
|
|
||||||
ws@8.18.3: {}
|
ws@8.18.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ app.openapi(route, async (c) => {
|
|||||||
mediaListEntry.status,
|
mediaListEntry.status,
|
||||||
);
|
);
|
||||||
if (wasAdded) {
|
if (wasAdded) {
|
||||||
await maybeScheduleNextAiringEpisode(c.req, media.id);
|
await maybeScheduleNextAiringEpisode(media.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { zValidator } from "@hono/zod-validator";
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getEpisodesFromAniwatch } from "~/controllers/episodes/getByAniListId/aniwatch";
|
||||||
import { fetchEpisodeUrl } from "~/controllers/episodes/getEpisodeUrl";
|
import { fetchEpisodeUrl } from "~/controllers/episodes/getEpisodeUrl";
|
||||||
import { getAdminSdkCredentials } from "~/libs/gcloud/getAdminSdkCredentials";
|
import { getAdminSdkCredentials } from "~/libs/gcloud/getAdminSdkCredentials";
|
||||||
import { sendFcmMessage } from "~/libs/gcloud/sendFcmMessage";
|
import { sendFcmMessage } from "~/libs/gcloud/sendFcmMessage";
|
||||||
@@ -16,39 +17,21 @@ import {
|
|||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
app.post(
|
export async function onNewEpisode(aniListId: number, episodeNumber: number) {
|
||||||
"/",
|
|
||||||
zValidator(
|
|
||||||
"json",
|
|
||||||
z.object({
|
|
||||||
aniListId: AniListIdSchema,
|
|
||||||
episodeNumber: EpisodeNumberSchema,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
async (c) => {
|
|
||||||
const { aniListId, episodeNumber } = await c.req.json<{
|
|
||||||
aniListId: number;
|
|
||||||
episodeNumber: number;
|
|
||||||
}>();
|
|
||||||
console.log(
|
console.log(
|
||||||
`Internal new episode route, aniListId: ${aniListId}, episodeNumber: ${episodeNumber}`,
|
`On new episode, aniListId: ${aniListId}, episodeNumber: ${episodeNumber}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!(await isWatchingTitle(aniListId))) {
|
if (!(await isWatchingTitle(aniListId))) {
|
||||||
console.log(`Title ${aniListId} is no longer being watched`);
|
console.log(`Title ${aniListId} is no longer being watched`);
|
||||||
return c.json(
|
return { success: true, result: { isNoLongerWatching: true } };
|
||||||
{ success: true, result: { isNoLongerWatching: true } },
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const episodes = await getEpisodesFromAniwatch(aniListId);
|
||||||
const fetchUrlResult = await fetchEpisodeUrl({ aniListId, episodeNumber });
|
const fetchUrlResult = await fetchEpisodeUrl({ aniListId, episodeNumber });
|
||||||
if (!fetchUrlResult) {
|
if (!fetchUrlResult) {
|
||||||
console.error(`Failed to fetch episode URL for episode`);
|
console.error(`Failed to fetch episode URL for episode`);
|
||||||
return c.json(
|
return { success: false, message: "Failed to fetch episode URL" };
|
||||||
{ success: false, message: "Failed to fetch episode URL" },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = await getTokensSubscribedToTitle(aniListId);
|
const tokens = await getTokensSubscribedToTitle(aniListId);
|
||||||
@@ -69,9 +52,32 @@ app.post(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await maybeScheduleNextAiringEpisode(c.req, aniListId);
|
await maybeScheduleNextAiringEpisode(aniListId);
|
||||||
|
|
||||||
return c.json(SuccessResponse, 200);
|
return SuccessResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/",
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
aniListId: AniListIdSchema,
|
||||||
|
episodeNumber: EpisodeNumberSchema,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const { aniListId, episodeNumber } = await c.req.json<{
|
||||||
|
aniListId: number;
|
||||||
|
episodeNumber: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const result = await onNewEpisode(aniListId, episodeNumber, c.req);
|
||||||
|
if (result.success) {
|
||||||
|
return c.json(result, 200);
|
||||||
|
} else {
|
||||||
|
return c.json(result, 500);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export async function getUpcomingTitlesFromAnilist(req: HonoRequest) {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(plannedToWatchTitles).map((titleId) =>
|
Array.from(plannedToWatchTitles).map((titleId) =>
|
||||||
maybeScheduleNextAiringEpisode(req, titleId),
|
maybeScheduleNextAiringEpisode(titleId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export const maybeUpdateLastConnectedAt = createMiddleware(async (c, next) => {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateDeviceLastConnectedAt(c.env, deviceId);
|
await updateDeviceLastConnectedAt(deviceId);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
||||||
import type { HonoRequest } from "hono";
|
import type { HonoRequest } from "hono";
|
||||||
|
|
||||||
|
import { AnilistUpdateType } from "~/libs/anilist/updateType.ts";
|
||||||
import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode";
|
import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode";
|
||||||
import { buildNewEpisodeTaskId } from "~/libs/tasks/id";
|
import { buildNewEpisodeTaskId } from "~/libs/tasks/id";
|
||||||
import { queueTask } from "~/libs/tasks/queueTask";
|
import { queueTask } from "~/libs/tasks/queueTask";
|
||||||
@@ -76,9 +77,9 @@ export async function updateWatchStatus(
|
|||||||
watchStatus,
|
watchStatus,
|
||||||
);
|
);
|
||||||
if (wasAdded) {
|
if (wasAdded) {
|
||||||
await maybeScheduleNextAiringEpisode(req, titleId);
|
await maybeScheduleNextAiringEpisode(titleId);
|
||||||
} else if (wasDeleted) {
|
} else if (wasDeleted) {
|
||||||
await removeTask("new-episode", buildNewEpisodeTaskId(titleId));
|
await removeTask("NEW_EPISODE", buildNewEpisodeTaskId(titleId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,13 +116,12 @@ app.openapi(route, async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await queueTask(
|
await queueTask(
|
||||||
"anilist-updates",
|
"ANILIST_UPDATES",
|
||||||
{
|
{
|
||||||
deviceId,
|
deviceId,
|
||||||
watchStatus,
|
watchStatus,
|
||||||
titleId,
|
titleId,
|
||||||
isRetrying: true,
|
updateType: AnilistUpdateType.UpdateWatchStatus,
|
||||||
nameSuffix: "watch-status",
|
|
||||||
},
|
},
|
||||||
{ req: c.req, scheduleConfig: { delay: { minute: 1 } } },
|
{ req: c.req, scheduleConfig: { delay: { minute: 1 } } },
|
||||||
);
|
);
|
||||||
|
|||||||
25
src/index.ts
25
src/index.ts
@@ -2,6 +2,10 @@ import { swaggerUI } from "@hono/swagger-ui";
|
|||||||
import { OpenAPIHono } from "@hono/zod-openapi";
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
||||||
|
|
||||||
import { maybeUpdateLastConnectedAt } from "~/controllers/maybeUpdateLastConnectedAt";
|
import { maybeUpdateLastConnectedAt } from "~/controllers/maybeUpdateLastConnectedAt";
|
||||||
|
import type { QueueName } from "~/libs/tasks/queueName.ts";
|
||||||
|
|
||||||
|
import { onNewEpisode } from "./controllers/internal/new-episode";
|
||||||
|
import type { QueueBody } from "./libs/tasks/queueTask";
|
||||||
|
|
||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
@@ -65,6 +69,25 @@ app.doc("/openapi.json", {
|
|||||||
|
|
||||||
app.get("/docs", swaggerUI({ url: "/openapi.json" }));
|
app.get("/docs", swaggerUI({ url: "/openapi.json" }));
|
||||||
|
|
||||||
export default app;
|
export default {
|
||||||
|
...app,
|
||||||
|
async queue(batch) {
|
||||||
|
switch (batch.queue as QueueName) {
|
||||||
|
case "ANILIST_UPDATES":
|
||||||
|
batch.retryAll();
|
||||||
|
break;
|
||||||
|
case "NEW_EPISODE":
|
||||||
|
for (const message of (batch as MessageBatch<QueueBody["NEW_EPISODE"]>)
|
||||||
|
.messages) {
|
||||||
|
await onNewEpisode(
|
||||||
|
message.body.aniListId,
|
||||||
|
message.body.episodeNumber,
|
||||||
|
);
|
||||||
|
message.ack();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} satisfies ExportedHandler<Env>;
|
||||||
|
|
||||||
export { AnilistDurableObject as AnilistDo } from "~/libs/anilist/anilist-do.ts";
|
export { AnilistDurableObject as AnilistDo } from "~/libs/anilist/anilist-do.ts";
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
import { DurableObject, env } from "cloudflare:workers";
|
import { DurableObject, env } from "cloudflare:workers";
|
||||||
import { GraphQLClient } from "graphql-request";
|
import { graphql, type ResultOf } from "gql.tada";
|
||||||
|
import { print } from "graphql";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { GetTitleQuery, _fetchTitleFromAnilist } from "~/libs/anilist/getTitle";
|
|
||||||
import { sleep } from "~/libs/sleep.ts";
|
import { sleep } from "~/libs/sleep.ts";
|
||||||
import type { Title } from "~/types/title";
|
import type { Title } from "~/types/title";
|
||||||
|
import { MediaFragment } from "~/types/title/mediaFragment";
|
||||||
|
|
||||||
|
const GetTitleQuery = graphql(
|
||||||
|
`
|
||||||
|
query GetTitle($id: Int!) {
|
||||||
|
Media(id: $id) {
|
||||||
|
...Media
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
[MediaFragment],
|
||||||
|
);
|
||||||
|
|
||||||
const nextAiringEpisodeSchema = z.nullable(
|
const nextAiringEpisodeSchema = z.nullable(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -29,12 +41,13 @@ export class AnilistDurableObject extends DurableObject {
|
|||||||
switch (operationName) {
|
switch (operationName) {
|
||||||
case "GetTitle": {
|
case "GetTitle": {
|
||||||
const { variables } = body;
|
const { variables } = body;
|
||||||
const cache = await this.state.storage.get(variables.id);
|
const storageKey = variables.id;
|
||||||
if (cache) {
|
const cache = await this.state.storage.get(storageKey);
|
||||||
return new Response(JSON.stringify(cache), {
|
// if (cache) {
|
||||||
headers: { "Content-Type": "application/json" },
|
// return new Response(JSON.stringify(cache), {
|
||||||
});
|
// headers: { "Content-Type": "application/json" },
|
||||||
}
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
const anilistResponse = await this.fetchTitleFromAnilist(
|
const anilistResponse = await this.fetchTitleFromAnilist(
|
||||||
variables.id,
|
variables.id,
|
||||||
@@ -45,7 +58,7 @@ export class AnilistDurableObject extends DurableObject {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const airingAt = (nextAiringEpisode?.airingAt ?? 0) * 1000;
|
const airingAt = (nextAiringEpisode?.airingAt ?? 0) * 1000;
|
||||||
await this.state.storage.put(variables.id, anilistResponse);
|
await this.state.storage.put(storageKey, anilistResponse);
|
||||||
if (airingAt) {
|
if (airingAt) {
|
||||||
await this.state.storage.setAlarm(airingAt);
|
await this.state.storage.setAlarm(airingAt);
|
||||||
await this.state.storage.put(`alarm:${variables.id}`, airingAt);
|
await this.state.storage.put(`alarm:${variables.id}`, airingAt);
|
||||||
@@ -63,7 +76,6 @@ export class AnilistDurableObject extends DurableObject {
|
|||||||
async alarm() {
|
async alarm() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const alarms = await this.state.storage.list({ prefix: "alarm:" });
|
const alarms = await this.state.storage.list({ prefix: "alarm:" });
|
||||||
console.log("alarm", now, alarms);
|
|
||||||
for (const [id, ttl] of Object.entries(alarms)) {
|
for (const [id, ttl] of Object.entries(alarms)) {
|
||||||
if (now >= ttl) {
|
if (now >= ttl) {
|
||||||
await this.state.storage.delete(id);
|
await this.state.storage.delete(id);
|
||||||
@@ -75,30 +87,70 @@ export class AnilistDurableObject extends DurableObject {
|
|||||||
id: number,
|
id: number,
|
||||||
token?: string | undefined,
|
token?: string | undefined,
|
||||||
): Promise<Title | undefined> {
|
): Promise<Title | undefined> {
|
||||||
const client = new GraphQLClient("https://graphql.anilist.co/");
|
const headers: any = {
|
||||||
const headers = new Headers();
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
headers.append("Authorization", `Bearer ${token}`);
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return client
|
const response = await fetch("http://localhost:3000/proxy", {
|
||||||
.request(GetTitleQuery, { id }, headers)
|
method: "POST",
|
||||||
.then((data) => data?.Media ?? undefined)
|
headers: {
|
||||||
.catch((error) => {
|
"Content-Type": "application/json",
|
||||||
if (error.message.includes("Not Found")) {
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: "https://graphql.anilist.co/",
|
||||||
|
method: "POST",
|
||||||
|
headers, // Pass the original headers here
|
||||||
|
data: {
|
||||||
|
operationName: "GetTitle",
|
||||||
|
query: print(GetTitleQuery),
|
||||||
|
variables: { id },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Handle Rate Limiting (429)
|
||||||
|
if (response.status === 429) {
|
||||||
|
const retryAfter = response.headers.get("Retry-After");
|
||||||
|
console.log("429, retrying in", retryAfter);
|
||||||
|
|
||||||
|
await sleep(Number(retryAfter || 1) * 1000); // specific fallback or ensure logic
|
||||||
|
return this.fetchTitleFromAnilist(id, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Handle HTTP Errors (like 404 or 500)
|
||||||
|
if (!response.ok) {
|
||||||
|
// If it is specifically a 404 Not Found HTTP status
|
||||||
|
if (response.status === 404) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (error.response?.status === 429) {
|
|
||||||
console.log(
|
console.error(JSON.stringify(await response.json(), null, 2));
|
||||||
"429, retrying in",
|
// Throw for other HTTP errors to be caught by caller
|
||||||
error.response.headers.get("Retry-After"),
|
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
||||||
);
|
|
||||||
return sleep(
|
|
||||||
Number(error.response.headers.get("Retry-After")!) * 1000,
|
|
||||||
).then(() => this.fetchTitleFromAnilist(id, token));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
// 3. Parse JSON
|
||||||
});
|
// We cast this to ResultOf<typeof GetTitleQuery> to maintain Tada type safety
|
||||||
|
const result = (await response.json().then((json) => json.data)) as {
|
||||||
|
data?: ResultOf<typeof GetTitleQuery>;
|
||||||
|
errors?: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Handle GraphQL Specific Errors (Anilist might return 200 OK but include errors)
|
||||||
|
if (result.errors && result.errors.length > 0) {
|
||||||
|
const errorMessage = JSON.stringify(result.errors);
|
||||||
|
|
||||||
|
if (errorMessage.includes("Not Found")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`GraphQL Error: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data?.Media ?? undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
import { env } from "cloudflare:workers";
|
import { env } from "cloudflare:workers";
|
||||||
import { graphql } from "gql.tada";
|
|
||||||
|
|
||||||
import type { Title } from "~/types/title";
|
import type { Title } from "~/types/title";
|
||||||
import { MediaFragment } from "~/types/title/mediaFragment";
|
|
||||||
|
|
||||||
export const GetTitleQuery = graphql(
|
|
||||||
`
|
|
||||||
query GetTitle($id: Int!) {
|
|
||||||
Media(id: $id) {
|
|
||||||
...Media
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
[MediaFragment],
|
|
||||||
);
|
|
||||||
|
|
||||||
export async function fetchTitleFromAnilist(
|
export async function fetchTitleFromAnilist(
|
||||||
id: number,
|
id: number,
|
||||||
|
|||||||
3
src/libs/anilist/updateType.ts
Normal file
3
src/libs/anilist/updateType.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export enum AnilistUpdateType {
|
||||||
|
UpdateWatchStatus,
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { HonoRequest } from "hono";
|
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -7,18 +6,9 @@ import {
|
|||||||
} from "~/models/unreleasedTitles";
|
} from "~/models/unreleasedTitles";
|
||||||
|
|
||||||
import { getNextEpisodeTimeUntilAiring } from "./anilist/getNextEpisodeAiringAt";
|
import { getNextEpisodeTimeUntilAiring } from "./anilist/getNextEpisodeAiringAt";
|
||||||
import { getCurrentDomain } from "./getCurrentDomain";
|
|
||||||
import { queueTask } from "./tasks/queueTask";
|
import { queueTask } from "./tasks/queueTask";
|
||||||
|
|
||||||
export async function maybeScheduleNextAiringEpisode(
|
export async function maybeScheduleNextAiringEpisode(aniListId: number) {
|
||||||
req: HonoRequest,
|
|
||||||
aniListId: number,
|
|
||||||
) {
|
|
||||||
const domain = getCurrentDomain(req);
|
|
||||||
if (!domain) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { nextAiring, status } = await getNextEpisodeTimeUntilAiring(aniListId);
|
const { nextAiring, status } = await getNextEpisodeTimeUntilAiring(aniListId);
|
||||||
if (
|
if (
|
||||||
!nextAiring ||
|
!nextAiring ||
|
||||||
@@ -33,9 +23,9 @@ export async function maybeScheduleNextAiringEpisode(
|
|||||||
|
|
||||||
const { airingAt, episode: nextEpisode } = nextAiring;
|
const { airingAt, episode: nextEpisode } = nextAiring;
|
||||||
await queueTask(
|
await queueTask(
|
||||||
"new-episode",
|
"NEW_EPISODE",
|
||||||
{ aniListId, episodeNumber: nextEpisode },
|
{ aniListId, episodeNumber: nextEpisode },
|
||||||
{ req, scheduleConfig: { epochTime: airingAt } },
|
{ scheduleConfig: { epochTime: airingAt } },
|
||||||
);
|
);
|
||||||
await removeUnreleasedTitle(aniListId);
|
await removeUnreleasedTitle(aniListId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
export type QueueName = "anilist-updates" | "new-episode";
|
type QueueKeys<T> = {
|
||||||
|
[K in keyof T]: T[K] extends Queue ? K : never;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
export type QueueName = QueueKeys<Cloudflare.Env>;
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
import { env as cloudflareEnv } from "cloudflare:workers";
|
import { env as cloudflareEnv } from "cloudflare:workers";
|
||||||
import type { HonoRequest } from "hono";
|
import type { HonoRequest } from "hono";
|
||||||
import isEqual from "lodash.isequal";
|
import { DateTime, Duration, type DurationLike } from "luxon";
|
||||||
import { DateTime, type DurationLike } from "luxon";
|
|
||||||
|
|
||||||
|
import { AnilistUpdateType } from "~/libs/anilist/updateType.ts";
|
||||||
import type { WatchStatus } from "~/types/title/watchStatus";
|
import type { WatchStatus } from "~/types/title/watchStatus";
|
||||||
|
|
||||||
import { FailedToQueueTaskError } from "../errors/FailedToQueueTask";
|
|
||||||
import { getAdminSdkCredentials } from "../gcloud/getAdminSdkCredentials";
|
|
||||||
import { getGoogleAuthToken } from "../gcloud/getGoogleAuthToken";
|
|
||||||
import { getCurrentDomain } from "../getCurrentDomain";
|
|
||||||
import { buildAnilistRetryTaskId, buildNewEpisodeTaskId } from "./id";
|
|
||||||
import type { QueueName } from "./queueName";
|
import type { QueueName } from "./queueName";
|
||||||
|
|
||||||
type QueueBody = {
|
export type QueueBody = {
|
||||||
"anilist-updates": {
|
ANILIST_UPDATES: {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
watchStatus: WatchStatus | null;
|
watchStatus: WatchStatus | null;
|
||||||
titleId: number;
|
titleId: number;
|
||||||
isRetrying: true;
|
updateType: AnilistUpdateType;
|
||||||
nameSuffix: string;
|
|
||||||
};
|
};
|
||||||
"new-episode": { aniListId: number; episodeNumber: number };
|
NEW_EPISODE: { aniListId: number; episodeNumber: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
type ScheduleConfig =
|
type ScheduleConfig =
|
||||||
@@ -39,164 +33,76 @@ export async function queueTask(
|
|||||||
body: QueueBody[QueueName],
|
body: QueueBody[QueueName],
|
||||||
{ scheduleConfig, req, env = cloudflareEnv }: QueueTaskOptionalArgs = {},
|
{ scheduleConfig, req, env = cloudflareEnv }: QueueTaskOptionalArgs = {},
|
||||||
) {
|
) {
|
||||||
const domain = req
|
const { scheduleTime, headers } = buildTask(
|
||||||
? getCurrentDomain(req)
|
|
||||||
: "https://aniplay-v2.rururu.workers.dev";
|
|
||||||
if (!domain) {
|
|
||||||
console.log("Skipping queue task due to local domain", queueName, body);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminSdkCredentials = getAdminSdkCredentials(env);
|
|
||||||
const { projectId } = adminSdkCredentials;
|
|
||||||
|
|
||||||
const task = buildTask(
|
|
||||||
projectId,
|
|
||||||
queueName,
|
queueName,
|
||||||
scheduleConfig,
|
scheduleConfig,
|
||||||
domain,
|
|
||||||
body,
|
body,
|
||||||
req?.header(),
|
req?.header(),
|
||||||
);
|
);
|
||||||
const { res } = await queueCloudTask(task);
|
const contentType =
|
||||||
|
headers["Content-Type"] === "application/json" ? "json" : "text";
|
||||||
if (!res.ok) {
|
if (!env) {
|
||||||
if (res.status === 409) {
|
const Cloudflare = await import("cloudflare").then(
|
||||||
if (
|
({ Cloudflare }) => Cloudflare,
|
||||||
await checkIfTaskExists(
|
|
||||||
env,
|
|
||||||
queueName,
|
|
||||||
task.name.split("/").at(-1)!,
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const hashedTaskName = await import("node:crypto").then(
|
|
||||||
({ createHash }) =>
|
|
||||||
createHash("sha256")
|
|
||||||
.update(task.name.split("/").at(-1)!)
|
|
||||||
.digest("hex"),
|
|
||||||
);
|
);
|
||||||
console.log("task name", hashedTaskName);
|
const client = new Cloudflare({ apiToken: env.CLOUDFLARE_TOKEN });
|
||||||
const { res } = await queueCloudTask({
|
let queueId: string | null = null;
|
||||||
...task,
|
const queues = await client.queues.list({
|
||||||
name:
|
account_id: env.CLOUDFLARE_ACCOUNT_ID,
|
||||||
task.name.split("/").slice(0, -1).join("/") + "/" + hashedTaskName,
|
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
for await (const queue of queues) {
|
||||||
if (await checkIfTaskExists(env, queueName, hashedTaskName, body)) {
|
if (queueId == queue.queue_name) {
|
||||||
return;
|
queueId = queue.queue_id!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!queueId) {
|
||||||
|
throw new Error(`Queue ${queueName} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new FailedToQueueTaskError(res.status, await res.text());
|
await client.queues.messages.push(queueId, {
|
||||||
}
|
body: { body, headers },
|
||||||
}
|
content_type: contentType,
|
||||||
}
|
delay_seconds: scheduleTime,
|
||||||
|
account_id: env.CLOUDFLARE_ACCOUNT_ID,
|
||||||
throw new FailedToQueueTaskError(res.status, await res.text());
|
});
|
||||||
}
|
} else {
|
||||||
|
await env[queueName].send(
|
||||||
async function queueCloudTask(task: object) {
|
{ body, headers },
|
||||||
const res = await fetch(
|
|
||||||
`https://content-cloudtasks.googleapis.com/v2/projects/${projectId}/locations/northamerica-northeast1/queues/${queueName}/tasks?alt=json`,
|
|
||||||
{
|
{
|
||||||
headers: {
|
contentType,
|
||||||
Authorization: `Bearer ${await getGoogleAuthToken(adminSdkCredentials)}`,
|
delaySeconds: scheduleTime,
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ task }),
|
|
||||||
method: "POST",
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return { res };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkIfTaskExists(
|
|
||||||
env: Cloudflare.Env,
|
|
||||||
queueName: QueueName,
|
|
||||||
taskId: string,
|
|
||||||
expectedBody: QueueBody[QueueName],
|
|
||||||
) {
|
|
||||||
const adminSdkCredentials = getAdminSdkCredentials(env);
|
|
||||||
|
|
||||||
const body = await fetch(
|
|
||||||
`https://content-cloudtasks.googleapis.com/v2/projects/${adminSdkCredentials.projectId}/locations/northamerica-northeast1/queues/${queueName}/tasks/${taskId}?responseView=FULL`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${await getGoogleAuthToken(adminSdkCredentials)}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then(({ httpRequest }) => httpRequest?.body);
|
|
||||||
|
|
||||||
return (
|
|
||||||
body &&
|
|
||||||
isEqual(
|
|
||||||
JSON.parse(Buffer.from(body as string, "base64").toString()),
|
|
||||||
expectedBody,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTask(
|
function buildTask(
|
||||||
projectId: string,
|
|
||||||
queueName: QueueName,
|
queueName: QueueName,
|
||||||
scheduleConfig: ScheduleConfig | undefined,
|
scheduleConfig: ScheduleConfig | undefined,
|
||||||
domain: string,
|
|
||||||
body: QueueBody[QueueName],
|
body: QueueBody[QueueName],
|
||||||
headers: Record<string, string> | undefined,
|
headers: Record<string, string> | undefined,
|
||||||
) {
|
) {
|
||||||
let scheduleTime: string | undefined;
|
let scheduleTime: number = 0;
|
||||||
if (scheduleConfig) {
|
if (scheduleConfig) {
|
||||||
const { delay, epochTime } = scheduleConfig;
|
const { delay, epochTime } = scheduleConfig;
|
||||||
if (epochTime) {
|
if (epochTime) {
|
||||||
scheduleTime = DateTime.fromSeconds(epochTime).toUTC().toISO();
|
scheduleTime = DateTime.fromSeconds(epochTime)
|
||||||
|
.diffNow("second")
|
||||||
|
.as("second");
|
||||||
} else if (delay) {
|
} else if (delay) {
|
||||||
scheduleTime = DateTime.now().plus(delay).toUTC().toISO();
|
scheduleTime = Duration.fromDurationLike(delay).as("second");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let taskId: string;
|
|
||||||
|
|
||||||
switch (queueName) {
|
switch (queueName) {
|
||||||
case "new-episode":
|
case "ANILIST_UPDATES":
|
||||||
const { aniListId } = body as QueueBody["new-episode"];
|
case "NEW_EPISODE":
|
||||||
taskId = buildNewEpisodeTaskId(aniListId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: `projects/${projectId}/locations/northamerica-northeast1/queues/${queueName}/tasks/${taskId}`,
|
body,
|
||||||
scheduleTime,
|
scheduleTime,
|
||||||
httpRequest: {
|
|
||||||
url: `${domain}/internal/new-episode`,
|
|
||||||
httpMethod: "POST",
|
|
||||||
body: Buffer.from(JSON.stringify(body)).toString("base64"),
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Anilist-Token": headers?.["X-Anilist-Token"],
|
"X-Anilist-Token": headers?.["X-Anilist-Token"],
|
||||||
},
|
},
|
||||||
},
|
|
||||||
};
|
|
||||||
case "anilist-updates":
|
|
||||||
const { deviceId, titleId, nameSuffix } =
|
|
||||||
body as QueueBody[typeof queueName];
|
|
||||||
taskId = buildAnilistRetryTaskId(deviceId, titleId, nameSuffix);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: `projects/${projectId}/locations/northamerica-northeast1/queues/${queueName}/tasks/${taskId}`,
|
|
||||||
scheduleTime,
|
|
||||||
httpRequest: {
|
|
||||||
url: `${domain}/watch-status`,
|
|
||||||
httpMethod: "POST",
|
|
||||||
body: Buffer.from(
|
|
||||||
JSON.stringify({ ...body, nameSuffix: undefined }),
|
|
||||||
).toString("base64"),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Anilist-Token": headers?.["X-Anilist-Token"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown queue name: ${queueName}`);
|
throw new Error(`Unknown queue name: ${queueName}`);
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import { createClient } from "@libsql/client";
|
// import { createClient } from "@libsql/client";
|
||||||
import { env as cloudflareEnv } from "cloudflare:workers";
|
import { env as cloudflareEnv } from "cloudflare:workers";
|
||||||
import { drizzle } from "drizzle-orm/libsql";
|
import { drizzle } from "drizzle-orm/d1";
|
||||||
|
|
||||||
type Db = ReturnType<typeof drizzle>;
|
type Db = ReturnType<typeof drizzle>;
|
||||||
let db: Db | null = null;
|
// let db: Db | null = null;
|
||||||
|
|
||||||
export function getDb(env: Cloudflare.Env = cloudflareEnv): Db {
|
export function getDb(env: Cloudflare.Env = cloudflareEnv): Db {
|
||||||
if (db) {
|
// if (db) {
|
||||||
return db;
|
// return db;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const client = createClient({
|
const db = drizzle(env.DB, { logger: true });
|
||||||
url: env.TURSO_URL,
|
|
||||||
authToken: env.TURSO_AUTH_TOKEN,
|
|
||||||
});
|
|
||||||
|
|
||||||
db = drizzle(client);
|
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|||||||
9
worker-configuration.d.ts
vendored
9
worker-configuration.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// Generated by Wrangler by running `wrangler types` (hash: ebfd0a114715273ef48ef9f20e119f14)
|
// Generated by Wrangler by running `wrangler types` (hash: 55b6bc5eef0b3709210a3c57164a6bda)
|
||||||
// Runtime types generated with workerd@1.20251011.0 2024-09-23 nodejs_compat
|
// Runtime types generated with workerd@1.20251011.0 2024-09-23 nodejs_compat
|
||||||
declare namespace Cloudflare {
|
declare namespace Cloudflare {
|
||||||
interface GlobalProps {
|
interface GlobalProps {
|
||||||
@@ -11,7 +11,14 @@ declare namespace Cloudflare {
|
|||||||
TURSO_AUTH_TOKEN: string;
|
TURSO_AUTH_TOKEN: string;
|
||||||
ENABLE_ANIFY: string;
|
ENABLE_ANIFY: string;
|
||||||
ADMIN_SDK_JSON: string;
|
ADMIN_SDK_JSON: string;
|
||||||
|
CLOUDFLARE_TOKEN: string;
|
||||||
|
CLOUDFLARE_D1_TOKEN: string;
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: string;
|
||||||
|
CLOUDFLARE_DATABASE_ID: string;
|
||||||
ANILIST_DO: DurableObjectNamespace<import("./src/index").AnilistDo>;
|
ANILIST_DO: DurableObjectNamespace<import("./src/index").AnilistDo>;
|
||||||
|
DB: D1Database;
|
||||||
|
ANILIST_UPDATES: Queue;
|
||||||
|
NEW_EPISODE: Queue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
interface Env extends Cloudflare.Env {}
|
interface Env extends Cloudflare.Env {}
|
||||||
|
|||||||
@@ -17,4 +17,29 @@ class_name = "AnilistDo"
|
|||||||
|
|
||||||
[[migrations]]
|
[[migrations]]
|
||||||
tag = "v1"
|
tag = "v1"
|
||||||
|
new_classes = ["AniwatchApiContainer"]
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "<v2>"
|
||||||
|
deleted_classes = ["AniwatchApiContainer"]
|
||||||
new_classes = ["AnilistDo"]
|
new_classes = ["AnilistDo"]
|
||||||
|
|
||||||
|
[[queues.producers]]
|
||||||
|
queue = "anilist-updates"
|
||||||
|
binding = "ANILIST_UPDATES"
|
||||||
|
|
||||||
|
[[queues.producers]]
|
||||||
|
queue = "new-episode"
|
||||||
|
binding = "NEW_EPISODE"
|
||||||
|
|
||||||
|
[[queues.consumers]]
|
||||||
|
queue = "anilist-updates"
|
||||||
|
|
||||||
|
[[queues.consumers]]
|
||||||
|
queue = "new-episode"
|
||||||
|
|
||||||
|
[[d1_databases]]
|
||||||
|
binding = "DB"
|
||||||
|
database_name = "aniplay"
|
||||||
|
database_id = "5083d01d-7444-4336-a629-7c3e2002b13d"
|
||||||
|
migrations_dir = "drizzle"
|
||||||
|
|||||||
Reference in New Issue
Block a user