fix(matcher): use folder path as series context
This commit is contained in:
+117
@@ -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");
|
||||
|
||||
@@ -73,6 +73,25 @@
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="series-dialog">
|
||||
<h3 id="series-title">Serie anfragen</h3>
|
||||
<p class="muted" id="series-info"></p>
|
||||
<div class="bar wrap">
|
||||
<label>von Folge <input id="series-from" type="number" min="0" /></label>
|
||||
<label>bis Folge <input id="series-to" type="number" min="0" /></label>
|
||||
<button type="button" id="series-all" class="secondary">Alle</button>
|
||||
<button type="button" id="series-none" class="secondary">Keine</button>
|
||||
</div>
|
||||
<div id="series-list" class="series-list"></div>
|
||||
<div class="row">
|
||||
<label>Library <select id="series-library"></select></label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button type="button" id="series-cancel" class="secondary">Abbrechen</button>
|
||||
<button type="button" id="series-submit">Anfragen</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</section>
|
||||
|
||||
<section id="view-missing" hidden>
|
||||
|
||||
@@ -97,6 +97,13 @@ dialog label { display: flex; flex-direction: column; gap: 0.2rem; margin: 0.5re
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
#quick-dialog { min-width: 480px; }
|
||||
#series-dialog { min-width: 520px; }
|
||||
.series-list { max-height: 45vh; overflow-y: auto; margin: 0.6rem 0; border: 1px solid var(--border); border-radius: 6px; }
|
||||
.series-list label { flex-direction: row; align-items: center; gap: 0.5rem; margin: 0; padding: 0.35rem 0.6rem; color: var(--text); font-size: 0.9rem; }
|
||||
.series-list label:nth-child(even) { background: rgba(255,255,255,0.03); }
|
||||
.series-list input[type=checkbox] { flex: none; }
|
||||
.series-list .num { flex: none; min-width: 3.2em; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.series-list .gap { padding: 0.35rem 0.6rem; color: var(--muted); font-size: 0.85rem; }
|
||||
#quick-results { display: flex; flex-direction: column; max-height: 50vh; overflow-y: auto; margin-top: 0.6rem; }
|
||||
.ok { color: var(--ok); }
|
||||
.error { color: #e08585; }
|
||||
@@ -110,5 +117,6 @@ h3, h4 { margin: 0.8rem 0 0.6rem; }
|
||||
main { padding: 0.8rem; }
|
||||
.cards { grid-template-columns: 1fr; }
|
||||
#quick-dialog { min-width: 0; width: 92vw; }
|
||||
#series-dialog { min-width: 0; width: 92vw; }
|
||||
td select { max-width: 200px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user