add(core): re-apply the naming scheme to imported titles

This commit is contained in:
Steppenstreuner
2026-08-29 06:11:35 +02:00
parent 87b139e236
commit 3e28ad5f94
8 changed files with 345 additions and 9 deletions
+83 -6
View File
@@ -576,6 +576,7 @@ function openDetail(id, status) {
$("#detail-nocover").hidden = !!r.cover_url;
if (r.cover_url) img.src = r.cover_url;
$("#detail-retag").hidden = !(r.status === "imported" && r.media_type === "audiobook");
$("#detail-relocate").hidden = r.status !== "imported";
$("#detail-dialog").showModal();
}
@@ -596,6 +597,26 @@ async function saveDetail(form) {
});
}
// after correcting series/title/author the files are still filed under the old
// name - this re-applies the library's scheme to what is already on disk
$("#detail-relocate").addEventListener("click", async () => {
if (!detailRequest) return;
const btn = $("#detail-relocate");
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Lege ab…';
try {
await saveDetail($("#detail-form"));
const res = await api(`/api/requests/${detailRequest.id}/relocate`, { method: "POST" });
toast(res.moved ? `Neu abgelegt: ${res.dest}` : "Liegt bereits richtig");
$("#detail-dialog").close();
loadRequests(detailRequest.status);
} catch (err) { toast(err.message, true); }
finally {
btn.disabled = false;
btn.textContent = "Speichern & neu ablegen";
}
});
$("#detail-retag").addEventListener("click", async () => {
if (!detailRequest) return;
const btn = $("#detail-retag");
@@ -726,6 +747,9 @@ function renderImportTable() {
<td class="mono">
${esc(item.name)}${item.is_dir ? " 📁" : ""}
${item.parts ? `<span class="tag">${item.parts.length} Teile · ${item.files.length} Dateien</span>` : ""}
${!item.parts && item.maybe_separate
? `<span class="tag warn" title="Die ${item.files.length} Dateien tragen verschiedene Titel und sind je groß genug für ein ganzes Buch — mit ✂️ aufteilen">${item.files.length} Titel?</span>`
: ""}
${item.rel_dir ? `<div class="subpath">${esc(item.rel_dir)}/</div>` : ""}
</td>
<td>${esc(item.media_type)}</td>
@@ -734,7 +758,9 @@ function renderImportTable() {
${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>` : ""}
${item.parts || (item.is_dir && item.files.length > 1)
? `<button class="secondary" data-split="${i}" title="${item.parts ? "Zusammenfassung wieder auflösen" : "Ordner in einzelne Titel aufteilen"}">✂️</button>`
: ""}
</td>
${scoreCell(item)}
</tr>`;
@@ -747,7 +773,7 @@ function renderImportTable() {
b.addEventListener("click", () => openQuickDialog(parseInt(b.dataset.quick)))
);
tbody.querySelectorAll("[data-split]").forEach((b) =>
b.addEventListener("click", () => splitMergedItem(parseInt(b.dataset.split)))
b.addEventListener("click", () => splitItem(parseInt(b.dataset.split)))
);
tbody.querySelectorAll("[data-check]").forEach((c) =>
c.addEventListener("change", () => {
@@ -857,16 +883,37 @@ $("#import-merge").addEventListener("click", () => {
renderImportTable();
});
function splitMergedItem(i) {
function splitItem(i) {
const item = scanItems[i];
if (!item.parts) return;
scanItems.splice(i, 1, ...item.parts);
if (item.parts) { // undo a merge
scanItems.splice(i, 1, ...item.parts);
} else if (item.is_dir && item.files.length > 1) {
// a folder holding one book per file (a series bought as single titles):
// turn every file into its own entry so each can get its own request
const parts = item.files.map((f) => ({
path: f,
name: f.split("/").pop().replace(/\.[^.]+$/, ""),
rel_dir: item.rel_dir ? `${item.rel_dir}/${item.name}` : item.name,
media_type: item.media_type,
is_dir: false,
files: [f],
checked: false,
request_id: null,
suggested_request_id: null,
score: null,
}));
scanItems.splice(i, 1, ...parts);
toast(`${item.name}" in ${parts.length} Einzeltitel aufgeteilt`);
} else {
return;
}
renderImportTable();
}
// ---- quick request from a scanned file ----
let quickItemIndex = null;
let quickEpisode = null;
let quickPicked = null; // metadata hit the manual fields were filled from
let lastSeries = "";
// "017 - Titel", "Folge 17: Titel", "[003] Titel" -> episode number + rest.
@@ -908,9 +955,13 @@ function openQuickDialog(i) {
$("#quick-file").textContent = (item.rel_dir ? item.rel_dir + "/" : "") + item.name;
$("#quick-episode").textContent = number != null ? `Folge ${number} — nicht mitgesucht` : "";
$("#quick-q").value = cleanFileName(rest);
quickPicked = null;
$("#quick-m-title").value = cleanFileName(rest);
$("#quick-m-authors").value = "";
$("#quick-m-volume").value = number ?? "";
$("#quick-m-series").value = lastSeries;
$("#quick-based-on").hidden = true;
$("#quick-manual-summary").textContent = "Nicht bei Audible? Manuell anlegen";
$("#quick-manual").open = false;
$("#quick-library").innerHTML =
libOptions(item.media_type) || "<option value=''>— keine Library —</option>";
@@ -988,6 +1039,7 @@ async function runQuickSearch() {
<strong>${esc(r.title)}</strong>
<span class="muted">${meta}</span>
</div>
<button data-edit="${j}" class="secondary" title="Felder vor dem Anlegen anpassen (z.B. Serie)">✎</button>
<button data-pick="${j}" title="Anfrage anlegen und mit diesem Eintrag verbinden">Verbinden</button>
</div>`;
})
@@ -995,6 +1047,9 @@ async function runQuickSearch() {
box.querySelectorAll("[data-pick]").forEach((btn) =>
btn.addEventListener("click", () => pickQuickResult(results[btn.dataset.pick]))
);
box.querySelectorAll("[data-edit]").forEach((btn) =>
btn.addEventListener("click", () => editBeforeRequest(results[btn.dataset.edit]))
);
} catch (err) {
box.innerHTML = "";
toast(err.message, true);
@@ -1033,13 +1088,35 @@ function pickQuickResult(r) {
});
}
// live shows, box sets and fan releases are not in the Audible catalogue at all
// take a hit into the fields so title/author/series can be corrected before the
// request is created - Audible files Harry Potter under "Wizarding World", and
// the series ends up in the folder name
function editBeforeRequest(r) {
quickPicked = r;
$("#quick-m-title").value = r.title || "";
$("#quick-m-authors").value = r.authors || "";
$("#quick-m-series").value = r.series || "";
$("#quick-m-volume").value = r.volume ?? quickEpisode ?? "";
$("#quick-based-on").textContent =
`Übernommen von Audible: „${r.title}" — Sprecher, Cover, Jahr und ASIN bleiben erhalten.`;
$("#quick-based-on").hidden = false;
$("#quick-manual-summary").textContent = "Felder anpassen";
$("#quick-manual").open = true;
$("#quick-m-title").focus();
}
$("#quick-m-submit").addEventListener("click", () => {
const title = $("#quick-m-title").value.trim();
if (!title) { toast("Titel angeben", true); return; }
const volume = $("#quick-m-volume").value;
const base = quickPicked
? { narrator: quickPicked.narrator || "", external_id: quickPicked.external_id,
year: quickPicked.year, cover_url: quickPicked.cover_url }
: {};
return requestAndConnect({
...base,
title,
authors: $("#quick-m-authors").value.trim(),
series: (lastSeries = $("#quick-m-series").value.trim()),
volume: volume ? parseInt(volume) : null,
});
+9 -1
View File
@@ -213,9 +213,13 @@
</div>
<div id="quick-foot">
<details id="quick-manual">
<summary>Nicht bei Audible? Manuell anlegen</summary>
<summary id="quick-manual-summary">
Nicht bei Audible? Manuell anlegen
</summary>
<p class="muted" id="quick-based-on" hidden></p>
<div class="quick-manual-fields">
<label>Titel <input id="quick-m-title" /></label>
<label>Autor <input id="quick-m-authors" placeholder="optional" /></label>
<label>Serie <input id="quick-m-series" placeholder="optional" /></label>
<label class="narrow"
>Folge/Band <input id="quick-m-volume" type="number" min="0" /></label>
@@ -316,6 +320,10 @@
<button type="button" id="detail-retag" class="secondary" hidden>
Speichern &amp; neu taggen
</button>
<button type="button" id="detail-relocate" class="secondary" hidden
title="Ordner und Dateinamen nach dem Schema der Library neu anlegen">
Speichern &amp; neu ablegen
</button>
<button value="ok" id="detail-save">Speichern</button>
</div>
</form>
+1
View File
@@ -54,6 +54,7 @@ button:disabled { opacity: 0.6; cursor: default; }
.muted { color: var(--muted); font-size: 0.88rem; }
.mono { font-family: ui-monospace, monospace; font-size: 0.85rem; word-break: break-all; }
.subpath { color: var(--muted); font-size: 0.75rem; font-family: ui-monospace, monospace; }
.tag.warn { background: #8a6d1f; }
.tag { display: inline-block; margin-left: 0.4rem; padding: 0.05rem 0.4rem; border-radius: 4px; background: var(--accent); color: #fff; font-size: 0.7rem; font-family: system-ui, sans-serif; vertical-align: 0.1em; }
/* view modes (Missing/Importiert) */