From 3a18ad060b5792d2a6c2a22897899806a10fa89b Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 23 Jul 2026 22:13:54 +0200 Subject: [PATCH] domain: delete dead jellyfinFieldMapping; scope playbackUnits to session boundary (phase 4e) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jellyfinFieldMapping.ts (SORT_FIELD_MAP friendly->Jellyfin sort names) had zero consumers — sort code passes raw Jellyfin field names directly — so it and its test are deleted. playbackUnits.ts can't be removed: its tick<->seconds helpers are still the correct converters for the remote Jellyfin *session* boundary (SessionInfo.playState.positionTicks, NowPlayingItem.runTimeTicks), which legitimately arrives in ticks. Documented that narrowed role; formatTime/ calculateProgress remain neutral seconds-based presentation helpers. Note (out of scope): sortBy still passes raw Jellyfin field names ("SortName", "CommunityRating") — a separate sort-taxonomy leak that would need its own Rust SortKey, like the search-scope work. Frontend 626 tests (jellyfinFieldMapping's 18 removed with it), check clean. --- src/lib/utils/jellyfinFieldMapping.test.ts | 138 --------------------- src/lib/utils/jellyfinFieldMapping.ts | 95 -------------- src/lib/utils/playbackUnits.ts | 24 ++-- 3 files changed, 13 insertions(+), 244 deletions(-) delete mode 100644 src/lib/utils/jellyfinFieldMapping.test.ts delete mode 100644 src/lib/utils/jellyfinFieldMapping.ts diff --git a/src/lib/utils/jellyfinFieldMapping.test.ts b/src/lib/utils/jellyfinFieldMapping.test.ts deleted file mode 100644 index 20f60fd4..00000000 --- a/src/lib/utils/jellyfinFieldMapping.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Jellyfin Field Mapping Tests - */ - -import { describe, it, expect } from "vitest"; -import { - SORT_FIELD_MAP, - getJellyfinSortField, - normalizeSortOrder, - ITEM_TYPES, - ITEM_TYPE_GROUPS, -} from "./jellyfinFieldMapping"; - -describe("Jellyfin Field Mapping", () => { - describe("SORT_FIELD_MAP", () => { - it("should map frontend sort keys to Jellyfin fields", () => { - expect(SORT_FIELD_MAP.title).toBe("SortName"); - expect(SORT_FIELD_MAP.artist).toBe("Artist"); - expect(SORT_FIELD_MAP.album).toBe("Album"); - expect(SORT_FIELD_MAP.year).toBe("ProductionYear"); - expect(SORT_FIELD_MAP.recent).toBe("DatePlayed"); - expect(SORT_FIELD_MAP.added).toBe("DateCreated"); - expect(SORT_FIELD_MAP.rating).toBe("CommunityRating"); - }); - - it("should have all common audio sorts", () => { - expect(SORT_FIELD_MAP).toHaveProperty("title"); - expect(SORT_FIELD_MAP).toHaveProperty("artist"); - expect(SORT_FIELD_MAP).toHaveProperty("album"); - expect(SORT_FIELD_MAP).toHaveProperty("year"); - expect(SORT_FIELD_MAP).toHaveProperty("recent"); - }); - - it("should have fallback sort names", () => { - expect(SORT_FIELD_MAP.name).toBe("SortName"); - }); - - it("should map aliases to same fields", () => { - expect(SORT_FIELD_MAP.title).toBe(SORT_FIELD_MAP.name); - expect(SORT_FIELD_MAP.recent).toBe("DatePlayed"); - expect(SORT_FIELD_MAP.dateAdded).toBe("DateCreated"); - expect(SORT_FIELD_MAP.datePlayed).toBe("DatePlayed"); - }); - }); - - describe("getJellyfinSortField()", () => { - it("should return mapped field for known keys", () => { - expect(getJellyfinSortField("artist")).toBe("Artist"); - expect(getJellyfinSortField("album")).toBe("Album"); - expect(getJellyfinSortField("year")).toBe("ProductionYear"); - }); - - it("should fallback to SortName for unknown keys", () => { - expect(getJellyfinSortField("unknown")).toBe("SortName"); - expect(getJellyfinSortField("")).toBe("SortName"); - expect(getJellyfinSortField("invalidKey")).toBe("SortName"); - }); - - it("should be case-sensitive", () => { - // Should work with exact case - expect(getJellyfinSortField("title")).toBe("SortName"); - // Unknown case variations fallback to default - expect(getJellyfinSortField("Title")).toBe("SortName"); - expect(getJellyfinSortField("TITLE")).toBe("SortName"); - }); - }); - - describe("normalizeSortOrder()", () => { - it("should accept valid ascending orders", () => { - expect(normalizeSortOrder("Ascending")).toBe("Ascending"); - expect(normalizeSortOrder("ascending")).toBe("Ascending"); - expect(normalizeSortOrder("asc")).toBe("Ascending"); - expect(normalizeSortOrder(undefined)).toBe("Ascending"); - }); - - it("should accept valid descending orders", () => { - expect(normalizeSortOrder("Descending")).toBe("Descending"); - expect(normalizeSortOrder("descending")).toBe("Descending"); - expect(normalizeSortOrder("desc")).toBe("Descending"); - }); - - it("should default to Ascending for unknown values", () => { - expect(normalizeSortOrder("invalid")).toBe("Ascending"); - expect(normalizeSortOrder("random")).toBe("Ascending"); - expect(normalizeSortOrder("")).toBe("Ascending"); - }); - }); - - describe("ITEM_TYPES", () => { - it("should define audio types", () => { - expect(ITEM_TYPES.AUDIO).toBe("Audio"); - expect(ITEM_TYPES.MUSIC_ALBUM).toBe("MusicAlbum"); - expect(ITEM_TYPES.MUSIC_ARTIST).toBe("MusicArtist"); - }); - - it("should define video types", () => { - expect(ITEM_TYPES.MOVIE).toBe("Movie"); - expect(ITEM_TYPES.SERIES).toBe("Series"); - expect(ITEM_TYPES.EPISODE).toBe("Episode"); - }); - - it("should have consistent case", () => { - // Jellyfin API uses CamelCase - expect(ITEM_TYPES.MUSIC_ALBUM).toBe("MusicAlbum"); - expect(ITEM_TYPES.MUSIC_ARTIST).toBe("MusicArtist"); - expect(ITEM_TYPES.MUSIC_VIDEO).toBe("MusicVideo"); - }); - }); - - describe("ITEM_TYPE_GROUPS", () => { - it("should group audio types correctly", () => { - expect(ITEM_TYPE_GROUPS.audio).toContain(ITEM_TYPES.AUDIO); - expect(ITEM_TYPE_GROUPS.audio).toContain(ITEM_TYPES.MUSIC_ALBUM); - expect(ITEM_TYPE_GROUPS.audio).toContain(ITEM_TYPES.MUSIC_ARTIST); - expect(ITEM_TYPE_GROUPS.audio.length).toBe(3); - }); - - it("should group video types correctly", () => { - expect(ITEM_TYPE_GROUPS.video).toContain(ITEM_TYPES.MOVIE); - expect(ITEM_TYPE_GROUPS.video).toContain(ITEM_TYPES.SERIES); - expect(ITEM_TYPE_GROUPS.video).toContain(ITEM_TYPES.EPISODE); - }); - - it("should provide movie and TV show subgroups", () => { - expect(ITEM_TYPE_GROUPS.movies).toEqual([ITEM_TYPES.MOVIE]); - expect(ITEM_TYPE_GROUPS.tvshows).toContain(ITEM_TYPES.SERIES); - expect(ITEM_TYPE_GROUPS.tvshows).toContain(ITEM_TYPES.EPISODE); - }); - - it("should have music alias for audio", () => { - expect(ITEM_TYPE_GROUPS.music).toEqual(ITEM_TYPE_GROUPS.audio); - }); - - it("should provide episodes filter", () => { - expect(ITEM_TYPE_GROUPS.episodes).toEqual([ITEM_TYPES.EPISODE]); - }); - }); -}); diff --git a/src/lib/utils/jellyfinFieldMapping.ts b/src/lib/utils/jellyfinFieldMapping.ts deleted file mode 100644 index febfe866..00000000 --- a/src/lib/utils/jellyfinFieldMapping.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Jellyfin Field Mapping - * - * Maps frontend sort option keys to Jellyfin API field names. - * This provides the single source of truth for how different UI sort options - * translate to backend database queries. - */ - -/** - * Maps friendly sort names to Jellyfin API field names - * Used by all library views for consistent sorting - */ -export const SORT_FIELD_MAP = { - // Default/fallback sorts - title: "SortName", - name: "SortName", - - // Audio-specific sorts - artist: "Artist", - album: "Album", - year: "ProductionYear", - recent: "DatePlayed", - added: "DateCreated", - rating: "CommunityRating", - duration: "RunTimeTicks", - - // Video-specific sorts - dateAdded: "DateCreated", - datePlayed: "DatePlayed", - IMDBRating: "CommunityRating", - - // Video series sorts - premiered: "PremiereDate", - episodeCount: "ChildCount", -} as const; - -/** - * Type-safe sort field names - */ -export type SortField = keyof typeof SORT_FIELD_MAP; - -/** - * Get Jellyfin API field name for a frontend sort key - * @param key Frontend sort key (e.g., "artist") - * @returns Jellyfin field name (e.g., "Artist") - */ -export function getJellyfinSortField(key: string): string { - const field = SORT_FIELD_MAP[key as SortField]; - return field || "SortName"; // Fallback to title sort -} - -/** - * Validate sort order string - * @param order Sort order value - * @returns Valid sort order for Jellyfin API - */ -export function normalizeSortOrder(order: string | undefined): "Ascending" | "Descending" { - if (order === "Descending" || order === "desc" || order === "descending") { - return "Descending"; - } - return "Ascending"; -} - -/** - * Jellyfin ItemType constants for filtering - * Used in getItems() and search() calls - */ -export const ITEM_TYPES = { - // Audio types - AUDIO: "Audio", - MUSIC_ALBUM: "MusicAlbum", - MUSIC_ARTIST: "MusicArtist", - MUSIC_VIDEO: "MusicVideo", - - // Video types - MOVIE: "Movie", - SERIES: "Series", - SEASON: "Season", - EPISODE: "Episode", - - // Playlist - PLAYLIST: "Playlist", -} as const; - -/** - * Predefined item type groups for easy filtering - */ -export const ITEM_TYPE_GROUPS = { - audio: [ITEM_TYPES.AUDIO, ITEM_TYPES.MUSIC_ALBUM, ITEM_TYPES.MUSIC_ARTIST], - music: [ITEM_TYPES.AUDIO, ITEM_TYPES.MUSIC_ALBUM, ITEM_TYPES.MUSIC_ARTIST], - video: [ITEM_TYPES.MOVIE, ITEM_TYPES.SERIES, ITEM_TYPES.EPISODE], - movies: [ITEM_TYPES.MOVIE], - tvshows: [ITEM_TYPES.SERIES, ITEM_TYPES.SEASON, ITEM_TYPES.EPISODE], - episodes: [ITEM_TYPES.EPISODE], -} as const; diff --git a/src/lib/utils/playbackUnits.ts b/src/lib/utils/playbackUnits.ts index 3ed4cd44..b1fe4ff1 100644 --- a/src/lib/utils/playbackUnits.ts +++ b/src/lib/utils/playbackUnits.ts @@ -1,29 +1,31 @@ /** - * Playback unit conversion utilities + * Playback unit utilities. * - * Jellyfin uses "ticks" for time values where 10 million ticks = 1 second. - * This module provides type-safe conversion functions to eliminate magic numbers - * and prevent conversion bugs across the codebase. + * The app's own media model speaks milliseconds (see the domain migration in + * docs/specs/frontend-domain-model.md), so catalog code does NOT use the tick + * helpers here. Ticks survive only at the **remote Jellyfin session boundary** — + * `SessionInfo.playState.positionTicks` and `NowPlayingItem.runTimeTicks` arrive + * straight from a live Jellyfin session and are converted here for display. + * `formatTime`/`calculateProgress` are plain seconds-based presentation helpers. */ /** - * Number of Jellyfin ticks per second (10 million) + * Number of Jellyfin ticks per second (10 million). + * + * Only for the remote-session boundary (see module doc); catalog durations are + * milliseconds and never touch this. */ export const TICKS_PER_SECOND = 10_000_000; /** - * Convert seconds to Jellyfin ticks - * @param seconds - Time in seconds (e.g., 90.5 for 1 minute 30.5 seconds) - * @returns Time in Jellyfin ticks + * Convert seconds to Jellyfin ticks. Remote-session boundary only. */ export function secondsToTicks(seconds: number): number { return Math.floor(seconds * TICKS_PER_SECOND); } /** - * Convert Jellyfin ticks to seconds - * @param ticks - Time in Jellyfin ticks - * @returns Time in seconds + * Convert Jellyfin session ticks to seconds. Remote-session boundary only. */ export function ticksToSeconds(ticks: number | null | undefined): number { return (ticks ?? 0) / TICKS_PER_SECOND;