Files
jellytau/src/lib/stores/sessions.ts
T
dtourolle ad48d89dfe chore(format): run prettier over src/ and scripts/
Formatting was configured but never enforced: `bun run format:check`
reported 199 unformatted files and ran in no workflow and in no git hook,
so .prettierrc (printWidth 100, trailing commas) described an intention
rather than the tree.

This is the one-time sweep that makes the check gateable. Whitespace and
token-reflow only -- no behavioural change: `bun run check` reports 0
errors and all 1053 frontend tests pass before and after.

Kept out of every other commit on purpose. A 199-file diff mixed with
real changes is unreviewable, and the next commit turns format:check
into a hard CI gate so this cannot silently accumulate again.
2026-08-21 17:41:44 +02:00

261 lines
7.0 KiB
TypeScript

// Remote sessions store for controlling playback on other Jellyfin clients
// TRACES: UR-010 | DR-037
import { writable, derived } from "svelte/store";
import { commands, events } from "$lib/api/bindings";
import type { Session } from "$lib/api/types";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("Sessions");
interface SessionsState {
sessions: Session[];
selectedSessionId: string | null;
isLoading: boolean;
error: string | null;
lastUpdated: Date | null;
}
function createSessionsStore() {
const initialState: SessionsState = {
sessions: [],
selectedSessionId: null,
isLoading: false,
error: null,
lastUpdated: null,
};
const { subscribe, update } = writable<SessionsState>(initialState);
// Listen for session updates from Rust backend (generated tauri-specta event)
events.playerStatusEvent.listen((event) => {
if (event.payload.type === "sessions_updated") {
const sessions = event.payload.sessions as unknown as Session[];
log.debug(`Received ${sessions.length} sessions from backend`);
sessions.forEach((s, i) => {
log.debug(
`Session ${i}: id=${s.id}, device=${s.deviceName}, supportsRemoteControl=${s.supportsRemoteControl}`,
);
});
update((s) => ({
...s,
sessions,
lastUpdated: new Date(),
error: null,
}));
}
});
/**
* Manually fetch sessions from backend (for refresh button)
*/
async function refresh(): Promise<void> {
try {
update((s) => ({ ...s, isLoading: true, error: null }));
const sessions = await commands.sessionsPollNow();
log.debug(`Manual refresh returned ${sessions.length} sessions`);
sessions.forEach((s, i) => {
log.debug(
`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,
}));
log.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<void> {
try {
await commands.remoteSendCommand(sessionId ?? "", "PlayPause");
// Refresh after command to get updated state
await refresh();
} catch (error) {
log.error("Failed to send play/pause command:", error);
throw error;
}
}
/**
* Send stop command
*/
async function sendStop(sessionId: string | null | undefined): Promise<void> {
try {
await commands.remoteSendCommand(sessionId ?? "", "Stop");
await refresh();
} catch (error) {
log.error("Failed to send stop command:", error);
throw error;
}
}
/**
* Send next track command
*/
async function sendNext(sessionId: string | null | undefined): Promise<void> {
try {
await commands.remoteSendCommand(sessionId ?? "", "NextTrack");
await refresh();
} catch (error) {
log.error("Failed to send next track command:", error);
throw error;
}
}
/**
* Send previous track command
*/
async function sendPrevious(sessionId: string | null | undefined): Promise<void> {
try {
await commands.remoteSendCommand(sessionId ?? "", "PreviousTrack");
await refresh();
} catch (error) {
log.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<void> {
try {
await commands.remoteSessionSeek(sessionId ?? "", positionTicks);
// Don't refresh immediately for seek to avoid UI lag
} catch (error) {
log.error("Failed to send seek command:", error);
throw error;
}
}
/**
* Set volume (0-100)
*/
async function sendVolume(sessionId: string | null | undefined, volume: number): Promise<void> {
try {
await commands.remoteSessionSetVolume(sessionId ?? "", volume);
// Don't refresh immediately for volume to avoid UI lag
} catch (error) {
log.error("Failed to send volume command:", error);
throw error;
}
}
/**
* Toggle mute
*/
async function sendToggleMute(sessionId: string | null | undefined): Promise<void> {
try {
await commands.remoteSendCommand(sessionId ?? "", "ToggleMute");
await refresh();
} catch (error) {
log.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<void> {
log.debug("========== playOnSession called ==========");
log.debug("sessionId:", sessionId);
log.debug("itemIds array:", itemIds);
log.debug("itemIds.length:", itemIds.length);
log.debug("itemIds JSON:", JSON.stringify(itemIds));
log.debug("startIndex:", startIndex);
log.debug("About to call commands.remotePlayOnSession");
try {
// Use Rust player's Jellyfin client for remote playback
const result = await commands.remotePlayOnSession(sessionId ?? "", itemIds, startIndex);
log.debug("invoke succeeded, result:", result);
await refresh();
} catch (error) {
log.error("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);
log.debug(
`Filtering ${$sessions.sessions.length} total sessions, ${controllable.length} are controllable`,
);
$sessions.sessions.forEach((s, i) => {
const status = s.supportsRemoteControl ? "✓ CONTROLLABLE" : "✗ NOT CONTROLLABLE";
log.debug(
` ${status}: ${s.deviceName} (id=${s.id}, supportsRemoteControl=${s.supportsRemoteControl})`,
);
});
return controllable;
});