Homelabtoolkit v2
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { normalizeLookupItem } from '../../../lib/collection-tools.js';
|
||||
import {
|
||||
buildSingleGenreUpdate,
|
||||
normalizeGenreNames,
|
||||
pickSuggestedGenre
|
||||
} from '../../../lib/genre-cleanup.js';
|
||||
import { fetchEmby, fetchEmbyJson, normalizeEmbyGuid } from '../../../lib/server/emby-api.js';
|
||||
import { fetchTmdbDetails, searchTmdbByTitle } from '../../../lib/server/tmdb-api.js';
|
||||
|
||||
function getItemTmdbId(item) {
|
||||
const providerIds = item?.ProviderIds || item?.providerIds || {};
|
||||
return String(providerIds.Tmdb || providerIds.TMDB || providerIds.tmdb || '').trim();
|
||||
}
|
||||
|
||||
function getTmdbMediaType(item) {
|
||||
return item?.Type === 'Series' || item?.type === 'Series' ? 'tv' : 'movie';
|
||||
}
|
||||
|
||||
function normalizeTitle(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '');
|
||||
}
|
||||
|
||||
async function fetchFullItem(userId, itemId) {
|
||||
const candidates = [
|
||||
() => fetchEmbyJson(`/Users/${encodeURIComponent(userId)}/Items/${encodeURIComponent(itemId)}`),
|
||||
() =>
|
||||
fetchEmbyJson(`/Items/${encodeURIComponent(itemId)}`, {
|
||||
params: { UserId: userId }
|
||||
})
|
||||
];
|
||||
|
||||
let lastError = null;
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
return await candidate();
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Could not fetch item details from Emby');
|
||||
}
|
||||
|
||||
async function resolveTmdbMatch(item) {
|
||||
const directTmdbId = getItemTmdbId(item);
|
||||
if (directTmdbId) {
|
||||
return {
|
||||
tmdbId: Number(directTmdbId),
|
||||
mediaType: getTmdbMediaType(item),
|
||||
source: 'providerId'
|
||||
};
|
||||
}
|
||||
|
||||
const matches = await searchTmdbByTitle({
|
||||
mediaType: getTmdbMediaType(item),
|
||||
name: item?.Name || item?.name,
|
||||
year: item?.ProductionYear || item?.year
|
||||
});
|
||||
const exactTitle = normalizeTitle(item?.Name || item?.name);
|
||||
const bestMatch =
|
||||
matches.find(
|
||||
(candidate) =>
|
||||
normalizeTitle(candidate?.name) === exactTitle &&
|
||||
(!item?.ProductionYear || !candidate?.year || candidate.year === item.ProductionYear)
|
||||
) || matches[0];
|
||||
|
||||
if (!bestMatch?.tmdbId) return null;
|
||||
|
||||
return {
|
||||
tmdbId: Number(bestMatch.tmdbId),
|
||||
mediaType: bestMatch.mediaType,
|
||||
source: 'search'
|
||||
};
|
||||
}
|
||||
|
||||
async function inspectItemGenres(userId, itemId) {
|
||||
const fullItem = await fetchFullItem(userId, itemId);
|
||||
const normalizedItem = normalizeLookupItem(fullItem);
|
||||
const currentGenres = normalizeGenreNames([
|
||||
...(fullItem?.Genres || []),
|
||||
...((fullItem?.GenreItems || []).map((entry) => entry?.Name))
|
||||
]);
|
||||
|
||||
const tmdbMatch = await resolveTmdbMatch(fullItem);
|
||||
if (!tmdbMatch?.tmdbId) {
|
||||
return {
|
||||
item: normalizedItem,
|
||||
currentGenres,
|
||||
tmdb: null,
|
||||
suggestedGenre: ''
|
||||
};
|
||||
}
|
||||
|
||||
const details = await fetchTmdbDetails(tmdbMatch);
|
||||
const tmdbGenres = normalizeGenreNames((details?.genres || []).map((genre) => genre?.name));
|
||||
|
||||
return {
|
||||
item: normalizedItem,
|
||||
currentGenres,
|
||||
tmdb: {
|
||||
tmdbId: tmdbMatch.tmdbId,
|
||||
mediaType: tmdbMatch.mediaType,
|
||||
source: tmdbMatch.source,
|
||||
genres: tmdbGenres
|
||||
},
|
||||
suggestedGenre: pickSuggestedGenre(tmdbGenres, currentGenres)
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET({ url }) {
|
||||
const userId = normalizeEmbyGuid(url.searchParams.get('userId'));
|
||||
const itemId = String(url.searchParams.get('itemId') || '').trim();
|
||||
|
||||
if (!userId) {
|
||||
throw error(400, 'Missing userId');
|
||||
}
|
||||
|
||||
if (!itemId) {
|
||||
throw error(400, 'Missing itemId');
|
||||
}
|
||||
|
||||
try {
|
||||
return json(await inspectItemGenres(userId, itemId));
|
||||
} catch (err) {
|
||||
if (String(err.message || '').includes('not configured')) {
|
||||
throw error(400, err.message);
|
||||
}
|
||||
throw error(502, `Could not inspect item genres: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const body = await request.json();
|
||||
const userId = normalizeEmbyGuid(body?.userId);
|
||||
const itemId = String(body?.itemId || '').trim();
|
||||
const genreName = String(body?.genreName || '').trim();
|
||||
|
||||
if (!userId) {
|
||||
throw error(400, 'Missing userId');
|
||||
}
|
||||
|
||||
if (!itemId) {
|
||||
throw error(400, 'Missing itemId');
|
||||
}
|
||||
|
||||
if (!genreName) {
|
||||
throw error(400, 'Missing genreName');
|
||||
}
|
||||
|
||||
try {
|
||||
const fullItem = await fetchFullItem(userId, itemId);
|
||||
const updatedItem = buildSingleGenreUpdate(fullItem, genreName);
|
||||
|
||||
await fetchEmby(`/Items/${encodeURIComponent(itemId)}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(updatedItem)
|
||||
});
|
||||
|
||||
return json(await inspectItemGenres(userId, itemId));
|
||||
} catch (err) {
|
||||
if (err?.status) throw err;
|
||||
if (String(err.message || '').includes('not configured')) {
|
||||
throw error(400, err.message);
|
||||
}
|
||||
throw error(502, `Could not update item genres: ${err.message}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user