Files
jellytau/src/lib/services/imageCache.ts
dtourolle ad48d89dfe chore(format): run prettier over src/ and scripts/
Formatting was configured but never enforced: `bun run format:check`
reported 199 unformatted files and ran in no workflow and in no git hook,
so .prettierrc (printWidth 100, trailing commas) described an intention
rather than the tree.

This is the one-time sweep that makes the check gateable. Whitespace and
token-reflow only -- no behavioural change: `bun run check` reports 0
errors and all 1053 frontend tests pass before and after.

Kept out of every other commit on purpose. A 199-file diff mixed with
real changes is unreviewable, and the next commit turns format:check
into a hard CI gate so this cannot silently accumulate again.
2026-08-21 17:41:44 +02:00

134 lines
3.8 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";
import { createLogger } from "$lib/utils/logger";
const log = createLogger("ImageCache");
/**
* 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. This is the only remaining
// convertFileSrc caller, which is why the asset-protocol scope is narrowed
// to $APPDATA/thumbnails/** — a path outside it resolves to nothing.
// TRACES: UR-012 | DR-134, DR-198
return convertFileSrc(cachedPath);
}
} catch (e) {
log.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
log.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);
}