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:
@@ -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>
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@
|
||||
const downloadProgress = $derived(downloadInfo?.progress || 0);
|
||||
|
||||
const progress = $derived(() => {
|
||||
if (!episode.userData || !episode.runTimeTicks) {
|
||||
if (!episode.userData || !episode.durationMs) {
|
||||
return 0;
|
||||
}
|
||||
return ((episode.userData.playbackPositionTicks ?? 0) / episode.runTimeTicks) * 100;
|
||||
return ((episode.userData.playbackPositionMs ?? 0) / episode.durationMs) * 100;
|
||||
});
|
||||
|
||||
const duration = $derived(formatDuration(episode.runTimeTicks));
|
||||
const duration = $derived(formatDuration(episode.durationMs));
|
||||
const episodeNumber = $derived(episode.indexNumber || 0);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -42,10 +42,10 @@
|
||||
|
||||
|
||||
function getProgress(item: MediaItem | Library): number {
|
||||
if (!showProgress || !("userData" in item) || !item.userData || !("runTimeTicks" in item) || !item.runTimeTicks) {
|
||||
if (!showProgress || !("userData" in item) || !item.userData || !("durationMs" in item) || !item.durationMs) {
|
||||
return 0;
|
||||
}
|
||||
return ((item.userData.playbackPositionTicks ?? 0) / item.runTimeTicks) * 100;
|
||||
return ((item.userData.playbackPositionMs ?? 0) / item.durationMs) * 100;
|
||||
}
|
||||
|
||||
function getTrackNumber(item: MediaItem | Library): string {
|
||||
@@ -59,7 +59,7 @@
|
||||
<div class="space-y-1">
|
||||
{#each items as item, index (item.id)}
|
||||
{@const subtitle = getSubtitle(item)}
|
||||
{@const duration = "runTimeTicks" in item ? formatDuration(item.runTimeTicks) : ""}
|
||||
{@const duration = "durationMs" in item ? formatDuration(item.durationMs) : ""}
|
||||
{@const progress = getProgress(item)}
|
||||
{@const trackNum = getTrackNumber(item)}
|
||||
{@const isPlayed = "userData" in item && item.userData?.isPlayed}
|
||||
|
||||
@@ -119,10 +119,10 @@
|
||||
const maxWidth = $derived(size === "large" ? 400 : size === "medium" ? 300 : 200);
|
||||
|
||||
const progress = $derived(() => {
|
||||
if (!showProgress || !("userData" in item) || !item.userData || !item.runTimeTicks) {
|
||||
if (!showProgress || !("userData" in item) || !item.userData || !item.durationMs) {
|
||||
return 0;
|
||||
}
|
||||
return ((item.userData.playbackPositionTicks ?? 0) / item.runTimeTicks) * 100;
|
||||
return ((item.userData.playbackPositionMs ?? 0) / item.durationMs) * 100;
|
||||
});
|
||||
|
||||
const subtitle = $derived(() => {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
const tracks = $derived(entries.map(e => ({ ...e } as MediaItem)));
|
||||
|
||||
const totalDuration = $derived(
|
||||
entries.reduce((sum, e) => sum + (e.runTimeTicks ?? 0), 0)
|
||||
entries.reduce((sum, e) => sum + (e.durationMs ?? 0), 0)
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
|
||||
<!-- Duration -->
|
||||
<div class="text-gray-400 text-right">
|
||||
{formatDuration(track.runTimeTicks)}
|
||||
{formatDuration(track.durationMs)}
|
||||
</div>
|
||||
|
||||
<!-- Download Button Placeholder -->
|
||||
@@ -421,7 +421,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-gray-400 text-sm {showDownload ? 'mr-20' : 'mr-12'}">
|
||||
{formatDuration(track.runTimeTicks)}
|
||||
{formatDuration(track.durationMs)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("TrackList", () => {
|
||||
artists: ["Artist 1"],
|
||||
albumName: "Album 1",
|
||||
albumId: "album-1",
|
||||
runTimeTicks: 1800000000, // 3 minutes
|
||||
durationMs: 180000, // 3 minutes
|
||||
primaryImageTag: "tag1",
|
||||
indexNumber: 1,
|
||||
},
|
||||
@@ -84,7 +84,7 @@ describe("TrackList", () => {
|
||||
artists: ["Artist 2"],
|
||||
albumName: "Album 2",
|
||||
albumId: "album-2",
|
||||
runTimeTicks: 2400000000, // 4 minutes
|
||||
durationMs: 240000, // 4 minutes
|
||||
primaryImageTag: "tag2",
|
||||
indexNumber: 2,
|
||||
},
|
||||
@@ -96,7 +96,7 @@ describe("TrackList", () => {
|
||||
artists: ["Artist 3", "Artist 4"],
|
||||
albumName: "Album 3",
|
||||
albumId: "album-3",
|
||||
runTimeTicks: 3000000000, // 5 minutes
|
||||
durationMs: 300000, // 5 minutes
|
||||
indexNumber: 3,
|
||||
},
|
||||
];
|
||||
@@ -187,7 +187,7 @@ describe("TrackList", () => {
|
||||
const tracksWithoutDuration: MediaItem[] = [
|
||||
{
|
||||
...mockTracks[0],
|
||||
runTimeTicks: undefined,
|
||||
durationMs: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user