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