TRACES: | DR-204 484 ungated `console.*` calls across 63 non-test frontend files shipped to end users with no way to turn them off. Mechanical substitution, no control flow, error handling or message semantics changed: console.log / console.debug -> log.debug console.info -> log.info console.warn -> log.warn console.error -> log.error Hand-written `"[Scope] …"` prefixes are dropped where the logger's scope now carries them; scope names that already existed are preserved verbatim (`[Auth]`, `[VideoPlayer]`, `[PiP]`, …) and inferred from the filename where a file had none. `src/routes/player/[id]/+page.svelte` keeps its `NextEpisode` and `AutoPlay` sub-scopes as separate loggers rather than flattening them into the page scope. `grep -rn 'console\.' src/` now matches nothing outside the tests and the facade itself.
135 lines
3.8 KiB
TypeScript
135 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);
|
|
}
|