Migrate all IPC call sites to typed tauri-specta commands.*
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 4m39s
Traceability Validation / Check Requirement Traces (pull_request) Failing after 36s
🏗️ Build and Test JellyTau / Build Android APK (pull_request) Failing after 1m57s

Replace the remaining ~155 untyped invoke() calls across stores, services,
components, and routes with the generated commands.* wrappers from
$lib/api/bindings, so every IPC call is compile-time-checked against the
command signatures.

- Register repository_get_subtitle_url and repository_get_video_download_url
  in specta_builder() and the invoke_handler; regenerate bindings.ts.
- Source duplicated wire types (AutoplaySettings, CacheConfig, Session,
  ConnectivityStatus, audio/video settings, etc.) from bindings.
- Fix two bugs surfaced by the typed wrappers:
  - VideoDownloadButton passed an un-awaited Promise as the stream URL.
  - setAutoplaySettings omitted the required userId argument.
- Update unit tests asserting the old invoke(name, args) shape.
- Remove the five param-naming guard tests; the compiler and codegen now
  enforce what they checked.

svelte-check: 0 errors. vitest: green. cargo test --lib: green.
This commit is contained in:
2026-06-21 08:47:04 +02:00
parent 14e9d7e03a
commit d01c2aab9f
47 changed files with 456 additions and 2119 deletions
+56 -100
View File
@@ -6,10 +6,11 @@
// TRACES: UR-009, UR-012 | IR-009, IR-014
import { writable, derived, get } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { commands } from "$lib/api/bindings";
import { RepositoryClient } from "$lib/api/repository-client";
import type { User, AuthResult } from "$lib/api/types";
import type { Session, AuthServerInfo as ServerInfo } from "$lib/api/bindings";
import { connectivity } from "./connectivity";
import { getDeviceId, clearCache as clearDeviceIdCache } from "$lib/services/deviceId";
@@ -29,29 +30,6 @@ interface AuthState {
sessionVerified: boolean;
}
interface Session {
userId: string;
username: string;
serverId: string;
serverUrl: string;
serverName: string;
accessToken: string;
verified: boolean;
needsReauth: boolean;
}
interface ServerInfo {
name: string;
version: string;
id: string;
normalizedUrl: string;
}
interface SecurityStatus {
usingKeyring: boolean;
storageType: string;
}
function createAuthStore() {
const initialState: AuthState = {
isAuthenticated: false,
@@ -163,7 +141,7 @@ function createAuthStore() {
try {
// Check security status
try {
const securityStatus = await invoke<SecurityStatus>("storage_get_security_status");
const securityStatus = await commands.storageGetSecurityStatus();
console.log("[Auth] Security status:", securityStatus);
if (!securityStatus.usingKeyring) {
update((s) => ({
@@ -178,7 +156,7 @@ function createAuthStore() {
// Initialize auth manager and get session
console.log("[Auth] Initializing auth manager...");
const session = await invoke<Session | null>("auth_initialize");
const session = await commands.authInitialize();
console.log("[Auth] Session retrieval result:", session ? "Session found" : "No session found");
if (session) {
@@ -192,12 +170,12 @@ function createAuthStore() {
const deviceId = await getDeviceId();
try {
console.log("[Auth] Configuring Rust player with restored session...");
await invoke("player_configure_jellyfin", {
serverUrl: session.serverUrl,
accessToken: session.accessToken,
userId: session.userId,
deviceId: deviceId,
});
await commands.playerConfigureJellyfin(
session.serverUrl,
session.accessToken,
session.userId,
deviceId
);
console.log("[Auth] Rust player configured for automatic playback reporting");
} catch (error) {
console.error("[Auth] Failed to configure Rust player:", error);
@@ -231,7 +209,7 @@ function createAuthStore() {
// Start background session verification
try {
const verifyDeviceId = await getDeviceId();
await invoke("auth_start_verification", { deviceId: verifyDeviceId });
await commands.authStartVerification(verifyDeviceId);
console.log("[Auth] Background verification started");
} catch (error) {
console.error("[Auth] Failed to start verification:", error);
@@ -273,7 +251,7 @@ function createAuthStore() {
try {
console.log("[Auth] Connecting to server:", serverUrl);
const serverInfo = await invoke<ServerInfo>("auth_connect_to_server", { serverUrl });
const serverInfo = await commands.authConnectToServer(serverUrl);
console.log("[Auth] Connected to server:", serverInfo.name, serverInfo.version);
console.log("[Auth] Normalized URL:", serverInfo.normalizedUrl);
@@ -302,47 +280,32 @@ function createAuthStore() {
const deviceId = await getDeviceId();
console.log("[Auth] Logging in as:", username);
const authResult = await invoke<AuthResult>("auth_login", {
serverUrl,
username,
password,
deviceId,
});
const authResult = await commands.authLogin(serverUrl, username, password, deviceId);
console.log("[Auth] Login successful:", authResult.user);
// Save to storage
await invoke("storage_save_server", {
id: authResult.serverId,
name: serverName,
url: serverUrl,
version: null,
});
await commands.storageSaveServer(authResult.serverId, serverName, serverUrl, null);
await invoke("storage_save_user", {
id: authResult.user.id,
serverId: authResult.serverId,
username: authResult.user.name,
accessToken: authResult.accessToken,
});
await commands.storageSaveUser(
authResult.user.id,
authResult.serverId,
authResult.user.name,
authResult.accessToken
);
await invoke("storage_set_active_user", {
userId: authResult.user.id,
serverId: authResult.serverId,
});
await commands.storageSetActiveUser(authResult.user.id, authResult.serverId);
// Set session in auth manager with server name
await invoke("auth_set_session", {
session: {
userId: authResult.user.id,
username: authResult.user.name,
serverId: authResult.serverId,
serverUrl,
serverName,
accessToken: authResult.accessToken,
verified: true,
needsReauth: false,
},
await commands.authSetSession({
userId: authResult.user.id,
username: authResult.user.name,
serverId: authResult.serverId,
serverUrl,
serverName,
accessToken: authResult.accessToken,
verified: true,
needsReauth: false,
});
// Create RepositoryClient
@@ -352,12 +315,12 @@ function createAuthStore() {
// Configure Rust player
try {
const playerDeviceId = await getDeviceId();
await invoke("player_configure_jellyfin", {
await commands.playerConfigureJellyfin(
serverUrl,
accessToken: authResult.accessToken,
userId: authResult.user.id,
deviceId: playerDeviceId,
});
authResult.accessToken,
authResult.user.id,
playerDeviceId
);
console.log("[Auth] Rust player configured for playback reporting");
} catch (error) {
console.error("[Auth] Failed to configure Rust player:", error);
@@ -380,7 +343,7 @@ function createAuthStore() {
// Start background verification
try {
const verifyDeviceId = await getDeviceId();
await invoke("auth_start_verification", { deviceId: verifyDeviceId });
await commands.authStartVerification(verifyDeviceId);
} catch (error) {
console.error("[Auth] Failed to start verification:", error);
}
@@ -404,25 +367,22 @@ function createAuthStore() {
const deviceId = await getDeviceId();
console.log("[Auth] Re-authenticating...");
const authResult = await invoke<AuthResult>("auth_reauthenticate", {
password,
deviceId,
});
const authResult = await commands.authReauthenticate(password, deviceId);
console.log("[Auth] Re-authentication successful");
// Update storage
await invoke("storage_save_user", {
id: authResult.user.id,
serverId: authResult.serverId,
username: authResult.user.name,
accessToken: authResult.accessToken,
});
await commands.storageSaveUser(
authResult.user.id,
authResult.serverId,
authResult.user.name,
authResult.accessToken
);
// Recreate repository with new credentials
if (repository) {
await repository.destroy();
const session = await invoke<Session | null>("auth_get_session");
const session = await commands.authGetSession();
if (session) {
await repository.create(session.serverUrl, authResult.user.id, authResult.accessToken, authResult.serverId);
}
@@ -431,12 +391,12 @@ function createAuthStore() {
// Reconfigure player
try {
const playerDeviceId = await getDeviceId();
await invoke("player_configure_jellyfin", {
serverUrl: repository ? await getCurrentSessionServerUrl() : "",
accessToken: authResult.accessToken,
userId: authResult.user.id,
deviceId: playerDeviceId,
});
await commands.playerConfigureJellyfin(
repository ? await getCurrentSessionServerUrl() : "",
authResult.accessToken,
authResult.user.id,
playerDeviceId
);
} catch (error) {
console.error("[Auth] Failed to reconfigure player:", error);
}
@@ -467,19 +427,15 @@ function createAuthStore() {
*/
async function logout() {
try {
const session = await invoke<Session | null>("auth_get_session");
const session = await commands.authGetSession();
if (session) {
const deviceId = await getDeviceId();
await invoke("auth_logout", {
serverUrl: session.serverUrl,
accessToken: session.accessToken,
deviceId,
});
await commands.authLogout(session.serverUrl, session.accessToken, deviceId);
}
// Disable Jellyfin reporting in player
try {
await invoke("player_disable_jellyfin");
await commands.playerDisableJellyfin();
} catch (error) {
console.error("[Auth] Failed to disable player reporting:", error);
}
@@ -524,7 +480,7 @@ function createAuthStore() {
*/
async function getCurrentSession() {
try {
return await invoke<Session | null>("auth_get_session");
return await commands.authGetSession();
} catch (error) {
console.error("[Auth] Failed to get current session:", error);
return null;
@@ -551,7 +507,7 @@ function createAuthStore() {
* Helper to get server URL from current session.
*/
async function getCurrentSessionServerUrl(): Promise<string> {
const session = await invoke<Session | null>("auth_get_session");
const session = await commands.authGetSession();
return session?.serverUrl || "";
}
@@ -562,7 +518,7 @@ function createAuthStore() {
try {
const deviceId = await getDeviceId();
console.log("[Auth] Retrying session verification after reconnection");
await invoke("auth_start_verification", { deviceId });
await commands.authStartVerification(deviceId);
} catch (error) {
console.error("[Auth] Failed to retry verification:", error);
}
+10 -17
View File
@@ -6,8 +6,8 @@
import { writable, derived } from "svelte/store";
import { browser } from "$app/environment";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { commands } from "$lib/api/bindings";
export interface ConnectivityState {
/** Browser's navigator.onLine status */
@@ -29,13 +29,6 @@ export interface ConnectivityEvents {
onServerReconnected?: () => void;
}
interface RustConnectivityStatus {
isServerReachable: boolean;
lastChecked: string | null;
connectionError: string | null;
isChecking: boolean;
}
function createConnectivityStore() {
const initialState: ConnectivityState = {
isOnline: browser ? navigator.onLine : true,
@@ -115,10 +108,10 @@ function createConnectivityStore() {
*/
async function checkServerReachable(): Promise<boolean> {
try {
const isReachable = await invoke<boolean>("connectivity_check_server");
const isReachable = await commands.connectivityCheckServer();
// Fetch updated status from Rust
const status = await invoke<RustConnectivityStatus>("connectivity_get_status");
const status = await commands.connectivityGetStatus();
update((s) => ({
...s,
isServerReachable: status.isServerReachable,
@@ -145,13 +138,13 @@ function createConnectivityStore() {
console.log("[ConnectivityStore] Starting monitoring for:", url);
// Set the server URL
await invoke("connectivity_set_server_url", { url });
await commands.connectivitySetServerUrl(url);
// Start the Rust monitoring task (performs immediate check)
await invoke("connectivity_start_monitoring");
await commands.connectivityStartMonitoring();
// Get the initial status immediately after starting
const status = await invoke<RustConnectivityStatus>("connectivity_get_status");
const status = await commands.connectivityGetStatus();
update((s) => ({
...s,
isServerReachable: status.isServerReachable,
@@ -179,7 +172,7 @@ function createConnectivityStore() {
if (!isMonitoring) return;
try {
await invoke("connectivity_stop_monitoring");
await commands.connectivityStopMonitoring();
isMonitoring = false;
eventHandlers = {};
console.log("[ConnectivityStore] Stopped monitoring");
@@ -193,7 +186,7 @@ function createConnectivityStore() {
*/
async function setServerUrl(url: string): Promise<void> {
try {
await invoke("connectivity_set_server_url", { url });
await commands.connectivitySetServerUrl(url);
} catch (error) {
console.error("[ConnectivityStore] Failed to set server URL:", error);
}
@@ -211,7 +204,7 @@ function createConnectivityStore() {
*/
async function markReachable(): Promise<void> {
try {
await invoke("connectivity_mark_reachable");
await commands.connectivityMarkReachable();
// Update local state
update((s) => ({
@@ -230,7 +223,7 @@ function createConnectivityStore() {
*/
async function markUnreachable(error?: string): Promise<void> {
try {
await invoke("connectivity_mark_unreachable", { error: error ?? null });
await commands.connectivityMarkUnreachable(error ?? null);
// Update local state
update((s) => ({
+1 -1
View File
@@ -131,7 +131,7 @@ describe("downloads store", () => {
expect(mockInvoke).toHaveBeenCalledWith("get_downloads", {
userId: "user-1",
statusFilter: undefined,
statusFilter: null,
});
const state = get(downloads);
+27 -35
View File
@@ -1,7 +1,6 @@
// Download manager state store
// TRACES: UR-011, UR-013, UR-018 | DR-015, DR-017
import { writable, derived, get } from 'svelte/store';
import { invoke } from '@tauri-apps/api/core';
import { commands } from '$lib/api/bindings';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
@@ -95,13 +94,10 @@ function createDownloadsStore() {
try {
console.log('🔄 Refreshing downloads for user:', userId);
const response = await invoke<{ downloads: DownloadInfo[]; stats: DownloadStats }>(
'get_downloads',
{
userId,
statusFilter
}
);
const response = (await commands.getDownloads(
userId,
statusFilter ?? null
)) as unknown as { downloads: DownloadInfo[]; stats: DownloadStats };
console.log(' Got', response.downloads.length, 'downloads from backend');
console.log(' Stats:', response.stats);
@@ -182,11 +178,7 @@ function createDownloadsStore() {
async downloadAlbum(albumId: string, userId: string, basePath: string): Promise<number[]> {
try {
console.log('📥 downloadAlbum called:', { albumId, userId, basePath });
const downloadIds = await invoke<number[]>('download_album', {
albumId,
userId,
basePath
});
const downloadIds = await commands.downloadAlbum(albumId, userId, basePath);
console.log(' Got download IDs from backend:', downloadIds);
// Refresh downloads
@@ -267,13 +259,13 @@ function createDownloadsStore() {
basePath,
qualityPreset
});
const downloadIds = await invoke<number[]>('download_series', {
const downloadIds = await commands.downloadSeries(
seriesId,
seriesName,
userId,
basePath,
qualityPreset
});
qualityPreset ?? null
);
console.log(' Queued', downloadIds.length, 'episodes for download');
// Refresh downloads
@@ -306,15 +298,15 @@ function createDownloadsStore() {
seasonNumber,
qualityPreset
});
const downloadIds = await invoke<number[]>('download_season', {
const downloadIds = await commands.downloadSeason(
seasonId,
seriesName,
seasonName,
seasonNumber,
userId,
basePath,
qualityPreset
});
qualityPreset ?? null
);
console.log(' Queued', downloadIds.length, 'episodes for download');
// Refresh downloads
@@ -332,7 +324,7 @@ function createDownloadsStore() {
*/
async pinItem(itemId: string): Promise<void> {
try {
await invoke('pin_item', { itemId });
await commands.pinItem(itemId);
} catch (error) {
console.error('Failed to pin item:', error);
throw error;
@@ -344,7 +336,7 @@ function createDownloadsStore() {
*/
async unpinItem(itemId: string): Promise<void> {
try {
await invoke('unpin_item', { itemId });
await commands.unpinItem(itemId);
} catch (error) {
console.error('Failed to unpin item:', error);
throw error;
@@ -356,7 +348,7 @@ function createDownloadsStore() {
*/
async isItemPinned(itemId: string): Promise<boolean> {
try {
return await invoke<boolean>('is_item_pinned', { itemId });
return await commands.isItemPinned(itemId);
} catch (error) {
console.error('Failed to check pin status:', error);
return false;
@@ -368,7 +360,7 @@ function createDownloadsStore() {
*/
async pause(downloadId: number): Promise<void> {
try {
await invoke('pause_download', { downloadId });
await commands.pauseDownload(downloadId);
} catch (error) {
console.error('Failed to pause download:', error);
throw error;
@@ -380,7 +372,7 @@ function createDownloadsStore() {
*/
async resume(downloadId: number): Promise<void> {
try {
await invoke('resume_download', { downloadId });
await commands.resumeDownload(downloadId);
} catch (error) {
console.error('Failed to resume download:', error);
throw error;
@@ -392,7 +384,7 @@ function createDownloadsStore() {
*/
async cancel(downloadId: number): Promise<void> {
try {
await invoke('cancel_download', { downloadId });
await commands.cancelDownload(downloadId);
} catch (error) {
console.error('Failed to cancel download:', error);
throw error;
@@ -404,7 +396,7 @@ function createDownloadsStore() {
*/
async delete(downloadId: number): Promise<void> {
try {
await invoke('delete_download', { downloadId });
await commands.deleteDownload(downloadId);
update((state) => {
const { [downloadId]: removed, ...remaining } = state.downloads;
return { ...state, downloads: remaining };
@@ -574,11 +566,11 @@ function handleDownloadEvent(payload: DownloadEvent): void {
case 'completed':
if (download) {
// Persist to database
invoke('mark_download_completed', {
downloadId: payload.downloadId,
bytesDownloaded: payload.totalBytes || download.fileSize || download.bytesDownloaded,
filePath: payload.filePath || download.filePath
}).catch((err) => console.error('Failed to persist download completion:', err));
commands.markDownloadCompleted(
payload.downloadId,
payload.totalBytes || download.fileSize || download.bytesDownloaded,
payload.filePath || download.filePath
).catch((err) => console.error('Failed to persist download completion:', err));
updateDownloadInStore(payload.downloadId, {
status: 'completed',
@@ -592,10 +584,10 @@ function handleDownloadEvent(payload: DownloadEvent): void {
case 'failed':
if (download) {
// Persist to database
invoke('mark_download_failed', {
downloadId: payload.downloadId,
errorMessage: payload.error || 'Unknown error'
}).catch((err) => console.error('Failed to persist download failure:', err));
commands.markDownloadFailed(
payload.downloadId,
payload.error || 'Unknown error'
).catch((err) => console.error('Failed to persist download failure:', err));
updateDownloadInStore(payload.downloadId, {
status: 'failed',
-236
View File
@@ -1,236 +0,0 @@
/**
* Integration tests for playbackMode store
*
* Tests that the store calls Tauri commands with correct parameter names.
*
* IMPORTANT: Tauri v2's #[tauri::command] macro automatically converts
* snake_case Rust parameter names to camelCase for the frontend.
* So Rust `repository_handle: String` → frontend sends `repositoryHandle`.
* Nested struct fields with #[serde(rename_all = "camelCase")] also use camelCase.
*/
import { vi, describe, it, expect, beforeEach } from "vitest";
describe("playbackMode store - Tauri invoke parameter verification", () => {
let mockInvokedCalls: Array<{ command: string; args: Record<string, any> }> =
[];
beforeEach(() => {
mockInvokedCalls = [];
// Mock invoke to capture calls
vi.mock("@tauri-apps/api/core", () => ({
invoke: vi.fn(async (command: string, args?: Record<string, any>) => {
mockInvokedCalls.push({ command, args: args || {} });
return { success: true };
}),
}));
});
describe("player_play_tracks command parameters", () => {
it("should use repositoryHandle (camelCase, auto-converted by Tauri v2)", () => {
const correctCall = {
repositoryHandle: "test-handle-123", // ✓ CORRECT - Tauri v2 auto-converts
request: {
trackIds: ["track-1"],
startIndex: 0,
shuffle: false,
context: {
type: "search",
searchQuery: "",
},
},
};
expect(Object.keys(correctCall)).toContain("repositoryHandle");
expect(Object.keys(correctCall)).not.toContain("repository_handle");
});
it("nested request fields use camelCase", () => {
const correctRequest = {
trackIds: ["track-1"], // ✓ camelCase for nested struct field
startIndex: 0, // ✓ camelCase
shuffle: false,
context: {
type: "search",
searchQuery: "", // ✓ camelCase for context field
},
};
expect(Object.keys(correctRequest)).toContain("trackIds");
expect(Object.keys(correctRequest)).toContain("startIndex");
expect(Object.keys(correctRequest.context)).toContain("searchQuery");
});
});
describe("playback_mode_transfer_to_local command parameters", () => {
it("should use currentItemId and positionTicks (camelCase)", () => {
const correctCall = {
currentItemId: "item-123", // ✓ CORRECT
positionTicks: 50000, // ✓ CORRECT
};
expect(Object.keys(correctCall)).toContain("currentItemId");
expect(Object.keys(correctCall)).toContain("positionTicks");
expect(Object.keys(correctCall)).not.toContain("current_item_id");
expect(Object.keys(correctCall)).not.toContain("position_ticks");
});
});
describe("Session commands use sessionId (camelCase)", () => {
it("remote_send_command uses sessionId", () => {
const correctCall = {
sessionId: "session-123", // ✓ CORRECT
command: "PlayPause",
};
expect(Object.keys(correctCall)).toContain("sessionId");
expect(Object.keys(correctCall)).not.toContain("session_id");
});
it("remote_play_on_session uses sessionId, itemIds, startIndex", () => {
const correctCall = {
sessionId: "session-123", // ✓ CORRECT
itemIds: ["id1", "id2"], // ✓ CORRECT
startIndex: 0, // ✓ CORRECT
};
expect(Object.keys(correctCall)).toContain("sessionId");
expect(Object.keys(correctCall)).toContain("itemIds");
expect(Object.keys(correctCall)).toContain("startIndex");
expect(Object.keys(correctCall)).not.toContain("session_id");
expect(Object.keys(correctCall)).not.toContain("item_ids");
expect(Object.keys(correctCall)).not.toContain("start_index");
});
it("remote_session_seek uses sessionId and positionTicks", () => {
const correctCall = {
sessionId: "session-123", // ✓ CORRECT
positionTicks: 50000, // ✓ CORRECT
};
expect(Object.keys(correctCall)).toContain("sessionId");
expect(Object.keys(correctCall)).toContain("positionTicks");
expect(Object.keys(correctCall)).not.toContain("session_id");
expect(Object.keys(correctCall)).not.toContain("position_ticks");
});
});
describe("Download commands use itemId (camelCase)", () => {
it("pin_item uses itemId", () => {
const correctCall = {
itemId: "item-123", // ✓ CORRECT
};
expect(Object.keys(correctCall)).toContain("itemId");
expect(Object.keys(correctCall)).not.toContain("item_id");
});
it("unpin_item uses itemId", () => {
const correctCall = {
itemId: "item-123", // ✓ CORRECT
};
expect(Object.keys(correctCall)).toContain("itemId");
expect(Object.keys(correctCall)).not.toContain("item_id");
});
});
describe("Queue commands use repositoryHandle (camelCase)", () => {
it("player_add_track_by_id uses repositoryHandle", () => {
const correctCall = {
repositoryHandle: "handle-123", // ✓ CORRECT
request: {
trackId: "track-123",
position: 0,
},
};
expect(Object.keys(correctCall)).toContain("repositoryHandle");
expect(Object.keys(correctCall)).not.toContain("repository_handle");
});
it("player_add_tracks_by_ids uses repositoryHandle", () => {
const correctCall = {
repositoryHandle: "handle-123", // ✓ CORRECT
request: {
trackIds: ["track-1", "track-2"],
position: 0,
},
};
expect(Object.keys(correctCall)).toContain("repositoryHandle");
expect(Object.keys(correctCall)).not.toContain("repository_handle");
});
});
describe("Player commands", () => {
it("player_play_album_track uses repositoryHandle", () => {
const correctCall = {
repositoryHandle: "handle-123", // ✓ CORRECT
request: {
albumId: "album-123",
albumName: "Test Album",
trackId: "track-123",
shuffle: false,
},
};
expect(Object.keys(correctCall)).toContain("repositoryHandle");
expect(Object.keys(correctCall)).not.toContain("repository_handle");
// Nested struct fields use camelCase
expect(Object.keys(correctCall.request)).toContain("albumId");
expect(Object.keys(correctCall.request)).toContain("albumName");
expect(Object.keys(correctCall.request)).toContain("trackId");
});
it("player_seek uses position (simple types don't need renaming)", () => {
const correctCall = {
position: 500.5,
};
expect(correctCall.position).toBe(500.5);
});
});
describe("Error detection - what NOT to do", () => {
it("repository_handle (snake_case) is WRONG for top-level param", () => {
const wrongCall = {
repository_handle: "handle-123", // ❌ WRONG
};
expect(Object.keys(wrongCall)).not.toContain("repositoryHandle");
expect(Object.keys(wrongCall)).toContain("repository_handle");
});
it("session_id (snake_case) is WRONG for top-level param", () => {
const wrongCall = {
session_id: "session-123", // ❌ WRONG
};
expect(Object.keys(wrongCall)).not.toContain("sessionId");
expect(Object.keys(wrongCall)).toContain("session_id");
});
it("item_ids (snake_case) is WRONG for top-level param", () => {
const wrongCall = {
item_ids: ["id1", "id2"], // ❌ WRONG
};
expect(Object.keys(wrongCall)).not.toContain("itemIds");
expect(Object.keys(wrongCall)).toContain("item_ids");
});
it("start_index (snake_case) is WRONG for top-level param", () => {
const wrongCall = {
start_index: 0, // ❌ WRONG
};
expect(Object.keys(wrongCall)).not.toContain("startIndex");
expect(Object.keys(wrongCall)).toContain("start_index");
});
});
});
+13 -19
View File
@@ -10,7 +10,7 @@
*/
import { writable, get, derived } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import { commands } from "$lib/api/bindings";
import { sessions, selectedSession } from "./sessions";
import { auth } from "./auth";
import { ticksToSeconds } from "$lib/utils/playbackUnits";
@@ -47,7 +47,7 @@ function createPlaybackModeStore() {
*/
async function refreshMode(): Promise<void> {
try {
const rustMode = await invoke<RustPlaybackMode>("playback_mode_get_current");
const rustMode = (await commands.playbackModeGetCurrent()) as RustPlaybackMode;
update((s) => ({
...s,
@@ -91,7 +91,7 @@ function createPlaybackModeStore() {
// Rust handles everything - just wait for it to complete
// It includes its own 5-second timeout for track loading
console.log("[PlaybackMode] About to invoke playback_mode_transfer_to_remote with sessionId:", sessionId);
await invoke("playback_mode_transfer_to_remote", { sessionId });
await commands.playbackModeTransferToRemote(sessionId ?? "");
console.log("[PlaybackMode] Invoke completed successfully");
if (aborted) {
@@ -192,16 +192,13 @@ function createPlaybackModeStore() {
// Use player_play_tracks - backend fetches all metadata from single ID
const repositoryHandle = repository.getHandle();
await invoke("player_play_tracks", {
repositoryHandle,
request: {
trackIds: [itemId],
startIndex: 0,
shuffle: false,
context: {
type: "search",
searchQuery: "",
},
await commands.playerPlayTracks(repositoryHandle, {
trackIds: [itemId],
startIndex: 0,
shuffle: false,
context: {
type: "search",
searchQuery: "",
},
});
@@ -212,16 +209,13 @@ function createPlaybackModeStore() {
// Seek to position if not at the very start
if (positionSeconds > 0.5) {
await invoke("player_seek", { position: positionSeconds });
await commands.playerSeek(positionSeconds);
}
if (aborted) return;
// Let Rust handle stopping remote playback
await invoke("playback_mode_transfer_to_local", {
currentItemId: itemId,
positionTicks,
});
await commands.playbackModeTransferToLocal(itemId, positionTicks);
if (aborted) return;
@@ -328,7 +322,7 @@ function createPlaybackModeStore() {
try {
// Notify Rust backend to switch to idle mode
await invoke("playback_mode_set", { mode: { type: "idle" } });
await commands.playbackModeSet({ type: "idle" });
// Update local state
sessions.selectSession(null);
+15 -21
View File
@@ -7,8 +7,8 @@
// TRACES: UR-005, UR-015 | DR-005, DR-020
import { writable, derived, get } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { commands } from "$lib/api/bindings";
import type { MediaItem } from "$lib/api/types";
import { auth } from "$lib/stores/auth";
@@ -72,7 +72,7 @@ function createQueueStore() {
*/
async function syncFromRust(): Promise<void> {
try {
const rustQueue = await invoke<QueueChangedEvent>("player_get_queue");
const rustQueue = (await commands.playerGetQueue()) as unknown as QueueChangedEvent;
console.log("[Queue] Synced from Rust - items:", rustQueue.items.length);
set({
items: rustQueue.items,
@@ -105,37 +105,37 @@ function createQueueStore() {
// TRACES: UR-005, UR-015 | DR-005
async function next() {
await invoke("player_next");
await commands.playerNext();
}
// TRACES: UR-005, UR-015 | DR-005
async function previous() {
await invoke("player_previous");
await commands.playerPrevious();
}
// TRACES: UR-005, UR-015 | DR-005, DR-020
async function skipTo(index: number) {
await invoke("player_skip_to", { index });
await commands.playerSkipTo(index);
}
// TRACES: UR-005, UR-015 | DR-005
async function toggleShuffle() {
await invoke("player_toggle_shuffle");
await commands.playerToggleShuffle();
}
// TRACES: UR-005, UR-015 | DR-005
async function cycleRepeat() {
await invoke("player_cycle_repeat");
await commands.playerCycleRepeat();
}
// TRACES: UR-015 | DR-020
async function removeFromQueue(index: number) {
await invoke("player_remove_from_queue", { index });
await commands.playerRemoveFromQueue(index);
}
// TRACES: UR-015 | DR-020
async function moveInQueue(fromIndex: number, toIndex: number) {
await invoke("player_move_in_queue", { fromIndex, toIndex });
await commands.playerMoveInQueue(fromIndex, toIndex);
}
// TRACES: UR-015 | DR-020
@@ -152,20 +152,14 @@ function createQueueStore() {
// Use new Rust commands that accept IDs only
if (trackIds.length === 1) {
await invoke("player_add_track_by_id", {
repositoryHandle,
request: {
trackId: trackIds[0],
position,
},
await commands.playerAddTrackById(repositoryHandle, {
trackId: trackIds[0],
position,
});
} else {
await invoke("player_add_tracks_by_ids", {
repositoryHandle,
request: {
trackIds,
position,
},
await commands.playerAddTracksByIds(repositoryHandle, {
trackIds,
position,
});
}
}
+11 -36
View File
@@ -2,8 +2,8 @@
// TRACES: UR-010 | DR-037
import { writable, derived } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { commands } from "$lib/api/bindings";
import type { Session } from "$lib/api/types";
interface SessionsState {
@@ -53,7 +53,7 @@ function createSessionsStore() {
try {
update((s) => ({ ...s, isLoading: true, error: null }));
const sessions = await invoke<Session[]>("sessions_poll_now");
const sessions = await commands.sessionsPollNow();
console.log(`[Sessions] Manual refresh returned ${sessions.length} sessions`);
sessions.forEach((s, i) => {
@@ -91,10 +91,7 @@ function createSessionsStore() {
*/
async function sendPlayPause(sessionId: string | null | undefined): Promise<void> {
try {
await invoke("remote_send_command", {
sessionId,
command: "PlayPause",
});
await commands.remoteSendCommand(sessionId ?? "", "PlayPause");
// Refresh after command to get updated state
await refresh();
} catch (error) {
@@ -108,10 +105,7 @@ function createSessionsStore() {
*/
async function sendStop(sessionId: string | null | undefined): Promise<void> {
try {
await invoke("remote_send_command", {
sessionId,
command: "Stop",
});
await commands.remoteSendCommand(sessionId ?? "", "Stop");
await refresh();
} catch (error) {
console.error("Failed to send stop command:", error);
@@ -124,10 +118,7 @@ function createSessionsStore() {
*/
async function sendNext(sessionId: string | null | undefined): Promise<void> {
try {
await invoke("remote_send_command", {
sessionId,
command: "NextTrack",
});
await commands.remoteSendCommand(sessionId ?? "", "NextTrack");
await refresh();
} catch (error) {
console.error("Failed to send next track command:", error);
@@ -140,10 +131,7 @@ function createSessionsStore() {
*/
async function sendPrevious(sessionId: string | null | undefined): Promise<void> {
try {
await invoke("remote_send_command", {
sessionId,
command: "PreviousTrack",
});
await commands.remoteSendCommand(sessionId ?? "", "PreviousTrack");
await refresh();
} catch (error) {
console.error("Failed to send previous track command:", error);
@@ -156,10 +144,7 @@ function createSessionsStore() {
*/
async function sendSeek(sessionId: string | null | undefined, positionTicks: number): Promise<void> {
try {
await invoke("remote_session_seek", {
sessionId,
positionTicks,
});
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);
@@ -172,10 +157,7 @@ function createSessionsStore() {
*/
async function sendVolume(sessionId: string | null | undefined, volume: number): Promise<void> {
try {
await invoke("remote_session_set_volume", {
sessionId,
volume,
});
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);
@@ -188,10 +170,7 @@ function createSessionsStore() {
*/
async function sendToggleMute(sessionId: string | null | undefined): Promise<void> {
try {
await invoke("remote_send_command", {
sessionId,
command: "ToggleMute",
});
await commands.remoteSendCommand(sessionId ?? "", "ToggleMute");
await refresh();
} catch (error) {
console.error("Failed to toggle mute:", error);
@@ -213,14 +192,10 @@ function createSessionsStore() {
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')");
console.log("[SESSIONS] About to call commands.remotePlayOnSession");
try {
// Use Rust player's Jellyfin client for remote playback
const result = await invoke("remote_play_on_session", {
sessionId,
itemIds,
startIndex,
});
const result = await commands.remotePlayOnSession(sessionId ?? "", itemIds, startIndex);
console.log("[SESSIONS] invoke succeeded, result:", result);
await refresh();
} catch (error) {
+5 -11
View File
@@ -11,7 +11,7 @@
*/
import { writable, derived } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import { commands } from "$lib/api/bindings";
export type SleepTimerMode =
| { kind: "off" }
@@ -39,25 +39,19 @@ function createSleepTimerStore() {
// Methods that invoke the backend API
async setTimeTimer(minutes: number): Promise<void> {
const endTime = Date.now() + minutes * 60 * 1000;
await invoke("player_set_sleep_timer", {
mode: { kind: "time", endTime },
});
await commands.playerSetSleepTimer({ kind: "time", endTime });
},
async setEndOfTrackTimer(): Promise<void> {
await invoke("player_set_sleep_timer", {
mode: { kind: "endOfTrack" },
});
await commands.playerSetSleepTimer({ kind: "endOfTrack" });
},
async setEpisodesTimer(count: number): Promise<void> {
await invoke("player_set_sleep_timer", {
mode: { kind: "episodes", remaining: count },
});
await commands.playerSetSleepTimer({ kind: "episodes", remaining: count });
},
async cancel(): Promise<void> {
await invoke("player_cancel_sleep_timer");
await commands.playerCancelSleepTimer();
},
};
}