From d01c2aab9fced74961fe3dca6f41e6defc0e3fc4 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 21 Jun 2026 08:47:04 +0200 Subject: [PATCH] Migrate all IPC call sites to typed tauri-specta commands.* 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. --- src-tauri/src/lib.rs | 3 + src/lib/api/autoplay.test.ts | 32 +- src/lib/api/autoplay.ts | 20 +- src/lib/api/bindings.ts | 12 + src/lib/api/repository-client.ts | 196 ++------- src/lib/components/common/CachedImage.svelte | 17 +- .../downloads/StorageManagement.svelte | 8 +- .../library/AlbumDownloadButton.svelte | 10 +- .../components/library/DownloadButton.svelte | 3 +- .../library/PlaylistDetailView.svelte | 40 +- .../library/SeasonDownloadButton.svelte | 4 +- .../library/SeriesDownloadButton.svelte | 4 +- src/lib/components/library/TrackList.svelte | 33 +- .../library/VideoDownloadButton.svelte | 12 +- src/lib/components/player/AudioPlayer.svelte | 16 +- src/lib/components/player/MiniPlayer.svelte | 14 +- src/lib/components/player/Queue.svelte | 9 +- src/lib/components/player/VideoPlayer.svelte | 74 ++-- .../components/player/VolumeControl.svelte | 6 +- src/lib/components/sessions/CastButton.svelte | 8 +- src/lib/services/deviceId.ts | 4 +- src/lib/services/favorites.ts | 10 +- src/lib/services/imageCache.ts | 23 +- src/lib/services/playbackReporting.ts | 24 +- src/lib/services/playerEvents.ts | 13 +- src/lib/services/preload.test.ts | 24 +- src/lib/services/preload.ts | 37 +- src/lib/services/syncService.ts | 36 +- src/lib/stores/auth.ts | 156 +++---- src/lib/stores/connectivity.ts | 27 +- src/lib/stores/downloads.test.ts | 2 +- src/lib/stores/downloads.ts | 62 ++- src/lib/stores/playbackMode.invoke.test.ts | 236 ----------- src/lib/stores/playbackMode.ts | 32 +- src/lib/stores/queue.ts | 36 +- src/lib/stores/sessions.ts | 47 +-- src/lib/stores/sleepTimer.ts | 16 +- src/lib/utils/tauriCommandParams.test.ts | 255 ------------ src/lib/utils/tauriIntegration.test.ts | 387 ------------------ src/lib/utils/tauriInvokeDebug.test.ts | 348 ---------------- src/lib/utils/tauriRealCalls.test.ts | 107 ----- src/routes/+layout.svelte | 1 - src/routes/downloads/+page.svelte | 6 +- src/routes/library/+layout.svelte | 11 +- src/routes/library/[id]/+page.svelte | 28 +- src/routes/player/[id]/+page.svelte | 100 ++--- src/routes/settings/+page.svelte | 26 +- 47 files changed, 456 insertions(+), 2119 deletions(-) delete mode 100644 src/lib/stores/playbackMode.invoke.test.ts delete mode 100644 src/lib/utils/tauriCommandParams.test.ts delete mode 100644 src/lib/utils/tauriIntegration.test.ts delete mode 100644 src/lib/utils/tauriInvokeDebug.test.ts delete mode 100644 src/lib/utils/tauriRealCalls.test.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9ff9950..b0015c1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -101,6 +101,7 @@ use commands::{ repository_report_playback_start, repository_report_playback_progress, repository_report_playback_stopped, repository_get_image_url, repository_mark_favorite, repository_unmark_favorite, repository_get_person, repository_get_items_by_person, repository_get_similar_items, + repository_get_subtitle_url, repository_get_video_download_url, // Playlist commands playlist_create, playlist_delete, playlist_rename, playlist_get_items, playlist_add_items, playlist_remove_items, playlist_move_item, @@ -565,6 +566,8 @@ fn specta_builder() -> Builder { repository_get_person, repository_get_items_by_person, repository_get_similar_items, + repository_get_subtitle_url, + repository_get_video_download_url, // Playlist commands playlist_create, playlist_delete, diff --git a/src/lib/api/autoplay.test.ts b/src/lib/api/autoplay.test.ts index f7cd419..3956b0e 100644 --- a/src/lib/api/autoplay.test.ts +++ b/src/lib/api/autoplay.test.ts @@ -10,6 +10,7 @@ import { playNextEpisode, type AutoplaySettings, } from "./autoplay"; +import type { PlayItemRequest } from "./bindings"; vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn(async (command: string, args?: any) => { @@ -33,6 +34,23 @@ vi.mock("@tauri-apps/api/core", () => ({ }), })); +// setAutoplaySettings sources the user id from the auth store +vi.mock("$lib/stores/auth", () => ({ + auth: { getUserId: () => "user-1" }, +})); + +// Minimal valid PlayItemRequest for playNextEpisode tests +function makeItem(overrides: Partial = {}): PlayItemRequest { + return { + id: "item-123", + title: "Episode 1", + streamUrl: "http://example/stream", + videoCodec: "h264", + needsTranscoding: false, + ...overrides, + }; +} + describe("autoplay API", () => { beforeEach(() => { vi.clearAllMocks(); @@ -98,7 +116,7 @@ describe("autoplay API", () => { (c) => c[0] === "player_set_autoplay_settings" ); expect(call).toBeDefined(); - expect(call![1]).toEqual({ settings }); + expect(call![1]).toEqual({ userId: "user-1", settings }); }); it("should support different countdown values", async () => { @@ -130,11 +148,7 @@ describe("autoplay API", () => { describe("playNextEpisode", () => { it("should play next episode with item", async () => { - const mockItem = { - id: "item-123", - name: "Episode 1", - seriesId: "series-456", - }; + const mockItem = makeItem(); await playNextEpisode(mockItem); @@ -150,9 +164,9 @@ describe("autoplay API", () => { it("should handle different item types", async () => { const items = [ - { id: "1", name: "Episode 1" }, - { id: "2", name: "Episode 2", seasonNumber: 1 }, - { id: "3", name: "Episode 3", episodeNumber: 5 }, + makeItem({ id: "1", title: "Episode 1" }), + makeItem({ id: "2", title: "Episode 2" }), + makeItem({ id: "3", title: "Episode 3" }), ]; for (const item of items) { diff --git a/src/lib/api/autoplay.ts b/src/lib/api/autoplay.ts index e1e9963..f84ee43 100644 --- a/src/lib/api/autoplay.ts +++ b/src/lib/api/autoplay.ts @@ -4,28 +4,26 @@ * Functions to control autoplay in the backend. */ -import { invoke } from "@tauri-apps/api/core"; +import { commands } from "$lib/api/bindings"; +import type { PlayItemRequest, AutoplaySettings } from "$lib/api/bindings"; +import { auth } from "$lib/stores/auth"; -export interface AutoplaySettings { - enabled: boolean; - countdownSeconds: number; - maxEpisodes: number; -} +export type { AutoplaySettings }; export async function getAutoplaySettings(): Promise { - return invoke("player_get_autoplay_settings"); + return commands.playerGetAutoplaySettings(); } export async function setAutoplaySettings( settings: AutoplaySettings ): Promise { - return invoke("player_set_autoplay_settings", { settings }); + return commands.playerSetAutoplaySettings(auth.getUserId() ?? "", settings); } export async function cancelAutoplayCountdown(): Promise { - return invoke("player_cancel_autoplay_countdown"); + await commands.playerCancelAutoplayCountdown(); } -export async function playNextEpisode(item: any): Promise { - return invoke("player_play_next_episode", { item }); +export async function playNextEpisode(item: PlayItemRequest): Promise { + await commands.playerPlayNextEpisode(item); } diff --git a/src/lib/api/bindings.ts b/src/lib/api/bindings.ts index 4d9d39f..c90060e 100644 --- a/src/lib/api/bindings.ts +++ b/src/lib/api/bindings.ts @@ -1121,6 +1121,18 @@ async repositoryGetItemsByPerson(handle: string, personId: string, options: GetI async repositoryGetSimilarItems(handle: string, itemId: string, limit: number | null) : Promise { return await TAURI_INVOKE("repository_get_similar_items", { handle, itemId, limit }); }, +/** + * Get subtitle URL for a media item + */ +async repositoryGetSubtitleUrl(handle: string, itemId: string, mediaSourceId: string, streamIndex: number, format: string) : Promise { + return await TAURI_INVOKE("repository_get_subtitle_url", { handle, itemId, mediaSourceId, streamIndex, format }); +}, +/** + * Get video download URL with quality preset + */ +async repositoryGetVideoDownloadUrl(handle: string, itemId: string, quality: string, mediaSourceId: string | null) : Promise { + return await TAURI_INVOKE("repository_get_video_download_url", { handle, itemId, quality, mediaSourceId }); +}, /** * Create a new playlist */ diff --git a/src/lib/api/repository-client.ts b/src/lib/api/repository-client.ts index 75dc568..411f81a 100644 --- a/src/lib/api/repository-client.ts +++ b/src/lib/api/repository-client.ts @@ -2,9 +2,8 @@ // All API calls go through Tauri commands in src-tauri/src/commands/repository.rs // NO direct HTTP calls - everything routes through Rust backend -import { invoke } from "@tauri-apps/api/core"; +import { commands } from "./bindings"; import type { QualityPreset } from "./quality-presets"; -import { QUALITY_PRESETS } from "./quality-presets"; import type { Library, MediaItem, @@ -39,12 +38,7 @@ export class RepositoryClient { serverId: string ): Promise { console.log("[RepositoryClient] Creating Rust repository..."); - this.handle = await invoke("repository_create", { - serverUrl, - userId, - accessToken, - serverId, - }); + this.handle = await commands.repositoryCreate(serverUrl, userId, accessToken, serverId); // Store for URL construction this._serverUrl = serverUrl; @@ -60,7 +54,7 @@ export class RepositoryClient { */ async destroy(): Promise { if (this.handle) { - await invoke("repository_destroy", { handle: this.handle }); + await commands.repositoryDestroy(this.handle); this.handle = null; this._serverUrl = null; this._accessToken = null; @@ -84,119 +78,67 @@ export class RepositoryClient { // ===== Library Methods (all via Rust) ===== async getLibraries(): Promise { - return invoke("repository_get_libraries", { - handle: this.ensureHandle(), - }); + return commands.repositoryGetLibraries(this.ensureHandle()); } async getItems(parentId: string, options?: GetItemsOptions): Promise { - return invoke("repository_get_items", { - handle: this.ensureHandle(), - parentId, - options: options ?? null, - }); + return commands.repositoryGetItems(this.ensureHandle(), parentId, options ?? null); } async getItem(itemId: string): Promise { - return invoke("repository_get_item", { - handle: this.ensureHandle(), - itemId, - }); + return commands.repositoryGetItem(this.ensureHandle(), itemId); } async getLatestItems(parentId: string, limit?: number): Promise { - return invoke("repository_get_latest_items", { - handle: this.ensureHandle(), - parentId, - limit: limit ?? null, - }); + return commands.repositoryGetLatestItems(this.ensureHandle(), parentId, limit ?? null); } async getResumeItems(parentId?: string, limit?: number): Promise { - return invoke("repository_get_resume_items", { - handle: this.ensureHandle(), - parentId: parentId ?? null, - limit: limit ?? null, - }); + return commands.repositoryGetResumeItems(this.ensureHandle(), parentId ?? null, limit ?? null); } async getNextUpEpisodes(seriesId?: string, limit?: number): Promise { - return invoke("repository_get_next_up_episodes", { - handle: this.ensureHandle(), - seriesId: seriesId ?? null, - limit: limit ?? null, - }); + return commands.repositoryGetNextUpEpisodes(this.ensureHandle(), seriesId ?? null, limit ?? null); } async getRecentlyPlayedAudio(limit?: number): Promise { - return invoke("repository_get_recently_played_audio", { - handle: this.ensureHandle(), - limit: limit ?? null, - }); + return commands.repositoryGetRecentlyPlayedAudio(this.ensureHandle(), limit ?? null); } async getResumeMovies(limit?: number): Promise { - return invoke("repository_get_resume_movies", { - handle: this.ensureHandle(), - limit: limit ?? null, - }); + return commands.repositoryGetResumeMovies(this.ensureHandle(), limit ?? null); } async getGenres(parentId?: string): Promise { - return invoke("repository_get_genres", { - handle: this.ensureHandle(), - parentId: parentId ?? null, - }); + return commands.repositoryGetGenres(this.ensureHandle(), parentId ?? null); } async search(query: string, options?: SearchOptions): Promise { - return invoke("repository_search", { - handle: this.ensureHandle(), - query, - options: options ?? null, - }); + return commands.repositorySearch(this.ensureHandle(), query, options ?? null); } // ===== Playback Methods (all via Rust) ===== async getPlaybackInfo(itemId: string): Promise { - return invoke("repository_get_playback_info", { - handle: this.ensureHandle(), - itemId, - }); + return commands.repositoryGetPlaybackInfo(this.ensureHandle(), itemId); } async reportPlaybackStart(itemId: string, positionTicks: number): Promise { - return invoke("repository_report_playback_start", { - handle: this.ensureHandle(), - itemId, - positionTicks, - }); + await commands.repositoryReportPlaybackStart(this.ensureHandle(), itemId, positionTicks); } async reportPlaybackProgress(itemId: string, positionTicks: number): Promise { - return invoke("repository_report_playback_progress", { - handle: this.ensureHandle(), - itemId, - positionTicks, - }); + await commands.repositoryReportPlaybackProgress(this.ensureHandle(), itemId, positionTicks); } async reportPlaybackStopped(itemId: string, positionTicks: number): Promise { - return invoke("repository_report_playback_stopped", { - handle: this.ensureHandle(), - itemId, - positionTicks, - }); + await commands.repositoryReportPlaybackStopped(this.ensureHandle(), itemId, positionTicks); } // ===== Stream URL Methods (via Rust) ===== async getAudioStreamUrl(itemId: string): Promise { - return invoke("repository_get_audio_stream_url", { - handle: this.ensureHandle(), - itemId, - }); + return commands.repositoryGetAudioStreamUrl(this.ensureHandle(), itemId); } async getVideoStreamUrl( @@ -205,13 +147,13 @@ export class RepositoryClient { startTimeSeconds?: number, audioStreamIndex?: number ): Promise { - return invoke("repository_get_video_stream_url", { - handle: this.ensureHandle(), + return commands.repositoryGetVideoStreamUrl( + this.ensureHandle(), itemId, - mediaSourceId: mediaSourceId ?? null, - startTimeSeconds: startTimeSeconds ?? null, - audioStreamIndex: audioStreamIndex ?? null, - }); + mediaSourceId ?? null, + startTimeSeconds ?? null, + audioStreamIndex ?? null + ); } // ===== URL Construction Methods (sync, no server call) ===== @@ -221,12 +163,7 @@ export class RepositoryClient { * The Rust backend constructs and returns the URL with proper credentials handling */ async getImageUrl(itemId: string, imageType: ImageType = "Primary", options?: ImageOptions): Promise { - return invoke("repository_get_image_url", { - handle: this.ensureHandle(), - itemId, - imageType, - options: options ?? null, - }); + return commands.repositoryGetImageUrl(this.ensureHandle(), itemId, imageType, options ?? null); } /** @@ -239,13 +176,7 @@ export class RepositoryClient { streamIndex: number, format: string = "vtt" ): Promise { - return invoke("repository_get_subtitle_url", { - handle: this.ensureHandle(), - itemId, - mediaSourceId, - streamIndex, - format, - }); + return commands.repositoryGetSubtitleUrl(this.ensureHandle(), itemId, mediaSourceId, streamIndex, format); } /** @@ -258,110 +189,61 @@ export class RepositoryClient { quality: QualityPreset = "original", mediaSourceId?: string ): Promise { - return invoke("repository_get_video_download_url", { - handle: this.ensureHandle(), - itemId, - quality, - mediaSourceId: mediaSourceId ?? null, - }); + return commands.repositoryGetVideoDownloadUrl(this.ensureHandle(), itemId, quality, mediaSourceId ?? null); } // ===== Favorite Methods (via Rust) ===== async markFavorite(itemId: string): Promise { - return invoke("repository_mark_favorite", { - handle: this.ensureHandle(), - itemId, - }); + await commands.repositoryMarkFavorite(this.ensureHandle(), itemId); } async unmarkFavorite(itemId: string): Promise { - return invoke("repository_unmark_favorite", { - handle: this.ensureHandle(), - itemId, - }); + await commands.repositoryUnmarkFavorite(this.ensureHandle(), itemId); } // ===== Person Methods (via Rust) ===== async getPerson(personId: string): Promise { - return invoke("repository_get_person", { - handle: this.ensureHandle(), - personId, - }); + return commands.repositoryGetPerson(this.ensureHandle(), personId); } async getItemsByPerson(personId: string, options?: GetItemsOptions): Promise { - return invoke("repository_get_items_by_person", { - handle: this.ensureHandle(), - personId, - options: options ?? null, - }); + return commands.repositoryGetItemsByPerson(this.ensureHandle(), personId, options ?? null); } async getSimilarItems(itemId: string, limit?: number): Promise { - return invoke("repository_get_similar_items", { - handle: this.ensureHandle(), - itemId, - limit: limit ?? null, - }); + return commands.repositoryGetSimilarItems(this.ensureHandle(), itemId, limit ?? null); } // ===== Playlist Methods (via Rust) ===== async createPlaylist(name: string, itemIds?: string[]): Promise { - return invoke("playlist_create", { - handle: this.ensureHandle(), - name, - itemIds: itemIds ?? null, - }); + return commands.playlistCreate(this.ensureHandle(), name, itemIds ?? null); } async deletePlaylist(playlistId: string): Promise { - return invoke("playlist_delete", { - handle: this.ensureHandle(), - playlistId, - }); + await commands.playlistDelete(this.ensureHandle(), playlistId); } async renamePlaylist(playlistId: string, name: string): Promise { - return invoke("playlist_rename", { - handle: this.ensureHandle(), - playlistId, - name, - }); + await commands.playlistRename(this.ensureHandle(), playlistId, name); } async getPlaylistItems(playlistId: string): Promise { - return invoke("playlist_get_items", { - handle: this.ensureHandle(), - playlistId, - }); + return commands.playlistGetItems(this.ensureHandle(), playlistId); } async addToPlaylist(playlistId: string, itemIds: string[]): Promise { - return invoke("playlist_add_items", { - handle: this.ensureHandle(), - playlistId, - itemIds, - }); + await commands.playlistAddItems(this.ensureHandle(), playlistId, itemIds); } async removeFromPlaylist(playlistId: string, entryIds: string[]): Promise { - return invoke("playlist_remove_items", { - handle: this.ensureHandle(), - playlistId, - entryIds, - }); + await commands.playlistRemoveItems(this.ensureHandle(), playlistId, entryIds); } async movePlaylistItem(playlistId: string, itemId: string, newIndex: number): Promise { - return invoke("playlist_move_item", { - handle: this.ensureHandle(), - playlistId, - itemId, - newIndex, - }); + await commands.playlistMoveItem(this.ensureHandle(), playlistId, itemId, newIndex); } // ===== Getters ===== diff --git a/src/lib/components/common/CachedImage.svelte b/src/lib/components/common/CachedImage.svelte index 98133e8..6024c1a 100644 --- a/src/lib/components/common/CachedImage.svelte +++ b/src/lib/components/common/CachedImage.svelte @@ -1,5 +1,5 @@