fix(import): stop several entries from claiming one request
This commit is contained in:
+60
-14
@@ -621,6 +621,7 @@ $("#detail-form").addEventListener("submit", async (e) => {
|
||||
// ---- import ----
|
||||
let scanItems = [];
|
||||
let missingReqs = [];
|
||||
let importedReqs = [];
|
||||
let viewIdx = []; // indices into scanItems after filter/sort
|
||||
let importPage = 0;
|
||||
const IMPORT_PAGE_SIZE = 25;
|
||||
@@ -631,9 +632,10 @@ $("#scan-btn").addEventListener("click", async () => {
|
||||
$("#scan-info").innerHTML = '<span class="spinner"></span> Scanne…';
|
||||
try {
|
||||
const split = $("#split-dirs").checked;
|
||||
const [scan, reqs] = await Promise.all([
|
||||
const [scan, reqs, done] = await Promise.all([
|
||||
api("/api/import/scan?split_dirs=" + split),
|
||||
api("/api/requests?status=missing"),
|
||||
api("/api/requests?status=imported"),
|
||||
]);
|
||||
scanItems = scan.items;
|
||||
// selection state lives here, not in the DOM, so it survives paging/filtering
|
||||
@@ -642,6 +644,7 @@ $("#scan-btn").addEventListener("click", async () => {
|
||||
item.checked = !!item.suggested_request_id;
|
||||
});
|
||||
missingReqs = reqs;
|
||||
importedReqs = done;
|
||||
importPage = 0;
|
||||
$("#scan-info").textContent = `${scan.items.length} Kandidat(en) in ${scan.download_dir}`;
|
||||
$("#import-toolbar").hidden = scanItems.length === 0;
|
||||
@@ -699,13 +702,18 @@ function renderImportTable() {
|
||||
tbody.innerHTML = pageIdx
|
||||
.map((i) => {
|
||||
const item = scanItems[i];
|
||||
const option = (r, suffix = "") =>
|
||||
`<option value="${r.id}" ${r.id === item.request_id ? "selected" : ""}>
|
||||
${esc(r.title)} — ${esc(r.authors)} (${esc(r.library_name)})${suffix}</option>`;
|
||||
const opts = missingReqs
|
||||
.filter((r) => r.media_type === item.media_type)
|
||||
.map(
|
||||
(r) =>
|
||||
`<option value="${r.id}" ${r.id === item.request_id ? "selected" : ""}>
|
||||
${esc(r.title)} — ${esc(r.authors)} (${esc(r.library_name)})</option>`
|
||||
)
|
||||
.map((r) => option(r))
|
||||
.join("");
|
||||
// already imported audiobooks can take further parts (a late CD, or one
|
||||
// that failed while its siblings went through)
|
||||
const appendOpts = importedReqs
|
||||
.filter((r) => r.media_type === item.media_type && r.imported_path)
|
||||
.map((r) => option(r, " ↩︎"))
|
||||
.join("");
|
||||
return `<tr>
|
||||
<td><input type="checkbox" data-check="${i}" ${item.checked ? "checked" : ""}></td>
|
||||
@@ -716,7 +724,9 @@ function renderImportTable() {
|
||||
</td>
|
||||
<td>${esc(item.media_type)}</td>
|
||||
<td class="row">
|
||||
<select data-select="${i}"><option value="">— überspringen —</option>${opts}</select>
|
||||
<select data-select="${i}"><option value="">— überspringen —</option>${opts}
|
||||
${appendOpts ? `<optgroup label="Bereits importiert — Teile anhängen">${appendOpts}</optgroup>` : ""}
|
||||
</select>
|
||||
<button class="secondary" data-quick="${i}" title="Metadaten suchen und Anfrage direkt verbinden">🔍</button>
|
||||
${item.parts ? `<button class="secondary" data-split="${i}" title="Zusammenfassung wieder auflösen">✂️</button>` : ""}
|
||||
</td>
|
||||
@@ -1002,14 +1012,50 @@ async function pickQuickResult(r) {
|
||||
// ---- import execution (batched, with progress) ----
|
||||
const IMPORT_BATCH_SIZE = 20;
|
||||
|
||||
// several entries on one request means one audiobook split across folders:
|
||||
// importing them one by one only imports the first and fails the rest
|
||||
function collapseSharedRequests(chosen) {
|
||||
const byRequest = new Map();
|
||||
for (const item of chosen) {
|
||||
const group = byRequest.get(item.request_id);
|
||||
if (group) group.push(item);
|
||||
else byRequest.set(item.request_id, [item]);
|
||||
}
|
||||
const shared = [...byRequest.values()].filter((g) => g.length > 1);
|
||||
if (shared.length) {
|
||||
const names = shared
|
||||
.map((g) => `• ${g.map((it) => it.name).join(" + ")}`)
|
||||
.join("\n");
|
||||
const ok = confirm(
|
||||
`${shared.length} Anfrage(n) sind mehreren Einträgen zugeordnet:\n\n${names}\n\n` +
|
||||
"Als je ein Hörbuch mit durchlaufenden Parts importieren?\n" +
|
||||
"(Abbrechen: nichts wird importiert)"
|
||||
);
|
||||
if (!ok) return null;
|
||||
}
|
||||
return [...byRequest.values()].map((group) => ({
|
||||
path: group.length > 1 ? sharedParent(group) : group[0].path,
|
||||
is_dir: group.length > 1 ? true : group[0].is_dir,
|
||||
files: group.flatMap((it) => it.files),
|
||||
request_id: group[0].request_id,
|
||||
append: importedReqs.some((r) => r.id === group[0].request_id),
|
||||
}));
|
||||
}
|
||||
|
||||
// deepest folder that holds all of the group's entries
|
||||
function sharedParent(group) {
|
||||
const parts = group.map((it) => it.path.split("/"));
|
||||
const first = parts[0];
|
||||
let i = 0;
|
||||
while (i < first.length - 1 && parts.every((p) => p[i] === first[i])) i++;
|
||||
return first.slice(0, i).join("/") || group[0].path;
|
||||
}
|
||||
|
||||
$("#import-btn").addEventListener("click", async () => {
|
||||
const items = scanItems
|
||||
.filter((item) => item.checked && item.request_id)
|
||||
.map((item) => ({
|
||||
path: item.path, is_dir: item.is_dir, files: item.files,
|
||||
request_id: item.request_id,
|
||||
}));
|
||||
if (!items.length) { toast("Nichts ausgewählt", true); return; }
|
||||
const chosen = scanItems.filter((item) => item.checked && item.request_id);
|
||||
if (!chosen.length) { toast("Nichts ausgewählt", true); return; }
|
||||
const items = collapseSharedRequests(chosen);
|
||||
if (!items) return;
|
||||
const btn = $("#import-btn");
|
||||
btn.disabled = true;
|
||||
const allResults = [];
|
||||
|
||||
Reference in New Issue
Block a user