aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Media/Anime
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-03-01 13:12:25 -0800
committerFuwn <[email protected]>2026-03-01 13:25:59 -0800
commitceb53b18a4d766b95c7107d1f778d443a0e3d9b3 (patch)
tree5957306e0ae159bc0c9e6d99450035ea7ab6e566 /src/lib/Media/Anime
parentperf(match): pre-index subtitle schedule by day and token (diff)
downloaddue.moe-ceb53b18a4d766b95c7107d1f778d443a0e3d9b3.tar.xz
due.moe-ceb53b18a4d766b95c7107d1f778d443a0e3d9b3.zip
perf(match): cache indexed schedules and per-airing match results
Diffstat (limited to 'src/lib/Media/Anime')
-rw-r--r--src/lib/Media/Anime/Airing/Subtitled/match.ts72
1 files changed, 65 insertions, 7 deletions
diff --git a/src/lib/Media/Anime/Airing/Subtitled/match.ts b/src/lib/Media/Anime/Airing/Subtitled/match.ts
index 2281aca8..52cf581f 100644
--- a/src/lib/Media/Anime/Airing/Subtitled/match.ts
+++ b/src/lib/Media/Anime/Airing/Subtitled/match.ts
@@ -26,6 +26,7 @@ interface DayScheduleIndex {
interface ScheduleIndex {
byDay: Map<string, DayScheduleIndex>;
+ version: string;
}
const secondsUntil = (targetTime: string, targetDay: string) => {
@@ -131,11 +132,24 @@ const indexPush = (index: Map<string, number[]>, key: string, entryIndex: number
else index.set(key, [entryIndex]);
};
+const scheduleIndexCache = new WeakMap<SubsPlease, ScheduleIndex>();
+const closestMatchCache = new Map<string, Time | null>();
+
const buildScheduleIndex = (subsPlease: SubsPlease): ScheduleIndex => {
const byDay = new Map<string, DayScheduleIndex>();
+ const versionParts: string[] = [];
for (const [day, value] of Object.entries(subsPlease.schedule)) {
const flattenedValue = Array.isArray(value) ? value.flat() : [];
+ const firstEntry = flattenedValue[0];
+ const lastEntry = flattenedValue[flattenedValue.length - 1];
+
+ versionParts.push(
+ `${day}:${flattenedValue.length}:${firstEntry?.title || ''}:${firstEntry?.time || ''}:${
+ lastEntry?.title || ''
+ }:${lastEntry?.time || ''}`
+ );
+
const dayIndex: DayScheduleIndex = {
entries: [],
exactTitleIndex: new Map<string, number[]>(),
@@ -165,11 +179,23 @@ const buildScheduleIndex = (subsPlease: SubsPlease): ScheduleIndex => {
byDay.set(day, dayIndex);
}
- return { byDay };
+ return {
+ byDay,
+ version: `${subsPlease.tz}:${versionParts.join('|')}`
+ };
};
export const findClosestMatch = (scheduleIndex: ScheduleIndex, anime: Media): Time | null => {
- if (excludeMatch.includes(anime.id)) return null;
+ if (excludeMatch.includes(anime.id)) {
+ closestMatchCache.set(`${anime.id}:excluded`, null);
+
+ return null;
+ }
+
+ const cacheKey = `${anime.id}:${anime.nextAiringEpisode?.airingAt || 0}:${scheduleIndex.version}`;
+ const cached = closestMatchCache.get(cacheKey);
+
+ if (cached !== undefined) return cached;
const airingDay = new Date((anime.nextAiringEpisode?.airingAt || 0) * 1000).toLocaleString(
'en-US',
@@ -177,7 +203,11 @@ export const findClosestMatch = (scheduleIndex: ScheduleIndex, anime: Media): Ti
);
const dayIndex = scheduleIndex.byDay.get(airingDay);
- if (!dayIndex || dayIndex.entries.length === 0) return null;
+ if (!dayIndex || dayIndex.entries.length === 0) {
+ closestMatchCache.set(cacheKey, null);
+
+ return null;
+ }
let bestMatch: Time | null = null;
let bestScore = 0;
@@ -222,10 +252,26 @@ export const findClosestMatch = (scheduleIndex: ScheduleIndex, anime: Media): Ti
}
}
- if (bestScore < MIN_MATCH_SCORE) return null;
- if (bestScore - secondBestScore < MIN_MATCH_MARGIN) return null;
- if (bestNumericTokenOverlap === 0 && bestTokenOverlap < MIN_TOKEN_OVERLAP) return null;
+ if (bestScore < MIN_MATCH_SCORE) {
+ closestMatchCache.set(cacheKey, null);
+
+ return null;
+ }
+
+ if (bestScore - secondBestScore < MIN_MATCH_MARGIN) {
+ closestMatchCache.set(cacheKey, null);
+
+ return null;
+ }
+
+ if (bestNumericTokenOverlap === 0 && bestTokenOverlap < MIN_TOKEN_OVERLAP) {
+ closestMatchCache.set(cacheKey, null);
+
+ return null;
+ }
+ closestMatchCache.set(cacheKey, bestMatch);
+
return bestMatch;
};
@@ -289,6 +335,18 @@ export const findClosestMedia = (media: Media[], matchFor: string) => {
export const clearClosestMediaCache = () => findClosestMediaCache.clear();
+const getScheduleIndex = (subsPlease: SubsPlease): ScheduleIndex => {
+ const cached = scheduleIndexCache.get(subsPlease);
+
+ if (cached) return cached;
+
+ const built = buildScheduleIndex(subsPlease);
+
+ scheduleIndexCache.set(subsPlease, built);
+
+ return built;
+};
+
export const injectAiringTime = (anime: Media, subsPlease: SubsPlease | null) => {
if (season() !== anime.season) return anime;
@@ -308,7 +366,7 @@ export const injectAiringTime = (anime: Media, subsPlease: SubsPlease | null) =>
// || !(nativeUntilAiring !== undefined && nativeUntilAiring < 24 * 60 * 60)
)
) {
- const scheduleIndex = buildScheduleIndex(subsPlease);
+ const scheduleIndex = getScheduleIndex(subsPlease);
if ((anime.nextAiringEpisode?.episode || 0) > 1) {
const foundTime: Time | null = findClosestMatch(scheduleIndex, anime);