161 lines
4.5 KiB
JavaScript
161 lines
4.5 KiB
JavaScript
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}`);
|
|
}
|
|
}
|