// Playback reporting service // // Simplified service that delegates all logic to the Rust backend. // The backend handles: // - Local DB updates // - Jellyfin server reporting // - Offline queueing (via sync queue) // - Connectivity checks // // TRACES: UR-005, UR-019, UR-025 | DR-028, DR-047 import { commands } from "$lib/api/bindings"; import { auth } from "$lib/stores/auth"; import { createLogger } from "$lib/utils/logger"; const log = createLogger("PlaybackReporting"); /** * Record the start of playback **locally**, with the context it started from. * * The server is told by Rust: loading an item into the controller reports the * start, carrying the position the stream actually begins at (zero for an * ordinary play, the handoff point for a background-audio stream). What this * adds is the context — which album or series the play came from — which only * the local DB keeps. * * TRACES: UR-005, UR-025 | DR-028, DR-179 */ export async function reportPlaybackStart( itemId: string, positionSeconds: number, contextType: "container" | "single" = "single", contextId: string | null = null, ): Promise { const positionMs = Math.floor(positionSeconds * 1000); const userId = auth.getUserId(); log.debug( "reportPlaybackStart - itemId:", itemId, "positionSeconds:", positionSeconds, "context:", contextType, contextId, ); // Update local DB with context (always works, even offline) if (userId) { try { await commands.storageUpdatePlaybackContext( userId, itemId, positionMs, contextType, contextId, ); } catch (e) { log.error("Failed to update playback context:", e); } } } /** * Record playback progress **locally**. * * The server's copy is not sent from here. Position ticks already flow into Rust * through the player adapter (`player_report_position`), and the controller * reports them onward on a 30s throttle — one place that covers webview video, * native audio and the background-audio handoff alike, instead of a second * frequent IPC path racing it. * * This function is therefore the *local* half only, which is what its caller * needs for resume points that work offline. * * TRACES: UR-005 | DR-028, DR-179 */ export async function reportPlaybackProgress( itemId: string, positionSeconds: number, _isPaused = false, ): Promise { const positionMs = Math.floor(positionSeconds * 1000); const userId = auth.getUserId(); // Reduce logging for frequent progress updates if (Math.floor(positionSeconds) % 30 === 0) { log.debug("reportPlaybackProgress - itemId:", itemId, "position:", positionSeconds); } // Update local DB only (progress updates are frequent, don't report to server) if (userId) { try { await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs); } catch (e) { log.error("Failed to update local progress:", e); } } } /** * Report playback stopped to Jellyfin (or queue if offline) * * The Rust backend handles both local DB updates and server reporting, * automatically queuing for sync if the server is unreachable. * * TRACES: UR-005, UR-025 | DR-028 */ export async function reportPlaybackStopped( itemId: string, positionSeconds: number, ): Promise { const positionMs = Math.floor(positionSeconds * 1000); const userId = auth.getUserId(); log.debug("reportPlaybackStopped - itemId:", itemId, "positionSeconds:", positionSeconds); // Update local DB first (always works, even offline) if (userId) { try { await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs); } catch (e) { log.error("Failed to update local progress:", e); } } // Report to the server. Rust queues the position for the next reconnect if // the server cannot be reached (DR-154), so a throw here means the report // did not land *this time* — not that the position was lost. if (userId && positionSeconds > 0) { try { const repo = auth.getRepository(); await repo.reportPlaybackStopped(itemId, positionMs); } catch (e) { log.warn("Stop-report did not reach the server; queued for sync:", e); } } } /** * Mark an item as played (100% progress) * * TRACES: UR-025 | DR-028 */ export async function markAsPlayed(itemId: string): Promise { const userId = auth.getUserId(); log.debug("markAsPlayed - itemId:", itemId); // Update local DB first if (userId) { try { await commands.storageMarkPlayed(userId, itemId); } catch (e) { log.error("Failed to mark as played in local DB:", e); } } // Try to report to server via repository (handles queuing internally) try { const repo = auth.getRepository(); const item = await repo.getItem(itemId); if (item.durationMs) { await repo.reportPlaybackStopped(itemId, item.durationMs); } } catch (e) { log.error("Failed to report as played:", e); } }