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
+279
View File
@@ -0,0 +1,279 @@
// Library state store
import { writable, derived } from "svelte/store";
import type { Library, MediaItem, SearchResult, Genre } from "$lib/api/types";
import { auth } from "./auth";
export type ViewMode = "grid" | "list";
interface LibraryState {
libraries: Library[];
currentLibrary: Library | null;
items: MediaItem[];
currentItem: MediaItem | null;
isLoading: boolean;
error: string | null;
totalItems: number;
searchQuery: string;
searchResults: MediaItem[];
viewMode: ViewMode;
genres: Genre[];
selectedGenres: string[];
}
function getStoredViewMode(): ViewMode {
if (typeof localStorage === "undefined") return "grid";
const stored = localStorage.getItem("jellytau-view-mode");
return stored === "list" ? "list" : "grid";
}
function createLibraryStore() {
const initialState: LibraryState = {
libraries: [],
currentLibrary: null,
items: [],
currentItem: null,
isLoading: false,
error: null,
totalItems: 0,
searchQuery: "",
searchResults: [],
viewMode: getStoredViewMode(),
genres: [],
selectedGenres: [],
};
const { subscribe, set, update } = writable<LibraryState>(initialState);
// Test log to confirm cache logging is active
console.log("✅ [LibraryStore] Cache logging enabled - you should see cache hit/miss logs below");
async function loadLibraries() {
update((s) => ({ ...s, isLoading: true, error: null }));
try {
const startTime = performance.now();
const repo = auth.getRepository();
console.log("📚 [LibraryStore] Loading libraries...");
const libraries = await repo.getLibraries();
const loadTime = Math.round(performance.now() - startTime);
if (loadTime < 100) {
console.log(`🚀 [LibraryStore] CACHE HIT! Loaded ${libraries.length} libraries in ${loadTime}ms (instant)`);
} else {
console.log(`⏳ [LibraryStore] Loaded ${libraries.length} libraries in ${loadTime}ms (from server)`);
}
update((s) => ({
...s,
libraries,
isLoading: false,
}));
return libraries;
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to load libraries";
update((s) => ({ ...s, isLoading: false, error: message }));
throw error;
}
}
async function loadItems(
parentId: string,
options: { startIndex?: number; limit?: number; genres?: string[] } = {}
) {
update((s) => ({ ...s, isLoading: true, error: null }));
try {
const startTime = performance.now();
const repo = auth.getRepository();
console.log(`📚 [LibraryStore] Loading items for parent: ${parentId.substring(0, 8)}...`);
const result = await repo.getItems(parentId, {
startIndex: options.startIndex ?? 0,
limit: options.limit ?? 10000,
fields: ["PrimaryImageAspectRatio", "Overview", "MediaStreams"],
sortBy: "SortName",
sortOrder: "Ascending",
genres: options.genres,
});
const loadTime = Math.round(performance.now() - startTime);
if (loadTime < 100) {
console.log(`🚀 [LibraryStore] CACHE HIT! Loaded ${result.items.length} items in ${loadTime}ms (instant)`);
} else {
console.log(`⏳ [LibraryStore] Loaded ${result.items.length} items in ${loadTime}ms (from server)`);
}
update((s) => ({
...s,
items: result.items,
totalItems: result.totalRecordCount,
isLoading: false,
}));
return result;
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to load items";
update((s) => ({ ...s, isLoading: false, error: message }));
throw error;
}
}
async function loadItem(itemId: string) {
update((s) => ({ ...s, isLoading: true, error: null }));
try {
const repo = auth.getRepository();
const item = await repo.getItem(itemId);
console.log(`[LibraryStore] loadItem(${itemId}): ${item.name} (${item.type})`);
console.log(`[LibraryStore] - Has people? ${item.people ? `YES (${item.people.length})` : 'NO'}`);
if (item.people && item.people.length > 0) {
item.people.forEach((p, i) => {
console.log(`[LibraryStore] [${i}] ${p.name} (type: "${p.type}", id: "${p.id}")`);
});
}
update((s) => ({
...s,
currentItem: item,
isLoading: false,
}));
return item;
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to load item";
update((s) => ({ ...s, isLoading: false, error: message }));
throw error;
}
}
async function search(query: string) {
if (!query.trim()) {
update((s) => ({ ...s, searchQuery: "", searchResults: [] }));
return;
}
update((s) => ({ ...s, isLoading: true, error: null, searchQuery: query }));
try {
const repo = auth.getRepository();
// Add 10-second timeout to prevent indefinite hanging
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Search timeout - please try again")), 10000)
);
const result = await Promise.race([
repo.search(query, { limit: 10000 }),
timeoutPromise
]);
update((s) => ({
...s,
searchResults: result.items,
isLoading: false,
}));
return result;
} catch (error) {
const message = error instanceof Error ? error.message : "Search failed";
update((s) => ({ ...s, isLoading: false, error: message }));
throw error;
}
}
function setCurrentLibrary(library: Library | null) {
update((s) => ({ ...s, currentLibrary: library, items: [], currentItem: null }));
}
function clearSearch() {
update((s) => ({ ...s, searchQuery: "", searchResults: [] }));
}
function setViewMode(mode: ViewMode) {
if (typeof localStorage !== "undefined") {
localStorage.setItem("jellytau-view-mode", mode);
}
update((s) => ({ ...s, viewMode: mode }));
}
function toggleViewMode() {
update((s) => {
const newMode = s.viewMode === "grid" ? "list" : "grid";
if (typeof localStorage !== "undefined") {
localStorage.setItem("jellytau-view-mode", newMode);
}
return { ...s, viewMode: newMode };
});
}
async function loadGenres(parentId?: string) {
try {
const repo = auth.getRepository();
const genres = await repo.getGenres(parentId);
update((s) => ({ ...s, genres }));
return genres;
} catch (error) {
console.error("Failed to load genres:", error);
return [];
}
}
function setSelectedGenres(genres: string[]) {
update((s) => ({ ...s, selectedGenres: genres }));
}
function toggleGenre(genreName: string) {
update((s) => {
const current = s.selectedGenres;
const newGenres = current.includes(genreName)
? current.filter((g) => g !== genreName)
: [...current, genreName];
return { ...s, selectedGenres: newGenres };
});
}
function clearGenres() {
update((s) => ({ ...s, selectedGenres: [] }));
}
function reset() {
set(initialState);
}
return {
subscribe,
loadLibraries,
loadItems,
loadItem,
search,
setCurrentLibrary,
clearSearch,
setViewMode,
toggleViewMode,
loadGenres,
setSelectedGenres,
toggleGenre,
clearGenres,
reset,
};
}
export const library = createLibraryStore();
// Derived stores
export const libraries = derived(library, ($lib) => $lib.libraries);
export const currentLibrary = derived(library, ($lib) => $lib.currentLibrary);
export const libraryItems = derived(library, ($lib) => $lib.items);
export const isLibraryLoading = derived(library, ($lib) => $lib.isLoading);
export const libraryError = derived(library, ($lib) => $lib.error);
export const viewMode = derived(library, ($lib) => $lib.viewMode);
export const genres = derived(library, ($lib) => $lib.genres);
export const selectedGenres = derived(library, ($lib) => $lib.selectedGenres);