domain: catalog frontend off Jellyfin ticks -> milliseconds (phase 3a)

The catalog surface now speaks milliseconds, the app's neutral time unit.
Ticks no longer reach library/home components.

Rust:
- UserData gains playback_position_ms (dual-carry), populated from ticks
  at the offline mapping seam via domain::ticks_to_ms.

Frontend:
- formatDuration(duration.ts) and the two local copies now take ms, not
  ticks; all callers pass item.durationMs.
- Progress bars (EpisodeRow, EpisodeFocusView, MediaCard, LibraryListView)
  compute playbackPositionMs / durationMs — unit-consistent, no tick math.
- PlaylistDetailView totalDuration sums durationMs.
- duration.test.ts + TrackList.test.ts fixtures updated to ms.

Deferred: player/session/reporting tick math (Queue, SessionCard,
RemoteControls, playbackReporting, playerEvents) — those cross the
storage/Jellyfin command boundary in ticks and need command-signature
changes (phase 3b). Display {item.type} badge -> kind label (phase 4).

Rust 456, frontend 644, check + check:boundary clean.
This commit is contained in:
2026-07-23 21:30:04 +02:00
parent 3e962a202c
commit 7660a33dfc
14 changed files with 80 additions and 58 deletions
@@ -86,9 +86,9 @@
return null;
});
function formatDuration(ticks?: number | null): string {
if (!ticks) return "";
const seconds = Math.floor(ticks / 10000000);
function formatDuration(ms?: number | null): string {
if (!ms) return "";
const seconds = Math.floor(ms / 1000);
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
@@ -99,10 +99,10 @@
}
function getProgress(ep: MediaItem): number {
if (!ep.userData || !ep.runTimeTicks) {
if (!ep.userData || !ep.durationMs) {
return 0;
}
return ((ep.userData.playbackPositionTicks ?? 0) / ep.runTimeTicks) * 100;
return ((ep.userData.playbackPositionMs ?? 0) / ep.durationMs) * 100;
}
function handlePlay() {
@@ -116,7 +116,7 @@
const episodeLabel = $derived(
`S${episode.parentIndexNumber || 1}E${episode.indexNumber || 1}`
);
const duration = $derived(formatDuration(episode.runTimeTicks));
const duration = $derived(formatDuration(episode.durationMs));
const progress = $derived(getProgress(episode));
</script>