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**.
|
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.
|
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`).
|
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)
|
## Setup (Docker)
|
||||||
|
|
||||||
|
|||||||
+29
-2
@@ -785,6 +785,28 @@ $("#import-deselect-all").addEventListener("click", () => {
|
|||||||
|
|
||||||
// ---- quick request from a scanned file ----
|
// ---- quick request from a scanned file ----
|
||||||
let quickItemIndex = null;
|
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) {
|
function cleanFileName(name) {
|
||||||
return name
|
return name
|
||||||
@@ -797,8 +819,13 @@ function cleanFileName(name) {
|
|||||||
function openQuickDialog(i) {
|
function openQuickDialog(i) {
|
||||||
quickItemIndex = i;
|
quickItemIndex = i;
|
||||||
const item = scanItems[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-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 =
|
$("#quick-library").innerHTML =
|
||||||
libOptions(item.media_type) || "<option value=''>— keine Library —</option>";
|
libOptions(item.media_type) || "<option value=''>— keine Library —</option>";
|
||||||
$("#quick-results").innerHTML = "";
|
$("#quick-results").innerHTML = "";
|
||||||
@@ -893,7 +920,7 @@ async function pickQuickResult(r) {
|
|||||||
const req = await createRequest({
|
const req = await createRequest({
|
||||||
library_id: parseInt(libId),
|
library_id: parseInt(libId),
|
||||||
title: r.title, authors: r.authors, narrator: r.narrator || "",
|
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,
|
year: r.year, series: r.series, cover_url: r.cover_url,
|
||||||
});
|
});
|
||||||
setLastLib(scanItems[i].media_type, libId);
|
setLastLib(scanItems[i].media_type, libId);
|
||||||
|
|||||||
@@ -191,6 +191,7 @@
|
|||||||
<dialog id="quick-dialog">
|
<dialog id="quick-dialog">
|
||||||
<h3>Suchen & Anfragen</h3>
|
<h3>Suchen & Anfragen</h3>
|
||||||
<p class="muted mono" id="quick-file"></p>
|
<p class="muted mono" id="quick-file"></p>
|
||||||
|
<p class="muted" id="quick-episode"></p>
|
||||||
<form id="quick-search-form" class="bar">
|
<form id="quick-search-form" class="bar">
|
||||||
<input id="quick-q" placeholder="Titel/Serie suchen…" required />
|
<input id="quick-q" placeholder="Titel/Serie suchen…" required />
|
||||||
<button type="submit">Suchen</button>
|
<button type="submit">Suchen</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user