add(core): re-apply the naming scheme to imported titles
This commit is contained in:
+83
-6
@@ -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
@@ -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 & neu taggen
|
||||
</button>
|
||||
<button type="button" id="detail-relocate" class="secondary" hidden
|
||||
title="Ordner und Dateinamen nach dem Schema der Library neu anlegen">
|
||||
Speichern & neu ablegen
|
||||
</button>
|
||||
<button value="ok" id="detail-save">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -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) */
|
||||
|
||||
Reference in New Issue
Block a user