// Remote sessions store for controlling playback on other Jellyfin clients // TRACES: UR-010 | DR-037 import { writable, derived } from "svelte/store"; import { listen } from "@tauri-apps/api/event"; import { commands } from "$lib/api/bindings"; import type { Session } from "$lib/api/types"; interface SessionsState { sessions: Session[]; selectedSessionId: string | null; isLoading: boolean; error: string | null; lastUpdated: Date | null; } interface PlayerStatusEvent { type: string; sessions?: Session[]; } function createSessionsStore() { const initialState: SessionsState = { sessions: [], selectedSessionId: null, isLoading: false, error: null, lastUpdated: null, }; const { subscribe, update } = writable(initialState); // Listen for session updates from Rust backend listen("player-event", (event) => { if (event.payload.type === "sessions_updated" && event.payload.sessions) { console.log(`[Sessions] Received ${event.payload.sessions.length} sessions from backend`); event.payload.sessions.forEach((s, i) => { console.log(`[Sessions] Session ${i}: id=${s.id}, device=${s.deviceName}, supportsRemoteControl=${s.supportsRemoteControl}`); }); update((s) => ({ ...s, sessions: event.payload.sessions!, lastUpdated: new Date(), error: null, })); } }); /** * Manually fetch sessions from backend (for refresh button) */ async function refresh(): Promise { try { update((s) => ({ ...s, isLoading: true, error: null })); const sessions = await commands.sessionsPollNow(); console.log(`[Sessions] Manual refresh returned ${sessions.length} sessions`); sessions.forEach((s, i) => { console.log(`[Sessions] Session ${i}: id=${s.id}, device=${s.deviceName}, supportsRemoteControl=${s.supportsRemoteControl}`); }); update((s) => ({ ...s, sessions, isLoading: false, lastUpdated: new Date(), error: null, })); } catch (error) { const message = error instanceof Error ? error.message : "Failed to fetch sessions"; update((s) => ({ ...s, isLoading: false, error: message, })); console.error("Failed to fetch sessions:", error); } } /** * Select a session for control */ function selectSession(sessionId: string | null): void { update((s) => ({ ...s, selectedSessionId: sessionId })); } /** * Send play/pause toggle command */ async function sendPlayPause(sessionId: string | null | undefined): Promise { try { await commands.remoteSendCommand(sessionId ?? "", "PlayPause"); // Refresh after command to get updated state await refresh(); } catch (error) { console.error("Failed to send play/pause command:", error); throw error; } } /** * Send stop command */ async function sendStop(sessionId: string | null | undefined): Promise { try { await commands.remoteSendCommand(sessionId ?? "", "Stop"); await refresh(); } catch (error) { console.error("Failed to send stop command:", error); throw error; } } /** * Send next track command */ async function sendNext(sessionId: string | null | undefined): Promise { try { await commands.remoteSendCommand(sessionId ?? "", "NextTrack"); await refresh(); } catch (error) { console.error("Failed to send next track command:", error); throw error; } } /** * Send previous track command */ async function sendPrevious(sessionId: string | null | undefined): Promise { try { await commands.remoteSendCommand(sessionId ?? "", "PreviousTrack"); await refresh(); } catch (error) { console.error("Failed to send previous track command:", error); throw error; } } /** * Seek to position (in ticks) */ async function sendSeek(sessionId: string | null | undefined, positionTicks: number): Promise { try { await commands.remoteSessionSeek(sessionId ?? "", positionTicks); // Don't refresh immediately for seek to avoid UI lag } catch (error) { console.error("Failed to send seek command:", error); throw error; } } /** * Set volume (0-100) */ async function sendVolume(sessionId: string | null | undefined, volume: number): Promise { try { await commands.remoteSessionSetVolume(sessionId ?? "", volume); // Don't refresh immediately for volume to avoid UI lag } catch (error) { console.error("Failed to send volume command:", error); throw error; } } /** * Toggle mute */ async function sendToggleMute(sessionId: string | null | undefined): Promise { try { await commands.remoteSendCommand(sessionId ?? "", "ToggleMute"); await refresh(); } catch (error) { console.error("Failed to toggle mute:", error); throw error; } } /** * Play item(s) on remote session */ async function playOnSession( sessionId: string | null | undefined, itemIds: string[], startIndex = 0 ): Promise { console.log("[SESSIONS] ========== playOnSession called =========="); console.log("[SESSIONS] sessionId:", sessionId); console.log("[SESSIONS] itemIds array:", itemIds); console.log("[SESSIONS] itemIds.length:", itemIds.length); console.log("[SESSIONS] itemIds JSON:", JSON.stringify(itemIds)); console.log("[SESSIONS] startIndex:", startIndex); console.log("[SESSIONS] About to call commands.remotePlayOnSession"); try { // Use Rust player's Jellyfin client for remote playback const result = await commands.remotePlayOnSession(sessionId ?? "", itemIds, startIndex); console.log("[SESSIONS] invoke succeeded, result:", result); await refresh(); } catch (error) { console.error("[SESSIONS] Failed to play on session:", error); throw error; } } return { subscribe, refresh, selectSession, sendPlayPause, sendStop, sendNext, sendPrevious, sendSeek, sendVolume, sendToggleMute, playOnSession, }; } export const sessions = createSessionsStore(); // Derived stores /** * Sessions that are currently playing media */ export const activeSessions = derived( sessions, ($sessions) => $sessions.sessions.filter((s) => s.nowPlayingItem !== null) ); /** * Currently selected session */ export const selectedSession = derived( sessions, ($sessions) => $sessions.sessions.find((s) => s.id === $sessions.selectedSessionId) ?? null ); /** * Controllable sessions (support remote control) */ export const controllableSessions = derived( sessions, ($sessions) => { const controllable = $sessions.sessions.filter((s) => s.supportsRemoteControl); console.log(`[Sessions] Filtering ${$sessions.sessions.length} total sessions, ${controllable.length} are controllable`); $sessions.sessions.forEach((s, i) => { const status = s.supportsRemoteControl ? "✓ CONTROLLABLE" : "✗ NOT CONTROLLABLE"; console.log(`[Sessions] ${status}: ${s.deviceName} (id=${s.id}, supportsRemoteControl=${s.supportsRemoteControl})`); }); return controllable; } );