feat(genre): add 30-day localStorage cache for genres

This commit is contained in:
vicrodh committed 2026-02-02 11:06:32 -06:00
1 parent 586dfb5754
commit e599a7b803
1 file changed
+87 -2
+87 -2
View File
@@ -42,6 +42,48 @@ const STORAGE_KEYS: Record<GenreFilterContext, string> = {
favorites: 'qbz_genre_filter_favorites',
};
const GENRE_CACHE_KEY = 'qbz_genre_cache';
const GENRE_CACHE_TTL_DAYS = 30;
interface GenreCache {
timestamp: number;
parentGenres: GenreInfo[];
childrenByParent: Record<number, GenreInfo[]>;
}
function saveGenreCache() {
try {
const cache: GenreCache = {
timestamp: Date.now(),
parentGenres: state.parentGenres,
childrenByParent: Object.fromEntries(state.childrenByParent),
};
localStorage.setItem(GENRE_CACHE_KEY, JSON.stringify(cache));
} catch (e) {
console.error('Failed to save genre cache:', e);
}
}
function loadGenreCache(): GenreCache | null {
try {
const stored = localStorage.getItem(GENRE_CACHE_KEY);
if (!stored) return null;
const cache: GenreCache = JSON.parse(stored);
const ageInDays = (Date.now() - cache.timestamp) / (1000 * 60 * 60 * 24);
if (ageInDays > GENRE_CACHE_TTL_DAYS) {
localStorage.removeItem(GENRE_CACHE_KEY);
return null;
}
return cache;
} catch (e) {
console.error('Failed to load genre cache:', e);
return null;
}
}
let currentContext: GenreFilterContext = 'home';
const state: GenreFilterState = {
@@ -114,10 +156,24 @@ export function getContext(): GenreFilterContext {
return currentContext;
}
/** Initial load: only fetch top-level genres (fast) */
/** Initial load: use cache if valid, otherwise fetch from API */
export async function loadGenres(): Promise<void> {
if (state.parentGenres.length > 0) return;
// Try loading from cache first
const cache = loadGenreCache();
if (cache) {
state.parentGenres = cache.parentGenres;
state.childrenByParent = new Map(Object.entries(cache.childrenByParent).map(([k, v]) => [Number(k), v]));
state.allGenres = [...cache.parentGenres, ...Object.values(cache.childrenByParent).flat()];
state.genreTree = buildTreeFromCache(cache);
loadFromStorage('home');
loadFromStorage('favorites');
notifyAll();
return;
}
// Fetch from API
state.isLoading = true;
notifyAll();
@@ -125,7 +181,6 @@ export async function loadGenres(): Promise<void> {
const parentGenres = await invoke<GenreInfo[]>('get_genres', {});
parentGenres.sort((a, b) => a.name.localeCompare(b.name));
// Create tree nodes without children (lazy loaded)
const genreTree: GenreTreeNode[] = parentGenres.map(genre => ({
genre: { ...genre, childrenLoaded: false },
children: [],
@@ -135,6 +190,7 @@ export async function loadGenres(): Promise<void> {
state.allGenres = [...parentGenres];
state.genreTree = genreTree;
saveGenreCache();
loadFromStorage('home');
loadFromStorage('favorites');
} catch (e) {
@@ -148,6 +204,32 @@ export async function loadGenres(): Promise<void> {
}
}
function buildTreeFromCache(cache: GenreCache): GenreTreeNode[] {
return cache.parentGenres.map(parent => {
const children = cache.childrenByParent[parent.id] || [];
return {
genre: { ...parent, childrenLoaded: children.length > 0 },
children: children.map(child => {
const grandchildren = cache.childrenByParent[child.id] || [];
return {
genre: { ...child, childrenLoaded: grandchildren.length > 0 },
children: grandchildren.map(gc => ({ genre: gc, children: [] })),
};
}),
};
});
}
/** Force refresh genres from API (clears cache) */
export async function forceRefreshGenres(): Promise<void> {
localStorage.removeItem(GENRE_CACHE_KEY);
state.parentGenres = [];
state.allGenres = [];
state.genreTree = [];
state.childrenByParent = new Map();
await loadGenres();
}
/** Lazy load children for a specific genre (silent = don't notify) */
export async function loadChildren(genreId: number, silent = false): Promise<GenreInfo[]> {
// Check if already loaded
@@ -171,6 +253,9 @@ export async function loadChildren(genreId: number, silent = false): Promise<Gen
// Update tree node
updateTreeNode(genreId, taggedChildren);
// Update cache
saveGenreCache();
if (!silent) {
notifyAll();
}