From 755ae4b94f5c38c363f97bf445634b576990b012 Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Sat, 21 Sep 2024 13:45:37 -0400 Subject: [PATCH] feat: store unreleased titles where first episode time is unknown --- src/controllers/internal/new-episode/index.ts | 2 +- .../internal/upcoming-titles/anilist.ts | 19 +++++++++++- .../internal/upcoming-titles/index.ts | 3 +- src/libs/anilist/getNextEpisodeAiringAt.ts | 7 ++--- src/libs/maybeScheduleNextAiringEpisode.ts | 18 +++++++++-- src/models/schema.ts | 8 ++++- src/models/unreleasedTitles.ts | 30 +++++++++++++++++++ 7 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 src/models/unreleasedTitles.ts diff --git a/src/controllers/internal/new-episode/index.ts b/src/controllers/internal/new-episode/index.ts index 96f942e..d52f0e2 100644 --- a/src/controllers/internal/new-episode/index.ts +++ b/src/controllers/internal/new-episode/index.ts @@ -114,7 +114,7 @@ app.post( aniListId, ); - await Promise.all( + await Promise.allSettled( tokens.map(async (token) => { return sendFcmMessage( mapKeys( diff --git a/src/controllers/internal/upcoming-titles/anilist.ts b/src/controllers/internal/upcoming-titles/anilist.ts index 44e0d9f..d96fd81 100644 --- a/src/controllers/internal/upcoming-titles/anilist.ts +++ b/src/controllers/internal/upcoming-titles/anilist.ts @@ -1,8 +1,11 @@ import { graphql } from "gql.tada"; import { GraphQLClient } from "graphql-request"; +import type { HonoRequest } from "hono"; import { DateTime } from "luxon"; +import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; import { getValue, setValue } from "~/models/kv"; +import { filterUnreleasedTitles } from "~/models/unreleasedTitles"; import type { Env } from "~/types/env"; import type { Title } from "~/types/title"; import { MediaFragment } from "~/types/title/mediaFragment"; @@ -46,7 +49,7 @@ type AiringSchedule = { id: number; }; -export async function getUpcomingTitlesFromAnilist(env: Env) { +export async function getUpcomingTitlesFromAnilist(env: Env, req: HonoRequest) { const client = new GraphQLClient("https://graphql.anilist.co/"); const lastCheckedScheduleAt = await getValue( env, @@ -55,6 +58,7 @@ export async function getUpcomingTitlesFromAnilist(env: Env) { const twoDaysFromNow = DateTime.now().plus({ days: 2 }).toUnixInteger(); let currentPage = 1; + let plannedToWatchTitles = new Set(); let scheduleList: AiringSchedule[] = []; let shouldContinue = true; @@ -66,10 +70,17 @@ export async function getUpcomingTitlesFromAnilist(env: Env) { }); const { airingSchedules, pageInfo } = Page!; + plannedToWatchTitles = plannedToWatchTitles.union( + await filterUnreleasedTitles( + env, + airingSchedules!.map((schedule) => schedule!.media?.id!), + ), + ); scheduleList = scheduleList.concat( airingSchedules!.filter( (schedule): schedule is AiringSchedule => !!schedule && + !plannedToWatchTitles.has(schedule.media?.id) && schedule.media?.countryOfOrigin === "JP" && schedule.episode == 1, ), @@ -77,6 +88,12 @@ export async function getUpcomingTitlesFromAnilist(env: Env) { shouldContinue = pageInfo?.hasNextPage ?? false; } while (shouldContinue); + await Promise.allSettled( + Array.from(plannedToWatchTitles).map((titleId) => + maybeScheduleNextAiringEpisode(env, req, titleId), + ), + ); + if (scheduleList.length === 0) { return []; } diff --git a/src/controllers/internal/upcoming-titles/index.ts b/src/controllers/internal/upcoming-titles/index.ts index 7024b42..45a5d6e 100644 --- a/src/controllers/internal/upcoming-titles/index.ts +++ b/src/controllers/internal/upcoming-titles/index.ts @@ -22,9 +22,10 @@ app.post("/", async (c) => { const titles = await getUpcomingTitlesFromAnilist( env(c, "workerd"), + c.req, ); - await Promise.all( + await Promise.allSettled( titles.map(async (title) => { const titleName = title.media.title?.userPreferred ?? diff --git a/src/libs/anilist/getNextEpisodeAiringAt.ts b/src/libs/anilist/getNextEpisodeAiringAt.ts index 25bf04a..bcbef9a 100644 --- a/src/libs/anilist/getNextEpisodeAiringAt.ts +++ b/src/libs/anilist/getNextEpisodeAiringAt.ts @@ -4,6 +4,7 @@ import { GraphQLClient } from "graphql-request"; const GetNextEpisodeAiringAtQuery = graphql(` query GetNextEpisodeAiringAt($id: Int!) { Media(id: $id) { + status nextAiringEpisode { episode timeUntilAiring @@ -18,11 +19,9 @@ export function getNextEpisodeTimeUntilAiring(aniListId: number) { return client .request(GetNextEpisodeAiringAtQuery, { id: aniListId }) .then((data) => { + const status = data!.Media!.status; const nextAiring = data!.Media!.nextAiringEpisode; - if (!nextAiring) { - return null; - } - return nextAiring; + return { status, nextAiring }; }); } diff --git a/src/libs/maybeScheduleNextAiringEpisode.ts b/src/libs/maybeScheduleNextAiringEpisode.ts index ed9657c..a9bbdbc 100644 --- a/src/libs/maybeScheduleNextAiringEpisode.ts +++ b/src/libs/maybeScheduleNextAiringEpisode.ts @@ -2,6 +2,10 @@ import { Client } from "@upstash/qstash"; import type { HonoRequest } from "hono"; import { setTitleMessage } from "~/models/titleMessages"; +import { + addUnreleasedTitle, + removeUnreleasedTitle, +} from "~/models/unreleasedTitles"; import type { Env } from "~/types/env"; import { getNextEpisodeTimeUntilAiring } from "./anilist/getNextEpisodeAiringAt"; @@ -13,9 +17,14 @@ export async function maybeScheduleNextAiringEpisode( req: HonoRequest, aniListId: number, ) { - const nextAiring = await getNextEpisodeTimeUntilAiring(aniListId); + const { nextAiring, status } = await getNextEpisodeTimeUntilAiring(aniListId); if (!nextAiring) { - await deleteMessageIdForTitle(env, aniListId); + if (status === "NOT_YET_RELEASED") { + await addUnreleasedTitle(env, aniListId); + } else { + await deleteMessageIdForTitle(env, aniListId); + } + return; } @@ -30,5 +39,8 @@ export async function maybeScheduleNextAiringEpisode( delay: timeUntilAiring, contentBasedDeduplication: true, }); - await setTitleMessage(env, aniListId, messageId); + await Promise.allSettled([ + setTitleMessage(env, aniListId, messageId), + removeUnreleasedTitle(env, aniListId), + ]); } diff --git a/src/models/schema.ts b/src/models/schema.ts index 81cef83..ca97822 100644 --- a/src/models/schema.ts +++ b/src/models/schema.ts @@ -39,9 +39,15 @@ export const titleMessagesTable = sqliteTable("title_messages", { messageId: text("message_id").notNull(), }); +/** Used to keep track of titles that haven't been released yet and the time when the first episode will be released is unknown */ +export const unreleasedTitlesTable = sqliteTable("unreleased_titles", { + titleId: integer("title_id").notNull().primaryKey(), +}); + export const tables = [ - watchStatusTable, deviceTokensTable, keyValueTable, titleMessagesTable, + unreleasedTitlesTable, + watchStatusTable, ]; diff --git a/src/models/unreleasedTitles.ts b/src/models/unreleasedTitles.ts new file mode 100644 index 0000000..e218b07 --- /dev/null +++ b/src/models/unreleasedTitles.ts @@ -0,0 +1,30 @@ +import { eq, inArray } from "drizzle-orm"; + +import type { Env } from "~/types/env"; + +import { getDb } from "./db"; +import { unreleasedTitlesTable } from "./schema"; + +export function addUnreleasedTitle(env: Env, titleId: number) { + return getDb(env) + .insert(unreleasedTitlesTable) + .values({ titleId }) + .onConflictDoNothing() + .run(); +} + +export function filterUnreleasedTitles(env: Env, titleIds: number[]) { + return getDb(env) + .select({ titleId: unreleasedTitlesTable.titleId }) + .from(unreleasedTitlesTable) + .where(inArray(unreleasedTitlesTable.titleId, titleIds)) + .all() + .then((results) => new Set(results.map(({ titleId }) => titleId))); +} + +export function removeUnreleasedTitle(env: Env, titleId: number) { + return getDb(env) + .delete(unreleasedTitlesTable) + .where(eq(unreleasedTitlesTable.titleId, titleId)) + .run(); +}