Files

161 lines
4.5 KiB
JavaScript
Raw Permalink Normal View History

2026-06-08 21:58:16 +12:00
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}`);
}
}