domain: player/reporting ticks -> milliseconds (phase 4c)

Playback position now crosses the IPC boundary in milliseconds. Ticks
survive only inside Rust (DB storage, Jellyfin API) and at the genuine
remote-session boundary (session seek / transfer / RemoteControls).

Rust command signatures (ms in, converted to ticks internally):
- storage_update_playback_progress / _context: position_ms
- repository_report_playback_start / _progress / _stopped: position_ms
- PlaybackProgress.position_ticks -> position_ms (converted in the query)

Frontend:
- playbackReporting, playerEvents, VideoPlayer, Queue, player/[id] resume:
  seconds*1000 / durationMs/1000 instead of tick math.
- repository-client + syncService param names -> positionMs.
- Tests updated to ms fixtures/assertions.

Out of scope (legitimately ticks): NowPlayingItem, PlayState.positionTicks,
sessionSeek, playbackModeTransferToLocal, RemoteControls, SessionCard — the
remote Jellyfin session API.

Rust 456, frontend 644, check clean.
This commit is contained in:
2026-07-23 22:02:29 +02:00
parent 93d198ce21
commit ec8a7610f5
14 changed files with 88 additions and 71 deletions
+9 -9
View File
@@ -29,7 +29,7 @@ vi.mock("$lib/stores/auth", () => ({
getItem: vi.fn(async (id: string) => ({
id,
name: "Test Item",
runTimeTicks: 100000000,
durationMs: 10000,
})),
})),
},
@@ -61,7 +61,7 @@ describe("playback reporting service", () => {
(c) => c[0] === "storage_update_playback_context"
);
expect(call).toBeDefined();
expect(call![1]).toHaveProperty("positionTicks", 600000000); // 60 seconds
expect(call![1]).toHaveProperty("positionMs", 60000); // 60 seconds
});
it("should use single context by default", async () => {
@@ -121,7 +121,7 @@ describe("playback reporting service", () => {
const call = invokeSpy.mock.calls.find(
(c) => c[0] === "storage_update_playback_progress"
);
expect(call![1]).toHaveProperty("positionTicks", 450000000); // 45 seconds
expect(call![1]).toHaveProperty("positionMs", 45000); // 45 seconds
});
});
@@ -155,7 +155,7 @@ describe("playback reporting service", () => {
expect(mockRepo.reportPlaybackStopped).toHaveBeenCalled();
});
it("should convert seconds to ticks for server report", async () => {
it("should convert seconds to milliseconds for server report", async () => {
const { auth } = await import("$lib/stores/auth");
const authModule = vi.mocked(auth);
const mockRepo = {
@@ -167,7 +167,7 @@ describe("playback reporting service", () => {
expect(mockRepo.reportPlaybackStopped).toHaveBeenCalledWith(
"item-123",
900000000 // 90 seconds in ticks
90000 // 90 seconds in ms
);
});
@@ -207,7 +207,7 @@ describe("playback reporting service", () => {
getItem: vi.fn(async () => ({
id: "item-123",
name: "Item",
runTimeTicks: 100000000,
durationMs: 10000,
})),
};
authModule.getRepository = vi.fn(() => mockRepo as any);
@@ -217,11 +217,11 @@ describe("playback reporting service", () => {
expect(mockRepo.getItem).toHaveBeenCalledWith("item-123");
expect(mockRepo.reportPlaybackStopped).toHaveBeenCalledWith(
"item-123",
100000000
10000
);
});
it("should handle items without runTimeTicks", async () => {
it("should handle items without durationMs", async () => {
const { auth } = await import("$lib/stores/auth");
const authModule = vi.mocked(auth);
const mockRepo = {
@@ -229,7 +229,7 @@ describe("playback reporting service", () => {
getItem: vi.fn(async () => ({
id: "item-123",
name: "Item",
runTimeTicks: null,
durationMs: null,
})),
};
authModule.getRepository = vi.fn(() => mockRepo as any);