Homelabtoolkit v2
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user