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
+39 -157
View File
@@ -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<string> {
console.log("[RepositoryClient] Creating Rust repository...");
this.handle = await invoke<string>("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<void> {
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<Library[]> {
return invoke<Library[]>("repository_get_libraries", {
handle: this.ensureHandle(),
});
return commands.repositoryGetLibraries(this.ensureHandle());
}
async getItems(parentId: string, options?: GetItemsOptions): Promise<SearchResult> {
return invoke<SearchResult>("repository_get_items", {
handle: this.ensureHandle(),
parentId,
options: options ?? null,
});
return commands.repositoryGetItems(this.ensureHandle(), parentId, options ?? null);
}
async getItem(itemId: string): Promise<MediaItem> {
return invoke<MediaItem>("repository_get_item", {
handle: this.ensureHandle(),
itemId,
});
return commands.repositoryGetItem(this.ensureHandle(), itemId);
}
async getLatestItems(parentId: string, limit?: number): Promise<MediaItem[]> {
return invoke<MediaItem[]>("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<MediaItem[]> {
return invoke<MediaItem[]>("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<MediaItem[]> {
return invoke<MediaItem[]>("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<MediaItem[]> {
return invoke<MediaItem[]>("repository_get_recently_played_audio", {
handle: this.ensureHandle(),
limit: limit ?? null,
});
return commands.repositoryGetRecentlyPlayedAudio(this.ensureHandle(), limit ?? null);
}
async getResumeMovies(limit?: number): Promise<MediaItem[]> {
return invoke<MediaItem[]>("repository_get_resume_movies", {
handle: this.ensureHandle(),
limit: limit ?? null,
});
return commands.repositoryGetResumeMovies(this.ensureHandle(), limit ?? null);
}
async getGenres(parentId?: string): Promise<Genre[]> {
return invoke<Genre[]>("repository_get_genres", {
handle: this.ensureHandle(),
parentId: parentId ?? null,
});
return commands.repositoryGetGenres(this.ensureHandle(), parentId ?? null);
}
async search(query: string, options?: SearchOptions): Promise<SearchResult> {
return invoke<SearchResult>("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<PlaybackInfo> {
return invoke<PlaybackInfo>("repository_get_playback_info", {
handle: this.ensureHandle(),
itemId,
});
return commands.repositoryGetPlaybackInfo(this.ensureHandle(), itemId);
}
async reportPlaybackStart(itemId: string, positionTicks: number): Promise<void> {
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<void> {
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<void> {
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<string> {
return invoke<string>("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<string> {
return invoke<string>("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<string> {
return invoke<string>("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<string> {
return invoke<string>("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<string> {
return invoke<string>("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<void> {
return invoke("repository_mark_favorite", {
handle: this.ensureHandle(),
itemId,
});
await commands.repositoryMarkFavorite(this.ensureHandle(), itemId);
}
async unmarkFavorite(itemId: string): Promise<void> {
return invoke("repository_unmark_favorite", {
handle: this.ensureHandle(),
itemId,
});
await commands.repositoryUnmarkFavorite(this.ensureHandle(), itemId);
}
// ===== Person Methods (via Rust) =====
async getPerson(personId: string): Promise<MediaItem> {
return invoke<MediaItem>("repository_get_person", {
handle: this.ensureHandle(),
personId,
});
return commands.repositoryGetPerson(this.ensureHandle(), personId);
}
async getItemsByPerson(personId: string, options?: GetItemsOptions): Promise<SearchResult> {
return invoke<SearchResult>("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<SearchResult> {
return invoke<SearchResult>("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<PlaylistCreatedResult> {
return invoke<PlaylistCreatedResult>("playlist_create", {
handle: this.ensureHandle(),
name,
itemIds: itemIds ?? null,
});
return commands.playlistCreate(this.ensureHandle(), name, itemIds ?? null);
}
async deletePlaylist(playlistId: string): Promise<void> {
return invoke("playlist_delete", {
handle: this.ensureHandle(),
playlistId,
});
await commands.playlistDelete(this.ensureHandle(), playlistId);
}
async renamePlaylist(playlistId: string, name: string): Promise<void> {
return invoke("playlist_rename", {
handle: this.ensureHandle(),
playlistId,
name,
});
await commands.playlistRename(this.ensureHandle(), playlistId, name);
}
async getPlaylistItems(playlistId: string): Promise<PlaylistEntry[]> {
return invoke<PlaylistEntry[]>("playlist_get_items", {
handle: this.ensureHandle(),
playlistId,
});
return commands.playlistGetItems(this.ensureHandle(), playlistId);
}
async addToPlaylist(playlistId: string, itemIds: string[]): Promise<void> {
return invoke("playlist_add_items", {
handle: this.ensureHandle(),
playlistId,
itemIds,
});
await commands.playlistAddItems(this.ensureHandle(), playlistId, itemIds);
}
async removeFromPlaylist(playlistId: string, entryIds: string[]): Promise<void> {
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<void> {
return invoke("playlist_move_item", {
handle: this.ensureHandle(),
playlistId,
itemId,
newIndex,
});
await commands.playlistMoveItem(this.ensureHandle(), playlistId, itemId, newIndex);
}
// ===== Getters =====