Files
jellytau/src/lib/stores/queue.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

201 lines
5.3 KiB
TypeScript

// Queue state store - event-driven view of Rust player queue
//
// This store listens for queue_changed events from the Rust backend
// and provides reactive state for the frontend. All business logic
// (shuffle order, next/previous calculations, etc.) is handled by Rust.
//
// TRACES: UR-005, UR-015 | DR-005, DR-020
import { writable, derived, get } from "svelte/store";
import { commands, events } from "$lib/api/bindings";
import type { MediaItem } from "$lib/api/types";
import { auth } from "$lib/stores/auth";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("Queue");
export type RepeatMode = "off" | "all" | "one";
interface QueueState {
items: MediaItem[];
currentIndex: number | null;
shuffle: boolean;
repeat: RepeatMode;
hasNext: boolean;
hasPrevious: boolean;
}
interface QueueChangedEvent {
items: MediaItem[];
currentIndex: number | null;
shuffle: boolean;
repeat: RepeatMode;
hasNext: boolean;
hasPrevious: boolean;
}
function createQueueStore() {
const initialState: QueueState = {
items: [],
currentIndex: null,
shuffle: false,
repeat: "off",
hasNext: false,
hasPrevious: false,
};
const { subscribe, set } = writable<QueueState>(initialState);
// Listen for queue changed events from Rust backend
let unlisten: (() => void) | null = null;
async function init() {
// Initial sync from backend
await syncFromRust();
// Listen for queue changed events (generated tauri-specta event)
unlisten = await events.playerStatusEvent.listen((event) => {
if (event.payload.type === "queue_changed") {
const queueEvent = event.payload;
set({
items: queueEvent.items as unknown as MediaItem[],
currentIndex: queueEvent.current_index,
shuffle: queueEvent.shuffle,
repeat: queueEvent.repeat,
hasNext: queueEvent.has_next,
hasPrevious: queueEvent.has_previous,
});
}
});
}
/**
* Sync queue state from Rust backend (for initial load)
*/
async function syncFromRust(): Promise<void> {
try {
const rustQueue = (await commands.playerGetQueue()) as unknown as QueueChangedEvent;
log.debug("Synced from Rust - items:", rustQueue.items.length);
set({
items: rustQueue.items,
currentIndex: rustQueue.currentIndex,
shuffle: rustQueue.shuffle,
repeat: rustQueue.repeat,
hasNext: rustQueue.hasNext,
hasPrevious: rustQueue.hasPrevious,
});
} catch (error) {
log.error("Failed to sync from Rust:", error);
}
}
/**
* Clean up event listener
*/
function cleanup() {
if (unlisten) {
unlisten();
unlisten = null;
}
}
// Initialize on creation
init();
// All queue operations now invoke backend commands
// Backend handles all business logic and emits events
// TRACES: UR-005, UR-015 | DR-005
async function next() {
await commands.playerNext();
}
// TRACES: UR-005, UR-015 | DR-005
async function previous() {
await commands.playerPrevious();
}
// TRACES: UR-005, UR-015 | DR-005, DR-020
async function skipTo(index: number) {
await commands.playerSkipTo(index);
}
// TRACES: UR-005, UR-015 | DR-005
async function toggleShuffle() {
await commands.playerToggleShuffle();
}
// TRACES: UR-005, UR-015 | DR-005
async function cycleRepeat() {
await commands.playerCycleRepeat();
}
// TRACES: UR-015 | DR-020
async function removeFromQueue(index: number) {
await commands.playerRemoveFromQueue(index);
}
// TRACES: UR-015 | DR-020
async function moveInQueue(fromIndex: number, toIndex: number) {
await commands.playerMoveInQueue(fromIndex, toIndex);
}
// TRACES: UR-015 | DR-020
async function addToQueue(items: MediaItem | MediaItem[], position: "next" | "end" = "end") {
const toAdd = Array.isArray(items) ? items : [items];
const trackIds = toAdd.map((item) => item.id);
// Get repository handle from auth store
const authState = get(auth);
if (!authState.isAuthenticated) {
throw new Error("User not authenticated");
}
const repositoryHandle = auth.getRepository().getHandle();
// Use new Rust commands that accept IDs only
if (trackIds.length === 1) {
await commands.playerAddTrackById(repositoryHandle, {
trackId: trackIds[0],
position,
});
} else {
await commands.playerAddTracksByIds(repositoryHandle, {
trackIds,
position,
});
}
}
async function clear() {
set(initialState);
}
return {
subscribe,
next,
previous,
skipTo,
toggleShuffle,
cycleRepeat,
addToQueue,
removeFromQueue,
moveInQueue,
syncFromRust,
cleanup,
clear,
};
}
export const queue = createQueueStore();
// Derived stores for convenience
export const queueItems = derived(queue, ($q) => $q.items);
export const currentQueueIndex = derived(queue, ($q) => $q.currentIndex);
export const currentQueueItem = derived(queue, ($q) =>
$q.currentIndex !== null ? $q.items[$q.currentIndex] : null
);
export const isShuffle = derived(queue, ($q) => $q.shuffle);
export const repeatMode = derived(queue, ($q) => $q.repeat);
export const hasNext = derived(queue, ($q) => $q.hasNext);
export const hasPrevious = derived(queue, ($q) => $q.hasPrevious);