Files
jellytau/src/lib/services/playbackReporting.ts
dtourolle ec8a7610f5 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.
2026-07-23 22:02:29 +02:00

150 lines
4.4 KiB
TypeScript

// 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";
/**
* Report playback start to Jellyfin (or queue if offline)
*
* The Rust backend handles both local DB updates and server reporting,
* automatically queueing for sync if the server is unreachable.
*
* TRACES: UR-005, UR-025 | DR-028
*/
export async function reportPlaybackStart(
itemId: string,
positionSeconds: number,
contextType: "container" | "single" = "single",
contextId: string | null = null
): Promise<void> {
const positionMs = Math.floor(positionSeconds * 1000);
const userId = auth.getUserId();
console.log(
"[PlaybackReporting] 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) {
console.error("[PlaybackReporting] Failed to update playback context:", e);
}
}
}
/**
* Report playback progress to Jellyfin (or queue if offline)
*
* Note: Progress reports are frequent and are not queued for sync.
* The final position is captured by reportPlaybackStopped.
*
* TRACES: UR-005 | DR-028
*/
export async function reportPlaybackProgress(
itemId: string,
positionSeconds: number,
_isPaused = false
): Promise<void> {
const positionMs = Math.floor(positionSeconds * 1000);
const userId = auth.getUserId();
// Reduce logging for frequent progress updates
if (Math.floor(positionSeconds) % 30 === 0) {
console.log("[PlaybackReporting] 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) {
console.error("[PlaybackReporting] 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<void> {
const positionMs = Math.floor(positionSeconds * 1000);
const userId = auth.getUserId();
console.log("[PlaybackReporting] reportPlaybackStopped - itemId:", itemId, "positionSeconds:", positionSeconds);
// Update local DB first (always works, even offline)
if (userId) {
try {
await commands.storageUpdatePlaybackProgress(userId, itemId, positionMs);
} catch (e) {
console.error("[PlaybackReporting] Failed to update local progress:", e);
}
}
// Queue for sync to server (the sync service will handle retry logic)
if (userId && positionSeconds > 0) {
try {
// Get the repository to check if we should queue
const repo = auth.getRepository();
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
}
}
}
/**
* Mark an item as played (100% progress)
*
* TRACES: UR-025 | DR-028
*/
export async function markAsPlayed(itemId: string): Promise<void> {
const userId = auth.getUserId();
console.log("[PlaybackReporting] markAsPlayed - itemId:", itemId);
// Update local DB first
if (userId) {
try {
await commands.storageMarkPlayed(userId, itemId);
} catch (e) {
console.error("[PlaybackReporting] 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) {
console.error("[PlaybackReporting] Failed to report as played:", e);
}
}