add(schema): {Narrators} placeholder, {Narrator} now names the first speaker

This commit is contained in:
Steppenstreuner
2026-08-29 07:25:49 +02:00
parent d71ea54c6e
commit 7daa0d61a4
4 changed files with 42 additions and 4 deletions
+10 -1
View File
@@ -36,7 +36,16 @@ Jede Library kann eine **Sprache** tragen (`Deutsch`/`Englisch`, Default: egal).
## Namensschemata
Pro Library konfigurierbar (Tab *Libraries*), Platzhalter: `{Author}` `{Authors}` `{Title}` `{Year}` `{Series}` `{Volume}`.
Pro Library konfigurierbar (Tab *Libraries*), Platzhalter: `{Author}` `{Authors}` `{Narrator}` `{Narrators}` `{Title}` `{Year}` `{Series}` `{Volume}`.
`{Author}` und `{Narrator}` nennen jeweils nur den **ersten** Namen, `{Authors}` und `{Narrators}` die vollständige Liste — bei einer Full-Cast-Produktion mit 20 Sprechern willst du Letzteres nicht im Ordnernamen. Fehlt ein Wert, verschwindet er samt Klammern und Trennzeichen: `{Series}/{Volume} - {Title} ({Narrator})` wird für einen Einzeltitel ohne Sprecherangabe schlicht zu `Titel`.
Liegt dieselbe Ausgabe in mehreren Fassungen vor (Harry Potter mit Stephen Fry *und* als Full-Cast), gehört `{Narrator}` in den Ordnernamen — sonst kollidieren beide im selben Verzeichnis:
```
Harry Potter/01 - Philosopher's Stone (Stephen Fry)/
Harry Potter/01 - Philosopher's Stone (Hugh Laurie)/
```
Defaults:
- Ebook: `{Author}/{Title} ({Year})` / `{Author} - {Title}`
+3 -2
View File
@@ -288,8 +288,9 @@
</button>
</form>
<p class="muted">
Platzhalter: {Author} {Authors} {Narrator} {Title} {Year} {Series}
{Volume}
Platzhalter: {Author} {Authors} {Narrator} {Narrators} {Title} {Year}
{Series} {Volume} — {Author}/{Narrator} nennen den ersten Namen,
{Authors}/{Narrators} alle. Leere Werte fallen samt Trenner weg.
</p>
</section>
</main>
+23
View File
@@ -59,3 +59,26 @@ def test_missing_values_do_not_leave_dangling_separators():
series="Tony Ballard", volume=1, year=2020, narrator="")
assert render_template("{Series}/{Volume} - {Title}", numbered) == \
"Tony Ballard/01 - Die Höllenbrut"
def test_narrator_uses_the_first_name_only():
"""Two editions of one book differ by narrator; a full cast must not blow up
the folder name."""
fry = SimpleNamespace(title="Philosopher's Stone", authors="J.K. Rowling",
series="Harry Potter", volume=1, year=None,
narrator="Stephen Fry")
cast = SimpleNamespace(title="Philosopher's Stone", authors="J.K. Rowling",
series="Harry Potter", volume=1, year=None,
narrator="Hugh Laurie, Matthew Macfadyen, Riz Ahmed, Michelle Gomez")
template = "{Series}/{Volume} - {Title} ({Narrator})"
assert render_template(template, fry) == "Harry Potter/01 - Philosopher's Stone (Stephen Fry)"
assert render_template(template, cast) == "Harry Potter/01 - Philosopher's Stone (Hugh Laurie)"
# the full list stays available
assert render_template("{Narrators}", cast) == \
"Hugh Laurie, Matthew Macfadyen, Riz Ahmed, Michelle Gomez"
def test_narrator_missing_leaves_no_empty_brackets():
req = SimpleNamespace(title="Ein Buch", authors="Jemand", series="", volume=None,
year=None, narrator="")
assert render_template("{Series}/{Volume} - {Title} ({Narrator})", req) == "Ein Buch"
+6 -1
View File
@@ -14,12 +14,17 @@ def sanitize(part: str, max_len: int = 120) -> str:
def render_template(template: str, request) -> str:
"""Render a naming template against a BookRequest. Each path segment is sanitized."""
author = (request.authors or "").split(",")[0].strip() or "Unknown Author"
narrators = getattr(request, "narrator", "") or ""
# like {Author}: the first name only, so a full-cast production does not turn
# into a 200 character folder name
narrator = narrators.split(",")[0].strip()
values = {
"Author": author,
"Authors": request.authors or author,
"Title": request.title or "Unknown Title",
"Year": str(request.year) if request.year else "",
"Narrator": getattr(request, "narrator", "") or "",
"Narrator": narrator,
"Narrators": narrators,
"Series": request.series or "",
"Volume": f"{request.volume:02d}" if request.volume is not None else "",
}