UX: remove auto-cover feature and add inline game editing
This commit is contained in:
180
api/server.js
180
api/server.js
@@ -837,175 +837,6 @@ async function lookupBarcode(barcodeRaw) {
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchJsonWithTimeout(url, timeoutMs = 5000) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
return await response.json();
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function uniqueQueries(candidates) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const value of candidates) {
|
||||
const normalized = normalizeText(value);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
const key = normalized.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
out.push(normalized);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractWikipediaThumbnail(payload, targetTitle) {
|
||||
if (!payload || !payload.query || !payload.query.pages) {
|
||||
return "";
|
||||
}
|
||||
const pages = Object.values(payload.query.pages);
|
||||
if (!pages.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const normalizedTitle = normalizeText(targetTitle).toLowerCase();
|
||||
const ranked = pages
|
||||
.filter((page) => page && page.thumbnail && page.thumbnail.source)
|
||||
.sort((a, b) => {
|
||||
const aTitle = normalizeText(a.title).toLowerCase();
|
||||
const bTitle = normalizeText(b.title).toLowerCase();
|
||||
const aScore = aTitle.includes(normalizedTitle) ? 1 : 0;
|
||||
const bScore = bTitle.includes(normalizedTitle) ? 1 : 0;
|
||||
return bScore - aScore;
|
||||
});
|
||||
|
||||
if (!ranked.length) {
|
||||
return "";
|
||||
}
|
||||
return normalizeText(ranked[0].thumbnail.source);
|
||||
}
|
||||
|
||||
async function fetchCoverFromWikipedia(title, consoleName, year) {
|
||||
const cleanTitle = normalizeText(title);
|
||||
if (!cleanTitle) {
|
||||
return "";
|
||||
}
|
||||
const cleanConsole = normalizeText(consoleName);
|
||||
const cleanYear = year != null ? String(year) : "";
|
||||
|
||||
const queries = uniqueQueries([
|
||||
cleanConsole ? `${cleanTitle} jeu video ${cleanConsole}` : "",
|
||||
cleanConsole ? `${cleanTitle} game ${cleanConsole}` : "",
|
||||
cleanConsole ? `${cleanTitle} ${cleanConsole} video game` : "",
|
||||
cleanYear ? `${cleanTitle} ${cleanYear} jeu video` : "",
|
||||
cleanYear ? `${cleanTitle} ${cleanYear} video game` : "",
|
||||
`${cleanTitle} jeu video`,
|
||||
`${cleanTitle} video game`,
|
||||
cleanTitle,
|
||||
]);
|
||||
|
||||
const wikiHosts = ["fr.wikipedia.org", "en.wikipedia.org"];
|
||||
for (const host of wikiHosts) {
|
||||
for (const query of queries) {
|
||||
const searchUrl =
|
||||
`https://${host}/w/api.php?action=query&format=json&generator=search&gsrlimit=8` +
|
||||
"&prop=pageimages|info&inprop=url&piprop=thumbnail&pithumbsize=280&redirects=1&origin=*" +
|
||||
`&gsrsearch=${encodeURIComponent(query)}`;
|
||||
const payload = await fetchJsonWithTimeout(searchUrl, 5500);
|
||||
const thumb = extractWikipediaThumbnail(payload, cleanTitle);
|
||||
if (thumb) {
|
||||
return thumb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function autoFillCovers(options = {}) {
|
||||
const overwrite = Boolean(options.overwrite);
|
||||
const limitInput = Number(options.limit);
|
||||
const limit = Number.isFinite(limitInput) && limitInput > 0 ? Math.min(limitInput, 500) : 250;
|
||||
|
||||
const whereClause = overwrite ? "" : "WHERE COALESCE(g.cover_url, '') = ''";
|
||||
const rowsResult = await pool.query(
|
||||
`
|
||||
SELECT
|
||||
g.id::text AS id,
|
||||
g.title,
|
||||
g.release_year,
|
||||
g.cover_url,
|
||||
c.name AS console_name
|
||||
FROM games g
|
||||
JOIN consoles c ON c.id = g.console_id
|
||||
${whereClause}
|
||||
ORDER BY g.created_at DESC
|
||||
LIMIT $1;
|
||||
`,
|
||||
[limit],
|
||||
);
|
||||
|
||||
let updated = 0;
|
||||
let notFound = 0;
|
||||
const sampleUpdated = [];
|
||||
const sampleNotFound = [];
|
||||
|
||||
for (const row of rowsResult.rows) {
|
||||
const coverUrl = await fetchCoverFromWikipedia(row.title, row.console_name, row.release_year);
|
||||
if (!coverUrl) {
|
||||
notFound += 1;
|
||||
if (sampleNotFound.length < 12) {
|
||||
sampleNotFound.push({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
consoleName: row.console_name,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
await pool.query("UPDATE games SET cover_url = $2 WHERE id = $1::uuid;", [row.id, coverUrl]);
|
||||
updated += 1;
|
||||
if (sampleUpdated.length < 12) {
|
||||
sampleUpdated.push({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
consoleName: row.console_name,
|
||||
});
|
||||
}
|
||||
await sleep(80);
|
||||
}
|
||||
|
||||
return {
|
||||
scanned: rowsResult.rows.length,
|
||||
updated,
|
||||
notFound,
|
||||
overwrite,
|
||||
sampleUpdated,
|
||||
sampleNotFound,
|
||||
};
|
||||
}
|
||||
|
||||
async function importCatalog(payload) {
|
||||
const brands = payload && payload.brands && typeof payload.brands === "object" ? payload.brands : {};
|
||||
const gamesByConsole =
|
||||
@@ -1555,17 +1386,6 @@ async function handleRequest(request, response) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "POST" && url.pathname === "/api/covers/autofill") {
|
||||
try {
|
||||
const body = await readJsonBody(request);
|
||||
const result = await autoFillCovers(body || {});
|
||||
sendJson(response, 200, { status: "ok", ...result });
|
||||
} catch (error) {
|
||||
sendJson(response, 400, { status: "error", message: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const barcodeLookupMatch = url.pathname.match(/^\/api\/barcode\/lookup\/([^/]+)$/);
|
||||
if (request.method === "GET" && barcodeLookupMatch) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user