import { readFileSync, existsSync } from 'fs'; import { resolve } from 'path'; import { json, error } from '@sveltejs/kit'; import { readCachedEmbyUserContext, writeCachedEmbyUserContext } from '../../../lib/server/emby-user-context-cache.js'; const CONFIG_PATH = resolve('config.json'); const EMBY_PAGE_SIZE = 200; function loadConfig() { if (!existsSync(CONFIG_PATH)) return {}; try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { return {}; } } function normalizeGuid(value) { return typeof value === 'string' ? value.replace(/-/g, '').trim().toLowerCase() : ''; } async function fetchJson(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`); } return response.json(); } async function fetchAllUserItems(base, embyGuid, apiKey, params = {}) { const allItems = []; let startIndex = 0; while (true) { const query = new URLSearchParams({ api_key: apiKey, Recursive: 'true', GroupItemsIntoCollections: 'false', Limit: String(EMBY_PAGE_SIZE), StartIndex: String(startIndex), ...Object.fromEntries( Object.entries(params).map(([key, value]) => [key, String(value)]) ) }); const payload = await fetchJson(`${base}/Users/${encodeURIComponent(embyGuid)}/Items?${query.toString()}`); const pageItems = payload.Items || payload || []; allItems.push(...pageItems); const total = Number(payload.TotalRecordCount || 0); if (!pageItems.length) break; if (total > 0 && allItems.length >= total) break; if (pageItems.length < EMBY_PAGE_SIZE) break; startIndex += pageItems.length; } return allItems; } function normalizeViews(items = []) { return items.map((item) => ({ id: String(item.Id || ''), name: item.Name || 'Unnamed view', type: item.CollectionType || item.Type || 'View' })); } function normalizeRecentlyPlayed(items = []) { return items.map((item) => ({ id: String(item.Id || ''), name: item.Name || item.SeriesName || 'Unknown item', type: item.Type || 'Item', seriesName: item.SeriesName || null, datePlayed: item.UserData?.LastPlayedDate || item.DateLastMediaAdded || null, isPlayed: item.UserData?.Played ?? true })); } function buildExcludedFolderLookup(items = []) { return Object.fromEntries( items .filter((item) => item?.Id) .map((item) => [ String(item.Id), { name: item.Name || item.Path || `Item ${item.Id}`, type: item.CollectionType || item.Type || 'Item' } ]) ); } export async function GET({ url }) { const embyGuid = normalizeGuid(url.searchParams.get('embyGuid')); const excludedIds = (url.searchParams.get('excludedIds') || '') .split(',') .map((id) => id.trim()) .filter(Boolean); if (!embyGuid) { throw error(400, 'Missing embyGuid'); } const { embyUrl, apiKey } = loadConfig(); const cached = readCachedEmbyUserContext(embyGuid); if (!embyUrl || !apiKey) { if (cached) { return json({ ...cached, source: 'cache' }); } throw error(400, 'Emby URL and API key not configured'); } const base = embyUrl.replace(/\/+$/, ''); try { const [viewsPayload, recentlyPlayedPayload, excludedPayload] = await Promise.all([ fetchJson(`${base}/Users/${encodeURIComponent(embyGuid)}/Views?api_key=${encodeURIComponent(apiKey)}`), fetchAllUserItems(base, embyGuid, apiKey, { Filters: 'IsPlayed', IncludeItemTypes: 'Movie,Series', SortBy: 'DatePlayed', SortOrder: 'Descending', Fields: 'UserData' }), excludedIds.length > 0 ? fetchJson( `${base}/Users/${encodeURIComponent(embyGuid)}/Items?api_key=${encodeURIComponent(apiKey)}&Ids=${encodeURIComponent(excludedIds.join(','))}&Fields=Path` ) : Promise.resolve({ Items: [] }) ]); const context = writeCachedEmbyUserContext(embyGuid, { views: normalizeViews(viewsPayload.Items || viewsPayload || []), recentlyPlayed: normalizeRecentlyPlayed(recentlyPlayedPayload), excludedFolderLookup: buildExcludedFolderLookup(excludedPayload.Items || excludedPayload || []), lastSyncedAt: new Date().toISOString() }); return json({ ...context, source: 'live' }); } catch (err) { if (cached) { const filteredLookup = excludedIds.length ? Object.fromEntries( Object.entries(cached.excludedFolderLookup || {}).filter(([id]) => excludedIds.includes(id)) ) : (cached.excludedFolderLookup || {}); return json({ ...cached, excludedFolderLookup: filteredLookup, source: 'cache', message: `Using cached Emby user context because live fetch failed: ${err.message}` }); } throw error(502, `Could not load Emby user context: ${err.message}`); } }