feat: store unreleased titles where first episode time is unknown

This commit is contained in:
2024-09-21 13:45:37 -04:00
parent c1bf12de4f
commit 755ae4b94f
7 changed files with 76 additions and 11 deletions

View File

@@ -114,7 +114,7 @@ app.post(
aniListId, aniListId,
); );
await Promise.all( await Promise.allSettled(
tokens.map(async (token) => { tokens.map(async (token) => {
return sendFcmMessage( return sendFcmMessage(
mapKeys( mapKeys(

View File

@@ -1,8 +1,11 @@
import { graphql } from "gql.tada"; import { graphql } from "gql.tada";
import { GraphQLClient } from "graphql-request"; import { GraphQLClient } from "graphql-request";
import type { HonoRequest } from "hono";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode";
import { getValue, setValue } from "~/models/kv"; import { getValue, setValue } from "~/models/kv";
import { filterUnreleasedTitles } from "~/models/unreleasedTitles";
import type { Env } from "~/types/env"; import type { Env } from "~/types/env";
import type { Title } from "~/types/title"; import type { Title } from "~/types/title";
import { MediaFragment } from "~/types/title/mediaFragment"; import { MediaFragment } from "~/types/title/mediaFragment";
@@ -46,7 +49,7 @@ type AiringSchedule = {
id: number; 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 client = new GraphQLClient("https://graphql.anilist.co/");
const lastCheckedScheduleAt = await getValue( const lastCheckedScheduleAt = await getValue(
env, env,
@@ -55,6 +58,7 @@ export async function getUpcomingTitlesFromAnilist(env: Env) {
const twoDaysFromNow = DateTime.now().plus({ days: 2 }).toUnixInteger(); const twoDaysFromNow = DateTime.now().plus({ days: 2 }).toUnixInteger();
let currentPage = 1; let currentPage = 1;
let plannedToWatchTitles = new Set<number>();
let scheduleList: AiringSchedule[] = []; let scheduleList: AiringSchedule[] = [];
let shouldContinue = true; let shouldContinue = true;
@@ -66,10 +70,17 @@ export async function getUpcomingTitlesFromAnilist(env: Env) {
}); });
const { airingSchedules, pageInfo } = Page!; const { airingSchedules, pageInfo } = Page!;
plannedToWatchTitles = plannedToWatchTitles.union(
await filterUnreleasedTitles(
env,
airingSchedules!.map((schedule) => schedule!.media?.id!),
),
);
scheduleList = scheduleList.concat( scheduleList = scheduleList.concat(
airingSchedules!.filter( airingSchedules!.filter(
(schedule): schedule is AiringSchedule => (schedule): schedule is AiringSchedule =>
!!schedule && !!schedule &&
!plannedToWatchTitles.has(schedule.media?.id) &&
schedule.media?.countryOfOrigin === "JP" && schedule.media?.countryOfOrigin === "JP" &&
schedule.episode == 1, schedule.episode == 1,
), ),
@@ -77,6 +88,12 @@ export async function getUpcomingTitlesFromAnilist(env: Env) {
shouldContinue = pageInfo?.hasNextPage ?? false; shouldContinue = pageInfo?.hasNextPage ?? false;
} while (shouldContinue); } while (shouldContinue);
await Promise.allSettled(
Array.from(plannedToWatchTitles).map((titleId) =>
maybeScheduleNextAiringEpisode(env, req, titleId),
),
);
if (scheduleList.length === 0) { if (scheduleList.length === 0) {
return []; return [];
} }

View File

@@ -22,9 +22,10 @@ app.post("/", async (c) => {
const titles = await getUpcomingTitlesFromAnilist( const titles = await getUpcomingTitlesFromAnilist(
env<Env, typeof c>(c, "workerd"), env<Env, typeof c>(c, "workerd"),
c.req,
); );
await Promise.all( await Promise.allSettled(
titles.map(async (title) => { titles.map(async (title) => {
const titleName = const titleName =
title.media.title?.userPreferred ?? title.media.title?.userPreferred ??

View File

@@ -4,6 +4,7 @@ import { GraphQLClient } from "graphql-request";
const GetNextEpisodeAiringAtQuery = graphql(` const GetNextEpisodeAiringAtQuery = graphql(`
query GetNextEpisodeAiringAt($id: Int!) { query GetNextEpisodeAiringAt($id: Int!) {
Media(id: $id) { Media(id: $id) {
status
nextAiringEpisode { nextAiringEpisode {
episode episode
timeUntilAiring timeUntilAiring
@@ -18,11 +19,9 @@ export function getNextEpisodeTimeUntilAiring(aniListId: number) {
return client return client
.request(GetNextEpisodeAiringAtQuery, { id: aniListId }) .request(GetNextEpisodeAiringAtQuery, { id: aniListId })
.then((data) => { .then((data) => {
const status = data!.Media!.status;
const nextAiring = data!.Media!.nextAiringEpisode; const nextAiring = data!.Media!.nextAiringEpisode;
if (!nextAiring) {
return null;
}
return nextAiring; return { status, nextAiring };
}); });
} }

View File

@@ -2,6 +2,10 @@ import { Client } from "@upstash/qstash";
import type { HonoRequest } from "hono"; import type { HonoRequest } from "hono";
import { setTitleMessage } from "~/models/titleMessages"; import { setTitleMessage } from "~/models/titleMessages";
import {
addUnreleasedTitle,
removeUnreleasedTitle,
} from "~/models/unreleasedTitles";
import type { Env } from "~/types/env"; import type { Env } from "~/types/env";
import { getNextEpisodeTimeUntilAiring } from "./anilist/getNextEpisodeAiringAt"; import { getNextEpisodeTimeUntilAiring } from "./anilist/getNextEpisodeAiringAt";
@@ -13,9 +17,14 @@ export async function maybeScheduleNextAiringEpisode(
req: HonoRequest, req: HonoRequest,
aniListId: number, aniListId: number,
) { ) {
const nextAiring = await getNextEpisodeTimeUntilAiring(aniListId); const { nextAiring, status } = await getNextEpisodeTimeUntilAiring(aniListId);
if (!nextAiring) { if (!nextAiring) {
if (status === "NOT_YET_RELEASED") {
await addUnreleasedTitle(env, aniListId);
} else {
await deleteMessageIdForTitle(env, aniListId); await deleteMessageIdForTitle(env, aniListId);
}
return; return;
} }
@@ -30,5 +39,8 @@ export async function maybeScheduleNextAiringEpisode(
delay: timeUntilAiring, delay: timeUntilAiring,
contentBasedDeduplication: true, contentBasedDeduplication: true,
}); });
await setTitleMessage(env, aniListId, messageId); await Promise.allSettled([
setTitleMessage(env, aniListId, messageId),
removeUnreleasedTitle(env, aniListId),
]);
} }

View File

@@ -39,9 +39,15 @@ export const titleMessagesTable = sqliteTable("title_messages", {
messageId: text("message_id").notNull(), 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 = [ export const tables = [
watchStatusTable,
deviceTokensTable, deviceTokensTable,
keyValueTable, keyValueTable,
titleMessagesTable, titleMessagesTable,
unreleasedTitlesTable,
watchStatusTable,
]; ];

View File

@@ -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();
}