First working POC

This commit is contained in:
2026-01-26 22:21:54 +01:00
commit cfddc1edea
255 changed files with 77606 additions and 0 deletions
+346
View File
@@ -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;
}
}