feat: store unreleased titles where first episode time is unknown
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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 [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ??
|
||||||
|
|||||||
@@ -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 };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
await deleteMessageIdForTitle(env, aniListId);
|
if (status === "NOT_YET_RELEASED") {
|
||||||
|
await addUnreleasedTitle(env, aniListId);
|
||||||
|
} else {
|
||||||
|
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),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
];
|
];
|
||||||
|
|||||||
30
src/models/unreleasedTitles.ts
Normal file
30
src/models/unreleasedTitles.ts
Normal 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user