First working POC
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
// Remote sessions store for controlling playback on other Jellyfin clients
|
||||
|
||||
import { writable, derived } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
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<SessionsState>(initialState);
|
||||
|
||||
// Listen for session updates from Rust backend
|
||||
listen<PlayerStatusEvent>("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<void> {
|
||||
try {
|
||||
update((s) => ({ ...s, isLoading: true, error: null }));
|
||||
|
||||
const sessions = await invoke<Session[]>("sessions_poll_now");
|
||||
|
||||
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): Promise<void> {
|
||||
try {
|
||||
await invoke("remote_send_command", {
|
||||
sessionId,
|
||||
command: "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): Promise<void> {
|
||||
try {
|
||||
await invoke("remote_send_command", {
|
||||
sessionId,
|
||||
command: "Stop",
|
||||
});
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
console.error("Failed to send stop command:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send next track command
|
||||
*/
|
||||
async function sendNext(sessionId: string): Promise<void> {
|
||||
try {
|
||||
await invoke("remote_send_command", {
|
||||
sessionId,
|
||||
command: "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): Promise<void> {
|
||||
try {
|
||||
await invoke("remote_send_command", {
|
||||
sessionId,
|
||||
command: "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, positionTicks: number): Promise<void> {
|
||||
try {
|
||||
await invoke("remote_session_seek", {
|
||||
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, volume: number): Promise<void> {
|
||||
try {
|
||||
await invoke("remote_session_set_volume", {
|
||||
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): Promise<void> {
|
||||
try {
|
||||
await invoke("remote_send_command", {
|
||||
sessionId,
|
||||
command: "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,
|
||||
itemIds: string[],
|
||||
startIndex = 0
|
||||
): Promise<void> {
|
||||
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 invoke('remote_play_on_session')");
|
||||
try {
|
||||
// Use Rust player's Jellyfin client for remote playback
|
||||
const result = await invoke("remote_play_on_session", {
|
||||
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;
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user