From ec8a7610f509897e7f95fe6028558006652ae9b4 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 23 Jul 2026 22:02:29 +0200 Subject: [PATCH] domain: player/reporting ticks -> milliseconds (phase 4c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src-tauri/src/commands/repository.rs | 10 ++++--- src-tauri/src/commands/storage/mod.rs | 26 +++++++++++------- src/lib/api/bindings.ts | 27 +++++++++++-------- src/lib/api/repository-client.test.ts | 2 +- src/lib/api/repository-client.ts | 12 ++++----- src/lib/components/player/Queue.svelte | 6 ++--- .../VideoPlayer.scrubRegression.test.ts | 4 +-- src/lib/components/player/VideoPlayer.svelte | 8 +++--- src/lib/services/playbackReporting.test.ts | 18 ++++++------- src/lib/services/playbackReporting.ts | 18 ++++++------- .../services/playerEvents.regression.test.ts | 10 +++---- src/lib/services/playerEvents.ts | 2 +- src/lib/services/syncService.ts | 6 ++--- src/routes/player/[id]/+page.svelte | 10 +++---- 14 files changed, 88 insertions(+), 71 deletions(-) diff --git a/src-tauri/src/commands/repository.rs b/src-tauri/src/commands/repository.rs index 63d11cbd..6f2028ef 100644 --- a/src-tauri/src/commands/repository.rs +++ b/src-tauri/src/commands/repository.rs @@ -579,8 +579,9 @@ pub async fn repository_report_playback_start( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, - position_ticks: i64, + position_ms: i64, ) -> Result<(), String> { + let position_ticks = position_ms * 10_000; let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref() .report_playback_start(&item_id, position_ticks) @@ -595,8 +596,9 @@ pub async fn repository_report_playback_progress( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, - position_ticks: i64, + position_ms: i64, ) -> Result<(), String> { + let position_ticks = position_ms * 10_000; let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref() .report_playback_progress(&item_id, position_ticks) @@ -611,8 +613,10 @@ pub async fn repository_report_playback_stopped( manager: State<'_, RepositoryManagerWrapper>, handle: String, item_id: String, - position_ticks: i64, + position_ms: i64, ) -> Result<(), String> { + // Milliseconds across the boundary; the Jellyfin API wants ticks. + let position_ticks = position_ms * 10_000; let repo = manager.0.get(&handle).ok_or("Repository not found")?; repo.as_ref() .report_playback_stopped(&item_id, position_ticks) diff --git a/src-tauri/src/commands/storage/mod.rs b/src-tauri/src/commands/storage/mod.rs index f537a2b6..51c72547 100644 --- a/src-tauri/src/commands/storage/mod.rs +++ b/src-tauri/src/commands/storage/mod.rs @@ -583,7 +583,9 @@ pub async fn storage_delete_user( #[serde(rename_all = "camelCase")] pub struct PlaybackProgress { pub item_id: String, - pub position_ticks: i64, + /// Resume position in milliseconds. Stored as Jellyfin ticks in the DB; + /// converted here so the frontend never sees ticks. + pub position_ms: i64, pub is_played: bool, pub is_favorite: bool, pub play_count: i32, @@ -597,8 +599,11 @@ pub async fn storage_update_playback_progress( db: State<'_, DatabaseWrapper>, user_id: String, item_id: String, - position_ticks: i64, + position_ms: i64, ) -> Result<(), String> { + // The frontend speaks milliseconds; ticks are a Jellyfin storage detail that + // stays on this side of the boundary. 10_000 ticks = 1 ms. + let position_ticks = position_ms * 10_000; let db_service = { let database = db.0.lock().map_err(|e| e.to_string())?; Arc::new(database.service()) @@ -649,12 +654,14 @@ pub async fn storage_update_playback_context( db: State<'_, DatabaseWrapper>, user_id: String, item_id: String, - position_ticks: i64, + position_ms: i64, context_type: Option, context_id: Option, ) -> Result<(), String> { use crate::storage::db_service::{Query, QueryParam}; + // Milliseconds in, Jellyfin ticks stored. 10_000 ticks = 1 ms. + let position_ticks = position_ms * 10_000; let db_service = { let database = db.0.lock().map_err(|e| e.to_string())?; Arc::new(database.service()) @@ -857,9 +864,10 @@ pub async fn storage_get_playback_progress( db_service .query_optional(query, |row| { + let position_ticks: i64 = row.get(1)?; Ok(PlaybackProgress { item_id: row.get(0)?, - position_ticks: row.get(1)?, + position_ms: position_ticks / 10_000, is_played: row.get::<_, i32>(2)? != 0, is_favorite: row.get::<_, i32>(3)? != 0, play_count: row.get(4)?, @@ -1495,7 +1503,7 @@ mod tests { fn test_playback_progress_serialization() { let progress = PlaybackProgress { item_id: "item-123".to_string(), - position_ticks: 150_000_000, + position_ms: 150_000_000, is_played: true, is_favorite: false, play_count: 3, @@ -1512,7 +1520,7 @@ mod tests { fn test_playback_progress_played_status() { let progress = PlaybackProgress { item_id: "item-456".to_string(), - position_ticks: 0, + position_ms: 0, is_played: true, is_favorite: true, play_count: 1, @@ -1530,7 +1538,7 @@ mod tests { fn test_playback_progress_not_played() { let progress = PlaybackProgress { item_id: "item-789".to_string(), - position_ticks: 30_000_000, + position_ms: 30_000_000, is_played: false, is_favorite: false, play_count: 0, @@ -1600,7 +1608,7 @@ mod tests { fn test_playback_progress_camel_case() { let progress = PlaybackProgress { item_id: "i1".to_string(), - position_ticks: 100, + position_ms: 100, is_played: true, is_favorite: false, play_count: 1, @@ -1609,7 +1617,7 @@ mod tests { let json = serde_json::to_string(&progress).unwrap(); // Verify camelCase serialization assert!(json.contains("itemId")); - assert!(json.contains("positionTicks")); + assert!(json.contains("positionMs")); assert!(json.contains("isPlayed")); assert!(json.contains("isFavorite")); assert!(json.contains("playCount")); diff --git a/src/lib/api/bindings.ts b/src/lib/api/bindings.ts index 45e5d5de..c8890a02 100644 --- a/src/lib/api/bindings.ts +++ b/src/lib/api/bindings.ts @@ -670,15 +670,15 @@ async storageDeleteUser(userId: string) : Promise { * Update playback progress in local database * This stores the progress locally for offline access and "continue watching" */ -async storageUpdatePlaybackProgress(userId: string, itemId: string, positionTicks: number) : Promise { - return await TAURI_INVOKE("storage_update_playback_progress", { userId, itemId, positionTicks }); +async storageUpdatePlaybackProgress(userId: string, itemId: string, positionMs: number) : Promise { + return await TAURI_INVOKE("storage_update_playback_progress", { userId, itemId, positionMs }); }, /** * Update playback progress with context in local database * This stores the progress along with playback context (container vs single) */ -async storageUpdatePlaybackContext(userId: string, itemId: string, positionTicks: number, contextType: string | null, contextId: string | null) : Promise { - return await TAURI_INVOKE("storage_update_playback_context", { userId, itemId, positionTicks, contextType, contextId }); +async storageUpdatePlaybackContext(userId: string, itemId: string, positionMs: number, contextType: string | null, contextId: string | null) : Promise { + return await TAURI_INVOKE("storage_update_playback_context", { userId, itemId, positionMs, contextType, contextId }); }, /** * Mark item as played in local database @@ -1332,20 +1332,20 @@ async repositoryOpenLiveStream(handle: string, itemId: string) : Promise { - return await TAURI_INVOKE("repository_report_playback_start", { handle, itemId, positionTicks }); +async repositoryReportPlaybackStart(handle: string, itemId: string, positionMs: number) : Promise { + return await TAURI_INVOKE("repository_report_playback_start", { handle, itemId, positionMs }); }, /** * Report playback progress */ -async repositoryReportPlaybackProgress(handle: string, itemId: string, positionTicks: number) : Promise { - return await TAURI_INVOKE("repository_report_playback_progress", { handle, itemId, positionTicks }); +async repositoryReportPlaybackProgress(handle: string, itemId: string, positionMs: number) : Promise { + return await TAURI_INVOKE("repository_report_playback_progress", { handle, itemId, positionMs }); }, /** * Report playback stopped */ -async repositoryReportPlaybackStopped(handle: string, itemId: string, positionTicks: number) : Promise { - return await TAURI_INVOKE("repository_report_playback_stopped", { handle, itemId, positionTicks }); +async repositoryReportPlaybackStopped(handle: string, itemId: string, positionMs: number) : Promise { + return await TAURI_INVOKE("repository_report_playback_stopped", { handle, itemId, positionMs }); }, /** * Get image URL for an item @@ -2070,7 +2070,12 @@ export type PlaybackMode = { type: "local" } | { type: "remote"; session_id: str /** * Playback progress info */ -export type PlaybackProgress = { itemId: string; positionTicks: number; isPlayed: boolean; isFavorite: boolean; playCount: number } +export type PlaybackProgress = { itemId: string; +/** + * Resume position in milliseconds. Stored as Jellyfin ticks in the DB; + * converted here so the frontend never sees ticks. + */ +positionMs: number; isPlayed: boolean; isFavorite: boolean; playCount: number } /** * Represents a media item that can be played * diff --git a/src/lib/api/repository-client.test.ts b/src/lib/api/repository-client.test.ts index a63dc352..d657b798 100644 --- a/src/lib/api/repository-client.test.ts +++ b/src/lib/api/repository-client.test.ts @@ -470,7 +470,7 @@ describe("RepositoryClient", () => { expect(invoke).toHaveBeenCalledWith("repository_report_playback_progress", { handle: "test-handle-123", itemId: "item123", - positionTicks: 5000000, + positionMs: 5000000, }); }); }); diff --git a/src/lib/api/repository-client.ts b/src/lib/api/repository-client.ts index 13461454..64063e8e 100644 --- a/src/lib/api/repository-client.ts +++ b/src/lib/api/repository-client.ts @@ -164,16 +164,16 @@ export class RepositoryClient { return commands.repositoryGetPlaybackInfo(this.ensureHandle(), itemId); } - async reportPlaybackStart(itemId: string, positionTicks: number): Promise { - await commands.repositoryReportPlaybackStart(this.ensureHandle(), itemId, positionTicks); + async reportPlaybackStart(itemId: string, positionMs: number): Promise { + await commands.repositoryReportPlaybackStart(this.ensureHandle(), itemId, positionMs); } - async reportPlaybackProgress(itemId: string, positionTicks: number): Promise { - await commands.repositoryReportPlaybackProgress(this.ensureHandle(), itemId, positionTicks); + async reportPlaybackProgress(itemId: string, positionMs: number): Promise { + await commands.repositoryReportPlaybackProgress(this.ensureHandle(), itemId, positionMs); } - async reportPlaybackStopped(itemId: string, positionTicks: number): Promise { - await commands.repositoryReportPlaybackStopped(this.ensureHandle(), itemId, positionTicks); + async reportPlaybackStopped(itemId: string, positionMs: number): Promise { + await commands.repositoryReportPlaybackStopped(this.ensureHandle(), itemId, positionMs); } // ===== Stream URL Methods (via Rust) ===== diff --git a/src/lib/components/player/Queue.svelte b/src/lib/components/player/Queue.svelte index c1a522b8..942a4bff 100644 --- a/src/lib/components/player/Queue.svelte +++ b/src/lib/components/player/Queue.svelte @@ -36,9 +36,9 @@ let dragDisabled = $state(true); const flipDurationMs = 200; - 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 mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, "0")}`; diff --git a/src/lib/components/player/VideoPlayer.scrubRegression.test.ts b/src/lib/components/player/VideoPlayer.scrubRegression.test.ts index fe16539d..f2f53ac8 100644 --- a/src/lib/components/player/VideoPlayer.scrubRegression.test.ts +++ b/src/lib/components/player/VideoPlayer.scrubRegression.test.ts @@ -97,8 +97,8 @@ function makeEpisode(): MediaItem { return { id: "ep1", name: "Episode 1", - type: "Episode", - runTimeTicks: 24 * 60 * 10_000_000, // 24 min + kind: "episode", + durationMs: 24 * 60 * 1000, // 24 min } as MediaItem; } diff --git a/src/lib/components/player/VideoPlayer.svelte b/src/lib/components/player/VideoPlayer.svelte index 21857d14..11f7e2b0 100644 --- a/src/lib/components/player/VideoPlayer.svelte +++ b/src/lib/components/player/VideoPlayer.svelte @@ -165,9 +165,9 @@ // Use known duration from media item (runTimeTicks is in 10M ticks/second) // Fallback to video element duration for direct streams const duration = $derived.by(() => { - // Explicitly check if runTimeTicks exists and is a valid number - if (media && media.runTimeTicks && media.runTimeTicks > 0) { - return media.runTimeTicks / 10_000_000; + // Explicitly check if durationMs exists and is a valid number + if (media && media.durationMs && media.durationMs > 0) { + return media.durationMs / 1000; } // Otherwise use the video element's duration return videoDuration; @@ -386,7 +386,7 @@ // Check if we're near the end of the video - if so, this is likely // end-of-stream rather than a real error. Jellyfin transcoded HLS // streams may not always terminate cleanly with #EXT-X-ENDLIST. - const knownDuration = media?.runTimeTicks ? media.runTimeTicks / 10_000_000 : videoDuration; + const knownDuration = media?.durationMs ? media.durationMs / 1000 : videoDuration; const effectiveTime = currentTime + seekOffset; const isNearEnd = knownDuration > 0 && effectiveTime > 0 && effectiveTime / knownDuration > 0.9; diff --git a/src/lib/services/playbackReporting.test.ts b/src/lib/services/playbackReporting.test.ts index 8e584737..bfe5b420 100644 --- a/src/lib/services/playbackReporting.test.ts +++ b/src/lib/services/playbackReporting.test.ts @@ -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); diff --git a/src/lib/services/playbackReporting.ts b/src/lib/services/playbackReporting.ts index fcc04225..e2307721 100644 --- a/src/lib/services/playbackReporting.ts +++ b/src/lib/services/playbackReporting.ts @@ -26,7 +26,7 @@ export async function reportPlaybackStart( contextType: "container" | "single" = "single", contextId: string | null = null ): Promise { - const positionTicks = Math.floor(positionSeconds * 10000000); + const positionMs = Math.floor(positionSeconds * 1000); const userId = auth.getUserId(); console.log( @@ -42,7 +42,7 @@ export async function reportPlaybackStart( // Update local DB with context (always works, even offline) if (userId) { try { - await commands.storageUpdatePlaybackContext(userId, itemId, positionTicks, contextType, contextId); + await commands.storageUpdatePlaybackContext(userId, itemId, positionMs, contextType, contextId); } catch (e) { console.error("[PlaybackReporting] Failed to update playback context:", e); } @@ -62,7 +62,7 @@ export async function reportPlaybackProgress( positionSeconds: number, _isPaused = false ): Promise { - const positionTicks = Math.floor(positionSeconds * 10000000); + const positionMs = Math.floor(positionSeconds * 1000); const userId = auth.getUserId(); // Reduce logging for frequent progress updates @@ -73,7 +73,7 @@ export async function reportPlaybackProgress( // Update local DB only (progress updates are frequent, don't report to server) if (userId) { try { - await commands.storageUpdatePlaybackProgress(userId, itemId, positionTicks); + await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs); } catch (e) { console.error("[PlaybackReporting] Failed to update local progress:", e); } @@ -89,7 +89,7 @@ export async function reportPlaybackProgress( * TRACES: UR-005, UR-025 | DR-028 */ export async function reportPlaybackStopped(itemId: string, positionSeconds: number): Promise { - const positionTicks = Math.floor(positionSeconds * 10000000); + const positionMs = Math.floor(positionSeconds * 1000); const userId = auth.getUserId(); console.log("[PlaybackReporting] reportPlaybackStopped - itemId:", itemId, "positionSeconds:", positionSeconds); @@ -97,7 +97,7 @@ export async function reportPlaybackStopped(itemId: string, positionSeconds: num // Update local DB first (always works, even offline) if (userId) { try { - await commands.storageUpdatePlaybackProgress(userId, itemId, positionTicks); + await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs); } catch (e) { console.error("[PlaybackReporting] Failed to update local progress:", e); } @@ -108,7 +108,7 @@ export async function reportPlaybackStopped(itemId: string, positionSeconds: num try { // Get the repository to check if we should queue const repo = auth.getRepository(); - await repo.reportPlaybackStopped(itemId, positionTicks); + await repo.reportPlaybackStopped(itemId, positionMs); } catch (e) { console.error("[PlaybackReporting] Failed to report to server:", e); // Server error - could queue, but for now just log @@ -140,8 +140,8 @@ export async function markAsPlayed(itemId: string): Promise { const repo = auth.getRepository(); const item = await repo.getItem(itemId); - if (item.runTimeTicks) { - await repo.reportPlaybackStopped(itemId, item.runTimeTicks); + if (item.durationMs) { + await repo.reportPlaybackStopped(itemId, item.durationMs); } } catch (e) { console.error("[PlaybackReporting] Failed to report as played:", e); diff --git a/src/lib/services/playerEvents.regression.test.ts b/src/lib/services/playerEvents.regression.test.ts index 161e396b..b64be386 100644 --- a/src/lib/services/playerEvents.regression.test.ts +++ b/src/lib/services/playerEvents.regression.test.ts @@ -73,8 +73,8 @@ function makeItem(overrides: Partial = {}): MediaItem { return { id: "track-1", name: "Test Track", - type: "Audio", - runTimeTicks: null, + kind: "track", + durationMs: null, ...overrides, } as MediaItem; } @@ -102,7 +102,7 @@ describe("Player Events — pause must not zero the slider duration", () => { it("preserves the live duration across pause when runTimeTicks is missing", async () => { // runTimeTicks is null — the previous code recomputed duration as 0 here, // which collapsed the slider's max and snapped the thumb to the start. - const item = makeItem({ runTimeTicks: null }); + const item = makeItem({ durationMs: null }); currentQueueItemStore.set(item); const { initPlayerEvents } = await import("./playerEvents"); @@ -130,8 +130,8 @@ describe("Player Events — pause must not zero the slider duration", () => { }); it("falls back to the runTimeTicks estimate when no live duration is known yet", async () => { - // 70s in ticks (1 tick = 100ns) → 700_000_000. - const item = makeItem({ runTimeTicks: 700_000_000 }); + // 70 seconds = 70_000 ms. + const item = makeItem({ durationMs: 70_000 }); currentQueueItemStore.set(item); const { initPlayerEvents } = await import("./playerEvents"); diff --git a/src/lib/services/playerEvents.ts b/src/lib/services/playerEvents.ts index 63ba939d..8f68aef3 100644 --- a/src/lib/services/playerEvents.ts +++ b/src/lib/services/playerEvents.ts @@ -170,7 +170,7 @@ function handlePositionUpdate(position: number, duration: number): void { * with 0 when runTimeTicks is missing (which would zero the slider's max). */ function resolveDuration(currentItem: MediaItem, isSameTrack: boolean): number { - const estimate = currentItem.runTimeTicks ? currentItem.runTimeTicks / 10000000 : 0; + const estimate = currentItem.durationMs ? currentItem.durationMs / 1000 : 0; if (isSameTrack) { const live = get(playbackDuration); if (live > 0) { diff --git a/src/lib/services/syncService.ts b/src/lib/services/syncService.ts index e356e407..8ab89b41 100644 --- a/src/lib/services/syncService.ts +++ b/src/lib/services/syncService.ts @@ -104,12 +104,12 @@ class SyncService { */ async queuePlaybackProgress( itemId: string, - positionTicks: number + positionMs: number ): Promise { // Update local state first - await commands.storageUpdatePlaybackProgress(auth.getUserId() ?? "", itemId, positionTicks); + await commands.storageUpdatePlaybackProgress(auth.getUserId() ?? "", itemId, positionMs); - return this.queueMutation("update_progress", itemId, { positionTicks }); + return this.queueMutation("update_progress", itemId, { positionMs }); } /** diff --git a/src/routes/player/[id]/+page.svelte b/src/routes/player/[id]/+page.svelte index 7e63b9aa..4b982fe8 100644 --- a/src/routes/player/[id]/+page.svelte +++ b/src/routes/player/[id]/+page.svelte @@ -185,9 +185,9 @@ const progress = await commands.storageGetPlaybackProgress(userId, id); console.log("Resume check - retrieved progress:", progress); - if (progress && progress.positionTicks > 0 && item.runTimeTicks) { - const positionSeconds = progress.positionTicks / 10_000_000; - const totalSeconds = item.runTimeTicks / 10_000_000; + if (progress && progress.positionMs > 0 && item.durationMs) { + const positionSeconds = progress.positionMs / 1000; + const totalSeconds = item.durationMs / 1000; const progressPercent = (positionSeconds / totalSeconds) * 100; console.log("Resume check - positionSeconds:", positionSeconds, "totalSeconds:", totalSeconds, "progressPercent:", progressPercent); @@ -206,7 +206,7 @@ console.log("Resume check - NOT showing dialog. Position > 30?", positionSeconds > 30, "Progress < 90?", progressPercent < 90); } } else { - console.log("Resume check - No valid progress found. Has progress?", !!progress, "Has position?", progress?.positionTicks, "Has runtime?", !!item.runTimeTicks); + console.log("Resume check - No valid progress found. Has progress?", !!progress, "Has position?", progress?.positionMs, "Has runtime?", !!item.durationMs); } } catch (e) { console.error("Failed to check saved progress:", e); @@ -354,7 +354,7 @@ title: t.name, artist: t.artists?.join(", ") || null, album: t.albumName || null, - duration: t.runTimeTicks ? t.runTimeTicks / 10000000 : null, + duration: t.durationMs ? t.durationMs / 1000 : null, artworkUrl: t.imageId ? repo.getImageUrl(t.albumId || t.id, "Primary", { maxWidth: 300, tag: t.imageId }) : null,