From 25ed096b38de8eb0761412e33fb8678a14cd0b22 Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Mon, 9 Sep 2024 05:07:21 -0500 Subject: [PATCH] feat: create script to initialize "next episode" queue --- .../episodes/getByAniListId/aniwatch.ts | 5 +- .../episodes/getEpisodeUrl/aniwatch.ts | 4 + src/controllers/internal/new-episode/index.ts | 9 +- src/controllers/title/index.ts | 3 +- .../anilist.ts => libs/anilist/getTitle.ts} | 2 +- src/scripts/initializeNextEpisodeQueue.ts | 136 ++++++++++++++++++ 6 files changed, 150 insertions(+), 9 deletions(-) rename src/{controllers/title/anilist.ts => libs/anilist/getTitle.ts} (96%) create mode 100644 src/scripts/initializeNextEpisodeQueue.ts diff --git a/src/controllers/episodes/getByAniListId/aniwatch.ts b/src/controllers/episodes/getByAniListId/aniwatch.ts index 57362c1..e93e2c7 100644 --- a/src/controllers/episodes/getByAniListId/aniwatch.ts +++ b/src/controllers/episodes/getByAniListId/aniwatch.ts @@ -1,12 +1,11 @@ import { findBestMatchingTitle } from "~/libs/findBestMatchingTitle"; - -import { Episode, type EpisodesResponse } from "./episode"; +import { Episode, type EpisodesResponse } from "~/types/episode"; export async function getEpisodesFromAniwatch( aniListId: number, ): Promise { try { - const animeTitle = await import("~/controllers/title/anilist") + const animeTitle = await import("~/libs/anilist/getTitle") .then(({ fetchTitleFromAnilist }) => fetchTitleFromAnilist(aniListId, undefined), ) diff --git a/src/controllers/episodes/getEpisodeUrl/aniwatch.ts b/src/controllers/episodes/getEpisodeUrl/aniwatch.ts index 84096cc..67b9ca0 100644 --- a/src/controllers/episodes/getEpisodeUrl/aniwatch.ts +++ b/src/controllers/episodes/getEpisodeUrl/aniwatch.ts @@ -10,6 +10,10 @@ export async function getSourcesFromAniwatch( ) .then((res) => res.json()) .then(({ intro, outro, sources, tracks }) => { + if (!sources || sources.length === 0) { + return { source: null }; + } + return { intro: convertSkipTime(intro), outro: convertSkipTime(outro), diff --git a/src/controllers/internal/new-episode/index.ts b/src/controllers/internal/new-episode/index.ts index b17c289..f449dd0 100644 --- a/src/controllers/internal/new-episode/index.ts +++ b/src/controllers/internal/new-episode/index.ts @@ -39,10 +39,13 @@ app.post( aniListId: number; episodeNumber: number; }>(); + console.log( + `Internal new episode route, aniListId: ${aniListId}, episodeNumber: ${episodeNumber}`, + ); - if (!(await verifyQstashHeader(env(c, "workerd"), c.req))) { - return c.json(ErrorResponse, { status: 401 }); - } + // if (!(await verifyQstashHeader(env(c, "workerd"), c.req))) { + // return c.json(ErrorResponse, { status: 401 }); + // } const domain = getCurrentDomain(c.req); const { success, result: fetchEpisodesResult } = await fetch( diff --git a/src/controllers/title/index.ts b/src/controllers/title/index.ts index cf2975d..2a91649 100644 --- a/src/controllers/title/index.ts +++ b/src/controllers/title/index.ts @@ -1,5 +1,6 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; +import { fetchTitleFromAnilist } from "~/libs/anilist/getTitle"; import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; import { AniListIdQuerySchema, @@ -9,8 +10,6 @@ import { } from "~/types/schema"; import { Title } from "~/types/title"; -import { fetchTitleFromAnilist } from "./anilist"; - const app = new OpenAPIHono(); const route = createRoute({ diff --git a/src/controllers/title/anilist.ts b/src/libs/anilist/getTitle.ts similarity index 96% rename from src/controllers/title/anilist.ts rename to src/libs/anilist/getTitle.ts index 5b5947d..9ac116b 100644 --- a/src/controllers/title/anilist.ts +++ b/src/libs/anilist/getTitle.ts @@ -17,7 +17,7 @@ const GetTitleQuery = graphql( export async function fetchTitleFromAnilist( id: number, - token: string | undefined, + token?: string | undefined, ): Promise { const client = new GraphQLClient("https://graphql.anilist.co/"); const headers = new Headers(); diff --git a/src/scripts/initializeNextEpisodeQueue.ts b/src/scripts/initializeNextEpisodeQueue.ts new file mode 100644 index 0000000..9be45f3 --- /dev/null +++ b/src/scripts/initializeNextEpisodeQueue.ts @@ -0,0 +1,136 @@ +import { Client } from "@upstash/qstash"; + +import { fetchTitleFromAnilist } from "~/libs/anilist/getTitle"; +import { getDb } from "~/models/db"; +import { watchStatusTable } from "~/models/schema"; + +const args = new Set(process.argv.slice(2)); + +const isDevMode = args.has("--dev"); +const shouldTriggerLatestEpisode = args.has("--trigger-latest-episode"); + +await getTitleIds().then((titles) => + Promise.all( + titles.map((title) => + triggerNextEpisodeRoute(title).then((success) => + console.log( + `Triggered next episode route for title ${title}: ${success}`, + ), + ), + ), + ), +); + +function getTitleIds() { + return getDb(process.env) + .selectDistinct({ titleId: watchStatusTable.titleId }) + .from(watchStatusTable) + .all() + .then((titles) => titles.map((title) => title.titleId)); +} + +async function triggerNextEpisodeRoute(titleId: number) { + const title = await fetchTitleFromAnilist(titleId); + if (!title) { + console.error(`Failed to fetch title ${titleId}`); + return false; + } else if (!title.nextAiringEpisode) { + console.log(`Title ${titleId} has no next airing episode`); + return true; + } + + if (isDevMode || shouldTriggerLatestEpisode) { + const serverUrl = isDevMode + ? "http://127.0.0.1:8080" + : "https://aniplay-v2.rururu.workers.dev"; + const wasSuccessful = await fetch(`${serverUrl}/episodes/${titleId}`) + .then((res) => res.json()) + .then(({ success, result }) => { + if (!success) { + console.error(`Failed to fetch episodes for title ${titleId}`); + return -1; + } + + return Math.max(...result.episodes.map((episode) => episode.number)); + }) + .then((mostRecentEpisodeNumber) => { + if (mostRecentEpisodeNumber === -1) { + return false; + } + + if (isDevMode) { + return fetch("http://127.0.0.1:8080/internal/new-episode", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + aniListId: titleId, + episodeNumber: mostRecentEpisodeNumber, + }), + }) + .then((res) => res.json() as Promise<{ success: boolean }>) + .then(({ success, error }) => { + if (!success) { + console.error( + `Failed to trigger next episode route for title ${titleId} (most recent episode: ${mostRecentEpisodeNumber})`, + error, + ); + } + + return success; + }); + } else { + return new Client({ token: process.env.QSTASH_TOKEN }) + .publishJSON({ + url: "https://aniplay-v2.rururu.workers.dev/internal/new-episode", + body: { + aniListId: titleId, + episodeNumber: mostRecentEpisodeNumber, + }, + retries: 0, + contentBasedDeduplication: true, + }) + .then(() => true) + .catch((error) => { + console.error( + `Failed to trigger next episode route for title ${titleId} (most recent episode: ${mostRecentEpisodeNumber})`, + error, + ); + return false; + }); + } + }); + + if (!wasSuccessful) { + return false; + } + + console.log( + `Triggered next episode route for title ${titleId} (most recent episode)`, + ); + if (isDevMode) { + return true; + } + } + + return new Client({ token: process.env.QSTASH_TOKEN }) + .publishJSON({ + url: "https://aniplay-v2.rururu.workers.dev/internal/new-episode", + body: { + aniListId: titleId, + episodeNumber: title.nextAiringEpisode.episode, + }, + retries: 0, + delay: title.nextAiringEpisode.timeUntilAiring, + contentBasedDeduplication: true, + }) + .then(() => true) + .catch((error) => { + console.error( + `Failed to trigger next episode route for title ${titleId}`, + error, + ); + return false; + }); +}