fix(matcher): use folder path as series context

This commit is contained in:
Steppenstreuner
2026-08-28 20:01:15 +02:00
parent b8f042025d
commit 0844d303db
12 changed files with 475 additions and 47 deletions
+117
View File
@@ -219,11 +219,15 @@ $("#search-form").addEventListener("submit", async (e) => {
<div class="row">
<select data-lib="${i}">${opts || "<option value=''>— keine Library —</option>"}</select>
<button data-req="${i}">Anfragen</button>
${r.series_id ? `<button class="secondary" data-series="${i}">Ganze Serie…</button>` : ""}
</div>
</div>
</div>`
)
.join("");
box.querySelectorAll("[data-series]").forEach((btn) =>
btn.addEventListener("click", () => openSeriesDialog(results[btn.dataset.series]))
);
box.querySelectorAll("[data-req]").forEach((btn) =>
btn.addEventListener("click", async () => {
const i = btn.dataset.req;
@@ -255,6 +259,119 @@ $("#search-form").addEventListener("submit", async (e) => {
}
});
// ---- request a whole series ----
let seriesEpisodes = [];
function renderSeriesList() {
const from = parseInt($("#series-from").value);
const to = parseInt($("#series-to").value);
const inRange = (e) =>
(isNaN(from) || (e.volume != null && e.volume >= from)) &&
(isNaN(to) || (e.volume != null && e.volume <= to));
let previous = null;
$("#series-list").innerHTML = seriesEpisodes
.map((e, i) => {
// make holes in the numbering visible - the crawl cannot reach every episode
let gap = "";
if (e.volume != null && previous != null && e.volume > previous + 1) {
const missing = e.volume - previous - 1;
gap = `<div class="gap">… ${missing} Folge${missing > 1 ? "n" : ""} nicht bei Audible gefunden (${previous + 1}${e.volume - 1})</div>`;
}
if (e.volume != null) previous = e.volume;
return `${gap}<label>
<input type="checkbox" data-ep="${i}" ${inRange(e) ? "checked" : ""}>
<span class="num">${e.volume ?? "—"}</span>
<span>${esc(e.title)}</span>
</label>`;
})
.join("");
updateSeriesCount();
}
function seriesChecked() {
return [...$("#series-list").querySelectorAll("input:checked")].map(
(c) => seriesEpisodes[c.dataset.ep]
);
}
function updateSeriesCount() {
const n = seriesChecked().length;
const btn = $("#series-submit");
btn.textContent = n ? `${n} anfragen` : "Anfragen";
btn.disabled = n === 0;
}
async function openSeriesDialog(result) {
const dlg = $("#series-dialog");
$("#series-title").textContent = result.series || "Serie";
$("#series-info").innerHTML = '<span class="spinner"></span> Folgen werden gesucht…';
$("#series-list").innerHTML = "";
$("#series-from").value = "";
$("#series-to").value = "";
$("#series-library").innerHTML = libOptions("audiobook") || "<option value=''>— keine Library —</option>";
$("#series-submit").disabled = true;
dlg.showModal();
try {
seriesEpisodes = await api(
`/api/search/series?series_id=${encodeURIComponent(result.series_id)}` +
`&title=${encodeURIComponent(result.series || result.title)}`
);
} catch (err) {
$("#series-info").textContent = err.message;
return;
}
const numbered = seriesEpisodes.filter((e) => e.volume != null);
$("#series-info").textContent =
`${seriesEpisodes.length} Folgen gefunden` +
(numbered.length ? ` (Nr. ${numbered[0].volume}${numbered[numbered.length - 1].volume})` : "") +
". Audible hat keine Serien-Abfrage — einzelne Folgen können fehlen.";
renderSeriesList();
}
$("#series-from").addEventListener("input", renderSeriesList);
$("#series-to").addEventListener("input", renderSeriesList);
$("#series-list").addEventListener("change", updateSeriesCount);
$("#series-all").addEventListener("click", () => {
$("#series-list").querySelectorAll("input").forEach((c) => (c.checked = true));
updateSeriesCount();
});
$("#series-none").addEventListener("click", () => {
$("#series-list").querySelectorAll("input").forEach((c) => (c.checked = false));
updateSeriesCount();
});
$("#series-cancel").addEventListener("click", () => $("#series-dialog").close());
$("#series-submit").addEventListener("click", async () => {
const libId = $("#series-library").value;
if (!libId) { toast("Erst eine Audiobook-Library anlegen (Tab Libraries)", true); return; }
const items = seriesChecked().map((e) => ({
title: e.title, authors: e.authors, narrator: e.narrator || "",
external_id: e.external_id, year: e.year, series: e.series,
volume: e.volume, cover_url: e.cover_url,
}));
const btn = $("#series-submit");
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Lege an…';
try {
const res = await api("/api/requests/bulk-items", {
method: "POST",
body: JSON.stringify({ library_id: parseInt(libId), items }),
});
setLastLib("audiobook", libId);
toast(
`${res.created.length} Anfragen angelegt` +
(res.skipped ? `, ${res.skipped} bereits vorhanden` : "")
);
$("#series-dialog").close();
refreshMissingBadge();
} catch (err) {
toast(err.message, true);
} finally {
btn.disabled = false;
updateSeriesCount();
}
});
// ---- manual request ----
$("#manual-btn").addEventListener("click", () => {
const dlg = $("#manual-dialog");