First working POC
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
// Thin TypeScript wrapper for Rust repository implementation
|
||||
// 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 type { QualityPreset } from "./quality-presets";
|
||||
import { QUALITY_PRESETS } from "./quality-presets";
|
||||
import type {
|
||||
Library,
|
||||
MediaItem,
|
||||
SearchResult,
|
||||
GetItemsOptions,
|
||||
SearchOptions,
|
||||
PlaybackInfo,
|
||||
ImageType,
|
||||
ImageOptions,
|
||||
Genre,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Repository client - thin wrapper over Rust HybridRepository
|
||||
* Uses handle-based system: create() returns a UUID handle for all operations
|
||||
*/
|
||||
export class RepositoryClient {
|
||||
private handle: string | null = null;
|
||||
private _serverUrl: string | null = null;
|
||||
private _accessToken: string | null = null;
|
||||
|
||||
/**
|
||||
* Create a new repository instance in Rust
|
||||
* Returns the repository handle for subsequent operations
|
||||
*/
|
||||
async create(
|
||||
serverUrl: string,
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
serverId: string
|
||||
): Promise<string> {
|
||||
console.log("[RepositoryClient] Creating Rust repository...");
|
||||
this.handle = await invoke<string>("repository_create", {
|
||||
serverUrl,
|
||||
userId,
|
||||
accessToken,
|
||||
serverId,
|
||||
});
|
||||
|
||||
// Store for URL construction
|
||||
this._serverUrl = serverUrl;
|
||||
this._accessToken = accessToken;
|
||||
|
||||
console.log("[RepositoryClient] Repository created with handle:", this.handle);
|
||||
return this.handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the repository instance in Rust
|
||||
* Call this on logout or when switching servers
|
||||
*/
|
||||
async destroy(): Promise<void> {
|
||||
if (this.handle) {
|
||||
await invoke("repository_destroy", { handle: this.handle });
|
||||
this.handle = null;
|
||||
this._serverUrl = null;
|
||||
this._accessToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
private ensureHandle(): string {
|
||||
if (!this.handle) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
return this.handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the repository handle for passing to backend commands
|
||||
*/
|
||||
getHandle(): string {
|
||||
return this.ensureHandle();
|
||||
}
|
||||
|
||||
// ===== Library Methods (all via Rust) =====
|
||||
|
||||
async getLibraries(): Promise<Library[]> {
|
||||
return invoke<Library[]>("repository_get_libraries", {
|
||||
handle: this.ensureHandle(),
|
||||
});
|
||||
}
|
||||
|
||||
async getItems(parentId: string, options?: GetItemsOptions): Promise<SearchResult> {
|
||||
return invoke<SearchResult>("repository_get_items", {
|
||||
handle: this.ensureHandle(),
|
||||
parentId,
|
||||
options: options ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getItem(itemId: string): Promise<MediaItem> {
|
||||
return invoke<MediaItem>("repository_get_item", {
|
||||
handle: 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,
|
||||
});
|
||||
}
|
||||
|
||||
async getResumeItems(parentId?: string, limit?: number): Promise<MediaItem[]> {
|
||||
return invoke<MediaItem[]>("repository_get_resume_items", {
|
||||
handle: this.ensureHandle(),
|
||||
parentId: parentId ?? null,
|
||||
limit: 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,
|
||||
});
|
||||
}
|
||||
|
||||
async getRecentlyPlayedAudio(limit?: number): Promise<MediaItem[]> {
|
||||
return invoke<MediaItem[]>("repository_get_recently_played_audio", {
|
||||
handle: this.ensureHandle(),
|
||||
limit: limit ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getResumeMovies(limit?: number): Promise<MediaItem[]> {
|
||||
return invoke<MediaItem[]>("repository_get_resume_movies", {
|
||||
handle: this.ensureHandle(),
|
||||
limit: limit ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getGenres(parentId?: string): Promise<Genre[]> {
|
||||
return invoke<Genre[]>("repository_get_genres", {
|
||||
handle: this.ensureHandle(),
|
||||
parentId: parentId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async search(query: string, options?: SearchOptions): Promise<SearchResult> {
|
||||
return invoke<SearchResult>("repository_search", {
|
||||
handle: this.ensureHandle(),
|
||||
query,
|
||||
options: options ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Playback Methods (all via Rust) =====
|
||||
|
||||
async getPlaybackInfo(itemId: string): Promise<PlaybackInfo> {
|
||||
return invoke<PlaybackInfo>("repository_get_playback_info", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
async reportPlaybackStart(itemId: string, positionTicks: number): Promise<void> {
|
||||
return invoke("repository_report_playback_start", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
positionTicks,
|
||||
});
|
||||
}
|
||||
|
||||
async reportPlaybackProgress(itemId: string, positionTicks: number): Promise<void> {
|
||||
return invoke("repository_report_playback_progress", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
positionTicks,
|
||||
});
|
||||
}
|
||||
|
||||
async reportPlaybackStopped(itemId: string, positionTicks: number): Promise<void> {
|
||||
return invoke("repository_report_playback_stopped", {
|
||||
handle: 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,
|
||||
});
|
||||
}
|
||||
|
||||
async getVideoStreamUrl(
|
||||
itemId: string,
|
||||
mediaSourceId?: string,
|
||||
startTimeSeconds?: number,
|
||||
audioStreamIndex?: number
|
||||
): Promise<string> {
|
||||
return invoke<string>("repository_get_video_stream_url", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
mediaSourceId: mediaSourceId ?? null,
|
||||
startTimeSeconds: startTimeSeconds ?? null,
|
||||
audioStreamIndex: audioStreamIndex ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== URL Construction Methods (sync, no server call) =====
|
||||
|
||||
/**
|
||||
* Get image URL - constructs URL synchronously (no server call)
|
||||
*/
|
||||
getImageUrl(itemId: string, imageType: ImageType = "Primary", options?: ImageOptions): string {
|
||||
if (!this._serverUrl || !this._accessToken) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
|
||||
let url = `${this._serverUrl}/Items/${itemId}/Images/${imageType}`;
|
||||
const params: string[] = [`api_key=${this._accessToken}`];
|
||||
|
||||
if (options) {
|
||||
if (options.maxWidth) params.push(`maxWidth=${options.maxWidth}`);
|
||||
if (options.maxHeight) params.push(`maxHeight=${options.maxHeight}`);
|
||||
if (options.quality) params.push(`quality=${options.quality}`);
|
||||
if (options.tag) params.push(`tag=${options.tag}`);
|
||||
}
|
||||
|
||||
return `${url}?${params.join('&')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subtitle URL - constructs URL synchronously (no server call)
|
||||
*/
|
||||
getSubtitleUrl(itemId: string, mediaSourceId: string, streamIndex: number, format: string = "vtt"): string {
|
||||
if (!this._serverUrl || !this._accessToken) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
return `${this._serverUrl}/Videos/${itemId}/${mediaSourceId}/Subtitles/${streamIndex}/Stream.${format}?api_key=${this._accessToken}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get video download URL with quality preset - constructs URL synchronously
|
||||
* Used for offline downloads
|
||||
*/
|
||||
getVideoDownloadUrl(
|
||||
itemId: string,
|
||||
quality: QualityPreset = "original",
|
||||
mediaSourceId?: string
|
||||
): string {
|
||||
if (!this._serverUrl || !this._accessToken) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
|
||||
const preset = QUALITY_PRESETS[quality];
|
||||
|
||||
if (quality === "original" || !preset.videoBitrate) {
|
||||
// Direct stream for original quality
|
||||
const params = new URLSearchParams({
|
||||
api_key: this._accessToken,
|
||||
Static: "true",
|
||||
audioStreamIndex: "0",
|
||||
});
|
||||
if (mediaSourceId) {
|
||||
params.append("MediaSourceId", mediaSourceId);
|
||||
}
|
||||
return `${this._serverUrl}/Videos/${itemId}/stream?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Transcoded download with quality preset
|
||||
const params = new URLSearchParams({
|
||||
api_key: this._accessToken,
|
||||
DeviceId: localStorage.getItem("jellytau_device_id") || "jellytau",
|
||||
Container: "mp4",
|
||||
VideoCodec: "h264",
|
||||
AudioCodec: "aac",
|
||||
AudioStreamIndex: "0",
|
||||
VideoBitrate: preset.videoBitrate.toString(),
|
||||
AudioBitrate: preset.audioBitrate.toString(),
|
||||
MaxHeight: preset.maxHeight?.toString() ?? "",
|
||||
TranscodingMaxAudioChannels: "2",
|
||||
});
|
||||
|
||||
if (mediaSourceId) {
|
||||
params.append("MediaSourceId", mediaSourceId);
|
||||
}
|
||||
|
||||
return `${this._serverUrl}/Videos/${itemId}/stream.mp4?${params.toString()}`;
|
||||
}
|
||||
|
||||
// ===== Favorite Methods (via Rust) =====
|
||||
|
||||
async markFavorite(itemId: string): Promise<void> {
|
||||
return invoke("repository_mark_favorite", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
async unmarkFavorite(itemId: string): Promise<void> {
|
||||
return invoke("repository_unmark_favorite", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Person Methods (via Rust) =====
|
||||
|
||||
async getPerson(personId: string): Promise<MediaItem> {
|
||||
return invoke<MediaItem>("repository_get_person", {
|
||||
handle: 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,
|
||||
});
|
||||
}
|
||||
|
||||
async getSimilarItems(itemId: string, limit?: number): Promise<SearchResult> {
|
||||
return invoke<SearchResult>("repository_get_similar_items", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
limit: limit ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Getters =====
|
||||
|
||||
get serverUrl(): string {
|
||||
if (!this._serverUrl) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
return this._serverUrl;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user