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.
This commit is contained in:
@@ -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> = {}): 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) {
|
||||
|
||||
+9
-11
@@ -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<AutoplaySettings> {
|
||||
return invoke("player_get_autoplay_settings");
|
||||
return commands.playerGetAutoplaySettings();
|
||||
}
|
||||
|
||||
export async function setAutoplaySettings(
|
||||
settings: AutoplaySettings
|
||||
): Promise<AutoplaySettings> {
|
||||
return invoke("player_set_autoplay_settings", { settings });
|
||||
return commands.playerSetAutoplaySettings(auth.getUserId() ?? "", settings);
|
||||
}
|
||||
|
||||
export async function cancelAutoplayCountdown(): Promise<void> {
|
||||
return invoke("player_cancel_autoplay_countdown");
|
||||
await commands.playerCancelAutoplayCountdown();
|
||||
}
|
||||
|
||||
export async function playNextEpisode(item: any): Promise<void> {
|
||||
return invoke("player_play_next_episode", { item });
|
||||
export async function playNextEpisode(item: PlayItemRequest): Promise<void> {
|
||||
await commands.playerPlayNextEpisode(item);
|
||||
}
|
||||
|
||||
@@ -1121,6 +1121,18 @@ async repositoryGetItemsByPerson(handle: string, personId: string, options: GetI
|
||||
async repositoryGetSimilarItems(handle: string, itemId: string, limit: number | null) : Promise<SearchResult> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
return await TAURI_INVOKE("repository_get_video_download_url", { handle, itemId, quality, mediaSourceId });
|
||||
},
|
||||
/**
|
||||
* Create a new playlist
|
||||
*/
|
||||
|
||||
@@ -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 =====
|
||||
|
||||
Reference in New Issue
Block a user