Defer initial queries for genres, studios, watchlist until after startup

This commit is contained in:
HighImKevin
2026-10-08 10:31:08 -04:00
parent e434a5461f
commit e8f9690712
4 changed files with 361 additions and 22 deletions
+178 -12
View File
@@ -1014,23 +1014,37 @@
LOG('Queue updated successfully');
},
/**
* Gets a genre ID from cached movie genres
* Lookup a genre ID from the local movie-genres catalog (localStorage / memory).
* Never awaits network — schedules a background sync when missing or older than 24h.
* @param {string} genreName - Name of the genre to find
* @returns {Promise<string|null>} - Genre ID or null if not found
* @returns {Promise<string|null>} - Genre ID or null if not found / not yet cached
*/
getGenreId: async function(genreName) {
if (!cachedMovieGenres) {
const url = `${ApiClient.serverAddress()}/Genres?IncludeItemTypes=Movie`;
// Use getData to fetch the genres with caching
const genres = await this.getData(url, true, GENRE_TTL);
cachedMovieGenres = genres.Items;
if (!genreName) return null;
hydrateMovieGenresFromStorage();
const empty = !cachedMovieGenres || cachedMovieGenres.length === 0;
if (empty || isMovieGenresSyncDue()) {
ensureGenreSyncScheduled();
}
// Find the genre by name (case insensitive)
const genre = cachedMovieGenres.find(g => g.Name.toLowerCase() === genreName.toLowerCase());
if (!cachedMovieGenres || cachedMovieGenres.length === 0) return null;
const needle = String(genreName).toLowerCase();
const genre = cachedMovieGenres.find((g) => g?.Name && g.Name.toLowerCase() === needle);
return genre ? genre.Id : null;
},
/**
* Subscribe to movie-genres catalog sync completion (localStorage + memory refreshed).
* @param {Function} callback
* @returns {Function} unsubscribe
*/
onMovieGenresSynced: function(callback) {
if (typeof callback !== 'function') return () => {};
movieGenresSyncedListeners.add(callback);
return () => movieGenresSyncedListeners.delete(callback);
},
/**
* Build query URL from section query configuration
* Handles minAge/maxAge conversion to MinPremiereDate/MaxPremiereDate
@@ -1336,10 +1350,162 @@
};
const GENRE_TTL = 24 * 60 * 60 * 1000; // 24 hours
const GENRE_SETTLE_DELAY_MS = 1500;
let cachedMovieGenres = null;
let movieGenresLastSyncedAt = null;
let genreSyncScheduledPromise = null;
let genreSyncInFlightPromise = null;
const movieGenresSyncedListeners = new Set();
function normalizeServerAddressForStorage(serverAddress) {
if (!serverAddress) return '';
return String(serverAddress).replace(/\/+$/, '');
}
function getMovieGenresStorageKey() {
try {
const serverAddress = normalizeServerAddressForStorage(
(typeof ApiClient !== 'undefined' && ApiClient?.serverAddress?.()) || ''
);
if (!serverAddress) return null;
return `kefinTweaks_movieGenres_${encodeURIComponent(serverAddress)}`;
} catch (_) {
return null;
}
}
function readMovieGenresFromStorage() {
const key = getMovieGenresStorageKey();
if (!key) return null;
try {
const raw = localStorage.getItem(key);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || !Array.isArray(parsed.items)) return null;
return {
items: parsed.items.filter((g) => g && g.Id && g.Name),
lastSyncedAt: Number(parsed.lastSyncedAt) || 0
};
} catch (err) {
WARN('Failed to read movie genres from localStorage:', err);
return null;
}
}
function writeMovieGenresToStorage(items, lastSyncedAt) {
const key = getMovieGenresStorageKey();
if (!key) return;
try {
localStorage.setItem(key, JSON.stringify({
items: (items || []).map((g) => ({ Id: g.Id, Name: g.Name })),
lastSyncedAt: lastSyncedAt || Date.now()
}));
} catch (err) {
WARN('Failed to write movie genres to localStorage:', err);
}
}
function hydrateMovieGenresFromStorage() {
if (cachedMovieGenres && cachedMovieGenres.length > 0) return true;
const stored = readMovieGenresFromStorage();
if (!stored?.items?.length) return false;
cachedMovieGenres = stored.items;
movieGenresLastSyncedAt = stored.lastSyncedAt || movieGenresLastSyncedAt;
LOG(`Hydrated movie genres from localStorage: ${cachedMovieGenres.length} items`);
return true;
}
function isMovieGenresSyncDue() {
const stored = readMovieGenresFromStorage();
const last = movieGenresLastSyncedAt || stored?.lastSyncedAt || 0;
const hasItems = (cachedMovieGenres && cachedMovieGenres.length > 0) || (stored?.items?.length > 0);
if (!hasItems) return true;
if (!last) return true;
return (Date.now() - last) >= GENRE_TTL;
}
function waitForDocumentComplete() {
if (document.readyState === 'complete') return Promise.resolve();
return new Promise((resolve) => {
window.addEventListener('load', () => resolve(), { once: true });
});
}
function notifyMovieGenresSynced() {
try {
window.dispatchEvent(new CustomEvent('kefinTweaksMovieGenresSynced'));
} catch (_) { /* ignore */ }
movieGenresSyncedListeners.forEach((cb) => {
try {
cb();
} catch (err) {
WARN('onMovieGenresSynced listener failed:', err);
}
});
}
async function runMovieGenresSync() {
if (genreSyncInFlightPromise) return genreSyncInFlightPromise;
genreSyncInFlightPromise = (async () => {
try {
if (window.userHelper?.waitForLogin) {
await window.userHelper.waitForLogin();
}
ensureApiClient();
const url = `${ApiClient.serverAddress()}/Genres?IncludeItemTypes=Movie`;
const genres = await apiHelper.getData(url, true, GENRE_TTL);
const items = Array.isArray(genres?.Items) ? genres.Items : [];
cachedMovieGenres = items;
movieGenresLastSyncedAt = Date.now();
writeMovieGenresToStorage(items, movieGenresLastSyncedAt);
LOG(`Synced movie genres: ${items.length} items`);
notifyMovieGenresSynced();
} catch (err) {
ERR('Failed to sync movie genres:', err);
} finally {
genreSyncInFlightPromise = null;
}
})();
return genreSyncInFlightPromise;
}
/**
* Deduped: after settle, sync Genres when cache is missing or older than 24h.
*/
function ensureGenreSyncScheduled() {
hydrateMovieGenresFromStorage();
if (!isMovieGenresSyncDue()) {
return genreSyncScheduledPromise || Promise.resolve();
}
if (genreSyncScheduledPromise) return genreSyncScheduledPromise;
genreSyncScheduledPromise = (async () => {
try {
if (window.userHelper?.waitForLogin) {
await window.userHelper.waitForLogin();
}
await waitForDocumentComplete();
await new Promise((resolve) => setTimeout(resolve, GENRE_SETTLE_DELAY_MS));
hydrateMovieGenresFromStorage();
if (!isMovieGenresSyncDue()) return;
await runMovieGenresSync();
} catch (err) {
ERR('Failed to schedule movie genres sync:', err);
} finally {
genreSyncScheduledPromise = null;
}
})();
return genreSyncScheduledPromise;
}
// Expose apiHelper to global window object
window.apiHelper = apiHelper;
// Daily / empty-cache sync without waiting for a getGenreId caller
ensureGenreSyncScheduled();
console.log('[KefinTweaks APIHelper] Module loaded and available at window.apiHelper');
})();