aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-06-05 14:48:42 +0000
committerFuwn <[email protected]>2026-06-05 14:48:42 +0000
commit577be4ea3440c258689a560d3d1a6c2ae158592b (patch)
tree763f658f5d785d64be43e2432052ec18e1e45f8f /src
parentfeat(schedule): add native track alongside sub and dub (diff)
downloaddue.moe-577be4ea3440c258689a560d3d1a6c2ae158592b.tar.xz
due.moe-577be4ea3440c258689a560d3d1a6c2ae158592b.zip
fix(airing): roll just-aired releases forward to stop refresh loopHEADmain
nextReleaseTime kept a just-aired release in the past for 5 minutes, showing a negative countdown and driving scheduleAiringRefresh into a 1-second revalidate loop. Always roll a past release to its next occurrence, and only arm the refresh timer on future airings.
Diffstat (limited to 'src')
-rw-r--r--src/lib/List/Anime/CleanAnimeList.svelte5
-rw-r--r--src/lib/Media/Anime/Airing/classify.test.ts24
-rw-r--r--src/lib/Media/Anime/Airing/match.ts6
3 files changed, 32 insertions, 3 deletions
diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte
index f2077d20..3fd43244 100644
--- a/src/lib/List/Anime/CleanAnimeList.svelte
+++ b/src/lib/List/Anime/CleanAnimeList.svelte
@@ -157,6 +157,7 @@ const scheduleAiringRefresh = () => {
return;
}
+ const now = Date.now() / 1000;
const nextAiringAt = media.reduce<number | null>((closest, currentMedia) => {
if (
currentMedia.status !== "RELEASING" &&
@@ -166,7 +167,9 @@ const scheduleAiringRefresh = () => {
const airingAt = currentMedia.nextAiringEpisode?.airingAt;
- if (!airingAt) return closest;
+ // Only arm the timer for upcoming airings; a past airingAt would clamp the
+ // timeout to 1s and revalidate every second.
+ if (!airingAt || airingAt <= now) return closest;
if (closest === null) return airingAt;
return airingAt < closest ? airingAt : closest;
diff --git a/src/lib/Media/Anime/Airing/classify.test.ts b/src/lib/Media/Anime/Airing/classify.test.ts
index eeff2036..7b3aea32 100644
--- a/src/lib/Media/Anime/Airing/classify.test.ts
+++ b/src/lib/Media/Anime/Airing/classify.test.ts
@@ -210,6 +210,30 @@ describe("countdown source fallback", () => {
});
});
+describe("release timing", () => {
+ it("rolls a just-aired release forward instead of returning a past time", () => {
+ clearInjectAiringTimeCache();
+
+ const media = baseMedia(170019);
+ const justAired = Math.floor(Date.now() / 1000) - 3 * 60;
+
+ media.nextAiringEpisode = {
+ episode: 10,
+ airingAt: justAired + 12 * 60 * 60,
+ };
+
+ const schedule = subScheduleFor(media, justAired);
+
+ settings.setKey("countdownSource", "sub");
+
+ const injected = injectAiringTime(media, schedule);
+
+ expect(injected.nextAiringEpisode?.airingAt).toBeGreaterThan(
+ Date.now() / 1000,
+ );
+ });
+});
+
describe("injectAiringTime cache safety", () => {
it("does not let caller mutation poison cached injected media", () => {
const media = baseMedia(444444);
diff --git a/src/lib/Media/Anime/Airing/match.ts b/src/lib/Media/Anime/Airing/match.ts
index 9cfe6102..1994b300 100644
--- a/src/lib/Media/Anime/Airing/match.ts
+++ b/src/lib/Media/Anime/Airing/match.ts
@@ -144,7 +144,9 @@ const findScheduleEntry = (
// Resolve the next future release time for a matched entry. AnimeSchedule gives
// the current week's episode; a delay window or a weekly cadence rolls a past
-// release forward to the next occurrence.
+// release forward to the next occurrence. This must never return a past time —
+// a stuck-in-the-past airingAt produces a negative countdown and a tight
+// refresh loop (see scheduleAiringRefresh).
const nextReleaseTime = (
entry: AiringEntry,
nowEpochSeconds: number,
@@ -155,7 +157,7 @@ const nextReleaseTime = (
const base = entry.airingAt;
if (!base) return 0;
- if (base > nowEpochSeconds - STALE_AIRING_GRACE_SECONDS) return base;
+ if (base > nowEpochSeconds) return base;
const weeksElapsed = Math.ceil((nowEpochSeconds - base) / SEVEN_DAYS_SECONDS);