Files
jellytau/src/lib/services/imageCache.ts
T
dtourolle d01c2aab9f
🏗️ 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
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.
2026-06-21 08:47:04 +02:00

129 lines
3.5 KiB
TypeScript

// Image cache service - Handles lazy caching of thumbnails with LRU eviction
// TRACES: UR-007 | DR-016
import { convertFileSrc } from "@tauri-apps/api/core";
import { commands } from "$lib/api/bindings";
/**
* Statistics about the thumbnail cache
*/
export interface ImageCacheStats {
totalSizeBytes: number;
itemCount: number;
limitBytes: number;
}
/**
* Get an image URL, checking cache first then falling back to server.
* Triggers background caching if not cached.
*
* @param serverUrl - The Jellyfin server base URL
* @param itemId - The Jellyfin item ID
* @param imageType - The image type (Primary, Backdrop, etc.)
* @param options - Image options (maxWidth, maxHeight, quality, tag)
* @returns The image URL (local asset URL if cached, server URL otherwise)
*/
export async function getCachedImageUrl(
serverUrl: string,
itemId: string,
imageType: string = "Primary",
options: {
maxWidth?: number;
maxHeight?: number;
quality?: number;
tag?: string;
} = {}
): Promise<string> {
const tag = options.tag || "default";
// Try to get cached version
try {
const cachedPath = await commands.thumbnailGetCached(itemId, imageType, tag);
if (cachedPath) {
// Convert file path to asset URL for Tauri
return convertFileSrc(cachedPath);
}
} catch (e) {
console.debug("Failed to check thumbnail cache:", e);
}
// Build server URL
const params = new URLSearchParams();
if (options.maxWidth) params.set("maxWidth", options.maxWidth.toString());
if (options.maxHeight) params.set("maxHeight", options.maxHeight.toString());
if (options.quality) params.set("quality", options.quality.toString());
if (options.tag) params.set("tag", options.tag);
const serverImageUrl = `${serverUrl}/Items/${itemId}/Images/${imageType}?${params.toString()}`;
// Trigger background caching (fire and forget)
commands.thumbnailSave(itemId, imageType, tag, serverImageUrl).catch((e) => {
// Silently fail - caching is best-effort
console.debug("Background thumbnail cache failed:", e);
});
// Return server URL for immediate display
return serverImageUrl;
}
/**
* Get thumbnail cache statistics
*/
export async function getCacheStats(): Promise<ImageCacheStats> {
return commands.thumbnailGetStats();
}
/**
* Set cache storage limit in bytes
*
* @param limitBytes - The maximum cache size in bytes (0 = unlimited)
*/
export async function setCacheLimit(limitBytes: number): Promise<void> {
await commands.thumbnailSetLimit(limitBytes);
}
/**
* Clear all cached thumbnails
*/
export async function clearCache(): Promise<void> {
await commands.thumbnailClearCache();
}
/**
* Delete cached thumbnails for a specific item
*
* @param itemId - The Jellyfin item ID
*/
export async function deleteItemCache(itemId: string): Promise<void> {
await commands.thumbnailDeleteItem(itemId);
}
/**
* Format bytes to human-readable string
*
* @param bytes - Number of bytes
* @returns Human-readable string (e.g., "1.5 GB")
*/
export function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024)
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
/**
* Convert gigabytes to bytes
*/
export function gbToBytes(gb: number): number {
return gb * 1024 * 1024 * 1024;
}
/**
* Convert bytes to gigabytes
*/
export function bytesToGb(bytes: number): number {
return bytes / (1024 * 1024 * 1024);
}