From 2e84bc8acad7e11793e12be47462865f0e5b252a Mon Sep 17 00:00:00 2001 From: Steppenstreuner Date: Mon, 31 Aug 2026 08:33:24 +0200 Subject: [PATCH] add(ui/ux): show file formats in the scan and filter by them --- README.md | 5 ++++- static/app.js | 39 ++++++++++++++++++++++++++++++++++ static/index.html | 3 +++ static/style.css | 1 + tests/test_import_flow.py | 42 +++++++++++++++++++++++++++++++++++++ tests/test_ui_dialogs.py | 18 ++++++++++++++++ wordarr/config.py | 8 +++++++ wordarr/importer/scanner.py | 19 +++++++++++++++++ 8 files changed, 134 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b50134b..16362b6 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ the same mount, otherwise moving turns into copy + delete. | `WORDARR_CONFIG_DIR` | `/config` | location of the SQLite database | | `WORDARR_AUDIBLE_REGIONS` | `de,com` | Audible marketplaces, first ranks first | | `WORDARR_MUSICBRAINZ_UA` | see `metadata/musicbrainz.py` | user agent MusicBrainz requires | +| `WORDARR_EXTRA_EBOOK_EXTENSIONS` | – | extra ebook formats the scan should offer, e.g. `.kepub,.lit,.rtf` | | `WORDARR_HARDCOVER_TOKEN` | – | Hardcover API token, the best ebook source; without it Hardcover is skipped | | `WORDARR_GOOGLE_BOOKS_KEY` | – | optional, only needed when the keyless Google Books quota runs dry | @@ -36,7 +37,9 @@ the same mount, otherwise moving turns into copy + delete. *Ganze Serie…* requests every episode at once. 3. **Import** – scan the download folder. wordarr suggests file→request matches, which you confirm or correct; entries can be merged, split, or turned into - requests from their folder names. Importing renames and moves the files + requests from their folder names. Every row names its format and size, and + the toolbar can filter by format - a Calibre export lists the same book once + per format, and only one of them belongs in the library. Importing renames and moves the files according to the library's scheme and writes audio tags. Imported titles can be edited afterwards; *Speichern & neu ablegen* re-applies diff --git a/static/app.js b/static/app.js index 42eb4f1..e053937 100644 --- a/static/app.js +++ b/static/app.js @@ -686,6 +686,7 @@ $("#scan-btn").addEventListener("click", async () => { importPage = 0; $("#scan-info").textContent = `${scan.items.length} Kandidat(en) in ${scan.download_dir}`; $("#import-toolbar").hidden = scanItems.length === 0; + formatOptions(); renderImportTable(); } catch (err) { $("#scan-info").textContent = ""; @@ -698,6 +699,7 @@ $("#scan-btn").addEventListener("click", async () => { function applyImportView() { const text = $("#import-filter-text").value.toLowerCase(); const mode = $("#import-filter-mode").value; + const format = $("#import-filter-format").value; const sort = $("#import-sort").value; viewIdx = scanItems .map((_, i) => i) @@ -707,12 +709,44 @@ function applyImportView() { if (mode === "suggested" && !item.suggested_request_id) return false; if (mode === "unassigned" && item.request_id) return false; if (mode === "conflict" && !item.conflict) return false; + if (format && !(item.formats || []).includes(format)) return false; return true; }); if (sort === "score") viewIdx.sort((a, b) => scanItems[b].score - scanItems[a].score); if (sort === "name") viewIdx.sort((a, b) => scanItems[a].name.localeCompare(scanItems[b].name)); } +// the split of a folder happens client side, so the extension comes off the name +function extOf(path) { + const ext = (path.split("/").pop().match(/\.([^.]+)$/) || [])[1]; + return ext ? [ext.toLowerCase()] : []; +} + +function humanSize(bytes) { + if (!bytes) return ""; + const units = ["B", "KB", "MB", "GB"]; + let n = bytes, u = 0; + while (n >= 1024 && u < units.length - 1) { n /= 1024; u++; } + return `${n.toFixed(n < 10 && u > 0 ? 1 : 0).replace(".", ",")} ${units[u]}`; +} + +// a Calibre export ships one book in a dozen formats, so the row has to say +// which one it is before anything can be picked +function formatTag(item) { + const formats = item.formats || []; + if (!formats.length) return ""; + const size = humanSize(item.size); + return `${esc(formats.join(", "))}${size ? " · " + size : ""}`; +} + +function formatOptions() { + const all = [...new Set(scanItems.flatMap((it) => it.formats || []))].sort(); + const current = $("#import-filter-format").value; + $("#import-filter-format").innerHTML = + '' + + all.map((f) => ``).join(""); +} + function scoreCell(item) { if (!item.suggested_request_id) return '—'; const cls = item.score >= 80 ? "score-hi" : item.score >= 50 ? "score-mid" : "score-lo"; @@ -757,6 +791,7 @@ function renderImportTable() { ${esc(item.name)}${item.is_dir ? " 📁" : ""} + ${formatTag(item)} ${item.parts ? `${item.parts.length} Teile · ${item.files.length} Dateien` : ""} ${!item.parts && item.maybe_separate ? `${item.files.length} Titel?` @@ -876,6 +911,7 @@ $("#import-prev").addEventListener("click", () => { importPage--; renderImportTa $("#import-next").addEventListener("click", () => { importPage++; renderImportTable(); }); $("#import-filter-text").addEventListener("input", () => { importPage = 0; renderImportTable(); }); $("#import-filter-mode").addEventListener("change", () => { importPage = 0; renderImportTable(); }); +$("#import-filter-format").addEventListener("change", () => { importPage = 0; renderImportTable(); }); $("#import-sort").addEventListener("change", () => { importPage = 0; renderImportTable(); }); $("#import-select-suggested").addEventListener("click", () => { @@ -1053,6 +1089,8 @@ $("#import-merge").addEventListener("click", () => { media_type: chosen[0].media_type, is_dir: true, files: chosen.flatMap((it) => it.files), + formats: [...new Set(chosen.flatMap((it) => it.formats || []))].sort(), + size: chosen.reduce((n, it) => n + (it.size || 0), 0), checked: true, request_id: chosen.find((it) => it.request_id)?.request_id ?? null, suggested_request_id: chosen.find((it) => it.suggested_request_id)?.suggested_request_id ?? null, @@ -1079,6 +1117,7 @@ function splitItem(i) { media_type: item.media_type, is_dir: false, files: [f], + formats: extOf(f), checked: false, request_id: null, suggested_request_id: null, diff --git a/static/index.html b/static/index.html index 97fb100..9eef9ca 100644 --- a/static/index.html +++ b/static/index.html @@ -157,6 +157,9 @@ +