diff options
| author | Fuwn <[email protected]> | 2026-03-03 22:54:56 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-03-03 22:54:56 -0800 |
| commit | 07a00c3ea35b4df7eb23275704dd26f842db76be (patch) | |
| tree | 097ee52433779830aa4ddd6666e85ed42878cfc9 /src/lib | |
| parent | refactor(effect): migrate svelte json hotspots to typed decoders (diff) | |
| download | due.moe-07a00c3ea35b4df7eb23275704dd26f842db76be.tar.xz due.moe-07a00c3ea35b4df7eb23275704dd26f842db76be.zip | |
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/List/Anime/rendering.test.ts | 96 | ||||
| -rw-r--r-- | src/lib/List/Anime/rendering.ts | 19 | ||||
| -rw-r--r-- | src/lib/List/CleanGrid.svelte | 5 | ||||
| -rw-r--r-- | src/lib/Media/Anime/Airing/Subtitled/match.ts | 13 | ||||
| -rw-r--r-- | src/lib/Media/Anime/Airing/classify.test.ts | 94 |
5 files changed, 225 insertions, 2 deletions
diff --git a/src/lib/List/Anime/rendering.test.ts b/src/lib/List/Anime/rendering.test.ts new file mode 100644 index 00000000..038e3729 --- /dev/null +++ b/src/lib/List/Anime/rendering.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "vitest"; +import type { Media } from "$lib/Data/AniList/media"; +import { shouldRenderAnimeCover } from "$lib/List/Anime/rendering"; + +const baseMedia = (id: number): Media => + ({ + id, + idMal: id, + status: "RELEASING", + type: "ANIME", + episodes: 12, + chapters: 0, + volumes: 0, + duration: 24, + format: "TV", + title: { + romaji: `Fixture Show ${id}`, + english: `Fixture Show ${id}`, + native: `Fixture Show ${id}`, + }, + nextAiringEpisode: { + episode: 9, + nativeEpisode: 10, + airingAt: Math.floor(Date.now() / 1000) + 24 * 60 * 60, + nativeAiringAt: Math.floor(Date.now() / 1000) + 36 * 60 * 60, + }, + synonyms: [], + mediaListEntry: { + progress: 8, + progressVolumes: 0, + status: "CURRENT", + score: 0, + repeat: 0, + startedAt: { + year: 2026, + month: 1, + day: 1, + }, + completedAt: { + year: 0, + month: 0, + day: 0, + }, + createdAt: 0, + updatedAt: 0, + customLists: {}, + }, + startDate: { + year: 2026, + month: 1, + }, + endDate: { + year: 2026, + month: 12, + }, + coverImage: { + extraLarge: "https://example.com/cover-xl.jpg", + medium: "https://example.com/cover-md.jpg", + }, + tags: [], + genres: [], + season: "WINTER", + isAdult: false, + relations: { + edges: [], + }, + }) as Media; + +describe("anime cover rendering", () => { + it("renders due media when nativeEpisode indicates user is behind", () => { + const media = baseMedia(194028); + + expect( + shouldRenderAnimeCover(media, { + upcoming: false, + notYetReleased: false, + }), + ).toBe(true); + }); + + it("hides caught-up releasing media outside upcoming lists", () => { + const media = baseMedia(194028); + + media.mediaListEntry = { + ...(media.mediaListEntry as NonNullable<typeof media.mediaListEntry>), + progress: 9, + }; + + expect( + shouldRenderAnimeCover(media, { + upcoming: false, + notYetReleased: false, + }), + ).toBe(false); + }); +}); diff --git a/src/lib/List/Anime/rendering.ts b/src/lib/List/Anime/rendering.ts new file mode 100644 index 00000000..8a79d74c --- /dev/null +++ b/src/lib/List/Anime/rendering.ts @@ -0,0 +1,19 @@ +import type { Media } from "$lib/Data/AniList/media"; +import { hasDueEpisodes } from "$lib/Media/Anime/Airing/classify"; + +interface AnimeCoverRenderOptions { + upcoming: boolean; + notYetReleased: boolean; +} + +export const shouldRenderAnimeCover = ( + media: Media, + options: AnimeCoverRenderOptions, +): boolean => { + const progress = media.mediaListEntry?.progress || 0; + + if (options.upcoming || options.notYetReleased) return true; + if (media.status === "FINISHED") return progress !== media.episodes; + + return hasDueEpisodes(media); +}; diff --git a/src/lib/List/CleanGrid.svelte b/src/lib/List/CleanGrid.svelte index 006a5e4e..384d4940 100644 --- a/src/lib/List/CleanGrid.svelte +++ b/src/lib/List/CleanGrid.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import type { Media } from "$lib/Data/AniList/media"; import ParallaxImage from "$lib/Image/ParallaxImage.svelte"; +import { shouldRenderAnimeCover } from "$lib/List/Anime/rendering"; import { outboundLink } from "$lib/Media/links"; import LinkedTooltip from "$lib/Tooltip/LinkedTooltip.svelte"; import settings from "$stores/settings"; @@ -30,7 +31,9 @@ $: processedMedia = {@const progress = (title.mediaListEntry || { progress: 0 }).progress} {@const isAboveFold = index < 6} - {#if type === 'anime' ? upcoming || notYetReleased || progress !== (title.nextAiringEpisode?.episode || 9999) - 1 : progress != title.episodes} + {#if type === 'anime' + ? shouldRenderAnimeCover(title, { upcoming, notYetReleased }) + : progress != title.episodes} <div class="cover-card" id={`${type}-${title.id}-${uniqueID}`}> <LinkedTooltip pin={`${type}-${title.id}-${uniqueID}`} diff --git a/src/lib/Media/Anime/Airing/Subtitled/match.ts b/src/lib/Media/Anime/Airing/Subtitled/match.ts index 0c91f3ac..40dda4b8 100644 --- a/src/lib/Media/Anime/Airing/Subtitled/match.ts +++ b/src/lib/Media/Anime/Airing/Subtitled/match.ts @@ -530,6 +530,9 @@ const buildInjectAiringTimeCacheKey = ( [ anime.id, anime.status, + anime.mediaListEntry?.status || "", + anime.mediaListEntry?.progress || 0, + anime.mediaListEntry?.updatedAt || 0, anime.nextAiringEpisode?.episode || 0, anime.nextAiringEpisode?.airingAt || 0, displayNativeCountdown ? 1 : 0, @@ -540,6 +543,14 @@ const buildInjectAiringTimeCacheKey = ( const cloneInjectedMedia = (media: Media): Media => ({ ...media, + mediaListEntry: media.mediaListEntry + ? { + ...media.mediaListEntry, + startedAt: { ...media.mediaListEntry.startedAt }, + completedAt: { ...media.mediaListEntry.completedAt }, + customLists: { ...media.mediaListEntry.customLists }, + } + : undefined, nextAiringEpisode: media.nextAiringEpisode ? { ...media.nextAiringEpisode } : undefined, @@ -634,3 +645,5 @@ export const injectAiringTime = ( return cloneInjectedMedia(cachedValue); }; + +export const clearInjectAiringTimeCache = () => injectAiringTimeCache.clear(); diff --git a/src/lib/Media/Anime/Airing/classify.test.ts b/src/lib/Media/Anime/Airing/classify.test.ts index fb7f2c8f..b05be83b 100644 --- a/src/lib/Media/Anime/Airing/classify.test.ts +++ b/src/lib/Media/Anime/Airing/classify.test.ts @@ -6,7 +6,10 @@ import { hasDueEpisodes, getAnimeEpisodeState, } from "$lib/Media/Anime/Airing/classify"; -import { injectAiringTime } from "$lib/Media/Anime/Airing/Subtitled/match"; +import { + clearInjectAiringTimeCache, + injectAiringTime, +} from "$lib/Media/Anime/Airing/Subtitled/match"; import type { SubsPlease } from "$lib/Media/Anime/Airing/Subtitled/subsPlease"; const toScheduleTime = (epochSeconds: number) => { @@ -196,4 +199,93 @@ describe("injectAiringTime cache safety", () => { expect(typeof second.nextAiringEpisode?.airingAt).toBe("number"); expect(typeof second.nextAiringEpisode?.nativeAiringAt).toBe("number"); }); + + it("does not reuse stale progress across cache hits for media 194028", () => { + clearInjectAiringTimeCache(); + + const media = baseMedia(194028); + const subtitledAiringAt = Math.floor(Date.now() / 1000) + 24 * 60 * 60; + const nativeAiringAt = subtitledAiringAt + 12 * 60 * 60; + const nativeAiringDate = new Date(nativeAiringAt * 1000); + const airingDay = nativeAiringDate.toLocaleString("en-US", { + weekday: "long", + }); + const subsPlease = { + tz: "America/Los_Angeles", + schedule: { + [airingDay]: [ + { + title: media.title.romaji, + page: "", + image_url: "", + time: toScheduleTime(subtitledAiringAt), + }, + ], + }, + } as unknown as SubsPlease; + + media.nextAiringEpisode = { + episode: 10, + airingAt: nativeAiringAt, + }; + + settings.setKey("displayNativeCountdown", false); + + const caughtUp = { + ...media, + mediaListEntry: { + ...(media.mediaListEntry || {}), + progress: 9, + }, + } as Media; + const behind = { + ...media, + mediaListEntry: { + ...(media.mediaListEntry || {}), + progress: 8, + }, + } as Media; + const cachedCaughtUp = injectAiringTime(caughtUp, subsPlease); + const updatedBehind = injectAiringTime(behind, subsPlease); + + expect(hasDueEpisodes(cachedCaughtUp)).toBe(false); + expect(hasDueEpisodes(updatedBehind)).toBe(true); + }); + + it("does not let caller mutate cached mediaListEntry progress", () => { + clearInjectAiringTimeCache(); + + const media = baseMedia(194028); + const subtitledAiringAt = Math.floor(Date.now() / 1000) + 24 * 60 * 60; + const nativeAiringAt = subtitledAiringAt + 12 * 60 * 60; + const nativeAiringDate = new Date(nativeAiringAt * 1000); + const airingDay = nativeAiringDate.toLocaleString("en-US", { + weekday: "long", + }); + const subsPlease = { + tz: "America/Los_Angeles", + schedule: { + [airingDay]: [ + { + title: media.title.romaji, + page: "", + image_url: "", + time: toScheduleTime(subtitledAiringAt), + }, + ], + }, + } as unknown as SubsPlease; + + settings.setKey("displayNativeCountdown", false); + + const originalProgress = media.mediaListEntry?.progress || 0; + const first = injectAiringTime(media, subsPlease); + + if (first.mediaListEntry) first.mediaListEntry.progress = 999; + + const second = injectAiringTime(media, subsPlease); + + expect(media.mediaListEntry?.progress).toBe(originalProgress); + expect(second.mediaListEntry?.progress).toBe(originalProgress); + }); }); |