Files
jellytau/src/lib/services/playbackReporting.ts
T
dtourolle d54d8cc7c4 refactor(logging): route frontend console calls through the logger
TRACES: | DR-204

484 ungated `console.*` calls across 63 non-test frontend files shipped to
end users with no way to turn them off. Mechanical substitution, no control
flow, error handling or message semantics changed:

  console.log / console.debug -> log.debug
  console.info                -> log.info
  console.warn                -> log.warn
  console.error               -> log.error

Hand-written `"[Scope] …"` prefixes are dropped where the logger's scope
now carries them; scope names that already existed are preserved verbatim
(`[Auth]`, `[VideoPlayer]`, `[PiP]`, …) and inferred from the filename
where a file had none. `src/routes/player/[id]/+page.svelte` keeps its
`NextEpisode` and `AutoPlay` sub-scopes as separate loggers rather than
flattening them into the page scope.

`grep -rn 'console\.' src/` now matches nothing outside the tests and the
facade itself.
2026-08-20 19:29:59 +02:00

162 lines
4.8 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";
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<void> {
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<void> {
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<void> {
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<void> {
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);
}
}