add(ui/ux): keep episode numbers out of the audible query
This commit is contained in:
@@ -7,7 +7,7 @@ Sonarr/Radarr-Style Request- & Import-Manager für **Ebooks**, **Comics/Mangas**
|
||||
1. **Anfragen**: Im Web-UI per Titel/Autor/ISBN suchen (Ebooks: Open Library · Audiobooks: Audible · Manga: AniList) oder manuell anlegen. Beim Request wählst du die **Ziel-Library** (z.B. deine 8 Bookorbit-Libraries oder die 3 Audiobookshelf-Libraries english/adult/kids). Der Titel erscheint als **Missing**.
|
||||
Gehört ein Audiobook-Treffer zu einer Serie, legt **„Ganze Serie…"** alle Folgen auf einmal an (Folgennummer + echter Titel von Audible, optional auf einen Folgenbereich eingegrenzt). Audible hat keine Serien-Abfrage — wordarr sammelt die Folgen über mehrere Suchläufe ein; sollte eine fehlen, zeigt der Dialog die Lücke an.
|
||||
2. **Download-Ordner**: wordarr überwacht keinen Downloader aktiv — du legst Dateien selbst in den Download-Ordner (`/mnt/downloads`).
|
||||
3. **Import missing**: Im Tab *Import* den Ordner scannen. wordarr schlägt per Fuzzy-Matching Datei→Request-Zuordnungen vor (bei Serien zählt die Folgennummer im Ordnernamen plus der Serienname im Pfad, `???` und `Fragezeichen` gelten als dasselbe); du bestätigst oder korrigierst. Beim Import wird die Datei nach dem Namensschema der Library **umbenannt und verschoben**. Ordner mit mehreren Audio-Dateien werden als ein Audiobook behandelt (`Titel - Part 01.mp3`, …). Auch Ordner, die in Disc-Unterordner (`CD`, `CD1`, `CD 2`, `Disc 3`, `Teil 1`) aufgeteilt sind, gelten als **ein** Audiobook — der Ordnername darüber liefert den Titel, die Parts werden über alle Discs hinweg durchnummeriert. Unterordner ohne Audio (`Cover`, Scans, Booklets) werden dabei ignoriert.
|
||||
3. **Import missing**: Im Tab *Import* den Ordner scannen. wordarr schlägt per Fuzzy-Matching Datei→Request-Zuordnungen vor (bei Serien zählt die Folgennummer im Ordnernamen plus der Serienname im Pfad, `???` und `Fragezeichen` gelten als dasselbe); du bestätigst oder korrigierst. Über 🔍 lässt sich zu einem Ordner direkt eine Anfrage suchen — eine führende Folgennummer (`017 - Titel`) wird dabei aus der Suchanfrage genommen, weil Audible sonst schlechter trifft, und stattdessen als Band/Folge übernommen. Beim Import wird die Datei nach dem Namensschema der Library **umbenannt und verschoben**. Ordner mit mehreren Audio-Dateien werden als ein Audiobook behandelt (`Titel - Part 01.mp3`, …). Auch Ordner, die in Disc-Unterordner (`CD`, `CD1`, `CD 2`, `Disc 3`, `Teil 1`) aufgeteilt sind, gelten als **ein** Audiobook — der Ordnername darüber liefert den Titel, die Parts werden über alle Discs hinweg durchnummeriert. Unterordner ohne Audio (`Cover`, Scans, Booklets) werden dabei ignoriert.
|
||||
|
||||
## Setup (Docker)
|
||||
|
||||
|
||||
+29
-2
@@ -785,6 +785,28 @@ $("#import-deselect-all").addEventListener("click", () => {
|
||||
|
||||
// ---- quick request from a scanned file ----
|
||||
let quickItemIndex = null;
|
||||
let quickEpisode = null;
|
||||
|
||||
// "017 - Titel", "Folge 17: Titel", "[003] Titel" -> episode number + rest.
|
||||
// A bare number without a separator ("100 Stunden") or without a leading zero is
|
||||
// left alone: there it is part of the title, not a numbering.
|
||||
const EPISODE_PATTERNS = [
|
||||
/^\s*(?:folge|teil|band|nr\.?)\s*(\d{1,4})\s*(?:[-–—._:]+\s*|\s+)/i, // "Folge 124: Titel"
|
||||
/^\s*[\[(](\d{1,4})[\])]\s*(?:[-–—._:]+\s*|\s+)/, // "[003] Titel"
|
||||
/^\s*(0\d{1,3})\s*(?:[-–—._:]+\s*|\s+)/, // "001 Titel"
|
||||
/^\s*(\d{1,3})\s*[-–—._:]+\s*/, // "17 - Titel"
|
||||
];
|
||||
|
||||
function splitEpisodeNumber(name) {
|
||||
for (const re of EPISODE_PATTERNS) {
|
||||
const m = name.match(re);
|
||||
if (!m) continue;
|
||||
const rest = name.slice(m[0].length).trim();
|
||||
if (!rest) break; // the number *is* the name
|
||||
return { number: parseInt(m[1], 10), rest };
|
||||
}
|
||||
return { number: null, rest: name };
|
||||
}
|
||||
|
||||
function cleanFileName(name) {
|
||||
return name
|
||||
@@ -797,8 +819,13 @@ function cleanFileName(name) {
|
||||
function openQuickDialog(i) {
|
||||
quickItemIndex = i;
|
||||
const item = scanItems[i];
|
||||
// the episode number wrecks the Audible keyword search, so it is kept out of
|
||||
// the query and used as the volume instead
|
||||
const { number, rest } = splitEpisodeNumber(item.name);
|
||||
quickEpisode = number;
|
||||
$("#quick-file").textContent = (item.rel_dir ? item.rel_dir + "/" : "") + item.name;
|
||||
$("#quick-q").value = cleanFileName(item.name);
|
||||
$("#quick-episode").textContent = number != null ? `Folge ${number} — nicht mitgesucht` : "";
|
||||
$("#quick-q").value = cleanFileName(rest);
|
||||
$("#quick-library").innerHTML =
|
||||
libOptions(item.media_type) || "<option value=''>— keine Library —</option>";
|
||||
$("#quick-results").innerHTML = "";
|
||||
@@ -893,7 +920,7 @@ async function pickQuickResult(r) {
|
||||
const req = await createRequest({
|
||||
library_id: parseInt(libId),
|
||||
title: r.title, authors: r.authors, narrator: r.narrator || "",
|
||||
external_id: r.external_id, volume: r.volume ?? null,
|
||||
external_id: r.external_id, volume: r.volume ?? quickEpisode,
|
||||
year: r.year, series: r.series, cover_url: r.cover_url,
|
||||
});
|
||||
setLastLib(scanItems[i].media_type, libId);
|
||||
|
||||
@@ -191,6 +191,7 @@
|
||||
<dialog id="quick-dialog">
|
||||
<h3>Suchen & Anfragen</h3>
|
||||
<p class="muted mono" id="quick-file"></p>
|
||||
<p class="muted" id="quick-episode"></p>
|
||||
<form id="quick-search-form" class="bar">
|
||||
<input id="quick-q" placeholder="Titel/Serie suchen…" required />
|
||||
<button type="submit">Suchen</button>
|
||||
|
||||
Reference in New Issue
Block a user