diff --git a/docs/Grundsaetzliche_Audio_Koventionen_und_Methoden_Jellyfin.md b/docs/Grundsaetzliche_Audio_Koventionen_und_Methoden_Jellyfin.md
new file mode 100644
index 0000000..2c7ec18
--- /dev/null
+++ b/docs/Grundsaetzliche_Audio_Koventionen_und_Methoden_Jellyfin.md
@@ -0,0 +1,927 @@
+
+
+# Ich digitalisiere gerade meine große CD Sammlung. Wie kann ich die Metadaten mit den Musikdateien verbinden?
+
+Du verbindest die Infos (Titel, Album, Artist, Jahr, Cover …) direkt als **Tags** in den Dateien – je nach Format z.B. ID3 (MP3) oder Vorbis-Kommentare (FLAC/OGG).[^1_1][^1_2]
+
+## Grundprinzip
+
+- Jede Audiodatei enthält ein Tag-Set: Titel, Album, Artist, Tracknummer, Discnummer, Jahr, Genre, Album-Artist, Cover-Art etc.[^1_2][^1_1]
+- Jellyfin und andere Player lesen primär diese Tags; die Ordnerstruktur hilft vor allem bei der Album‑Gruppe und Mehrfach‑CDs.[^1_3][^1_4]
+
+
+## Wenn du ein GUI‑Tool willst
+
+- Mp3tag (läuft auch gut unter Wine) zum massenhaften Bearbeiten von MP3, FLAC, OGG, MP4 usw.[^1_5][^1_6]
+- Workflow:
+ - Album‑Ordner laden.
+ - Basisdaten (Album, Artist, Jahr, Genre, Album‑Artist) im linken Panel setzen.
+ - Titel pro Track eintragen (oder aus deiner eigenen Datenquelle reinkopieren).
+ - Cover als Bilddatei ins Cover‑Feld ziehen → wird eingebettet.[^1_5]
+
+
+## Wenn du es skripten willst (Python)
+
+- Nutze die Bibliothek **mutagen**, die MP3, FLAC, OGG, AAC usw. unterstützt und Tags verlustfrei schreibt.[^1_7][^1_8][^1_9][^1_10]
+- Beispiel für FLAC (pseudocodehaft, an deine Struktur anpassbar):
+
+```python
+from mutagen.flac import FLAC
+from mutagen.id3 import ID3, APIC
+from pathlib import Path
+
+album_dir = Path("/pfad/zum/album")
+cover_data = Path(album_dir / "cover.jpg").read_bytes()
+
+tracks = [
+ "Titel 1",
+ "Titel 2",
+ # ...
+]
+
+for i, audio_file in enumerate(sorted(album_dir.glob("*.flac")), start=1):
+ audio = FLAC(audio_file)
+ audio["title"] = tracks[i-1]
+ audio["album"] = "Albumname"
+ audio["artist"] = "Künstler"
+ audio["albumartist"] = "Künstler"
+ audio["tracknumber"] = str(i)
+ audio["date"] = "1987"
+ audio["genre"] = "Rock"
+ # Cover einmal pro Datei einbetten
+ audio.add_picture({
+ "data": cover_data,
+ "mime": "image/jpeg",
+ "type": 3, # Front-Cover
+ "desc": "Cover"
+ })
+ audio.save()
+```
+
+- Für MP3 verwendest du in mutagen die Klasse `mutagen.mp3.MP3` mit ID3‑Frames (TIT2, TALB, TPE1, TPE2, APIC etc.).[^1_9][^1_7]
+
+
+## Jellyfin‑relevante Felder
+
+- Wichtige Tags pro Track:
+ - Titel (Title)
+ - Artist (Track‑Artist)
+ - Album
+ - Album‑Artist (für Sampler z.B. „Various Artists“)
+ - Tracknummer, Discnummer (+ Gesamtzahl Discs)
+ - Jahr / Erscheinungsdatum
+ - Genre
+- Wichtige Dateien im Album‑Ordner:
+ - Eingebettetes Cover im Tag, zusätzlich oft `folder.jpg`/`cover.jpg` im Ordner.[^1_11][^1_3]
+
+
+## Ordnerstruktur + Tags
+
+Eine typische, für Jellyfin gut passende Struktur:
+
+- `Artist/Album (Jahr)/CD1/01 Titel.flac` mit passenden Tags (Album, Discnumber=1, Tracknumber=1, …).
+- Jellyfin kommt auch mit „viel in einem Ordner“ klar, solange die Tags sauber sind, aber mit Album‑Ordnern behältst du als Mensch den Überblick besser.[^1_4][^1_3]
+
+Wenn du mir sagst, ob du aktuell FLAC, MP3 oder beides benutzt und ob du lieber bei GUI oder Python bleiben willst, kann ich dir ein konkretes Tagging‑Skript oder Mp3tag‑Preset passend zu deinem Ripping‑Workflow bauen.
+[^1_12][^1_13][^1_14][^1_15]
+
+
⁂
+
+[^1_1]: https://nemp-help.gausi.de/de/Metadata.html
+
+[^1_2]: https://de.wikipedia.org/wiki/Vorbis_comment
+
+[^1_3]: https://www.reddit.com/r/jellyfin/comments/mtfg1p/jellyfin_music_metadatafolder_structure/
+
+[^1_4]: https://www.reddit.com/r/jellyfin/comments/palkuy/love_it_but_help_please_tagging_metadata_and/
+
+[^1_5]: https://www.mp3tag.de
+
+[^1_6]: https://www.mp3tag.de/en/
+
+[^1_7]: https://products.documentprocessing.com/de/metadata/python/mutagen/
+
+[^1_8]: https://products.documentprocessing.com/metadata/python/mutagen/
+
+[^1_9]: https://mutagen.readthedocs.io
+
+[^1_10]: https://pypi.org/project/mutagen/
+
+[^1_11]: https://community.mp3tag.de/t/mp3tag-fur-jellyfin/67818
+
+[^1_12]: https://www.pcwelt.de/article/1151808/musicbrainz-picard-mp3-titelinfos-automatisch-vervollstaendigen.html
+
+[^1_13]: https://www.arkthinker.com/de/edit-audio/best-music-tag-editors/
+
+[^1_14]: https://www.reddit.com/r/audiophile/comments/wozzir/auto_tagging_flac_files/
+
+[^1_15]: https://www.reddit.com/r/ffmpeg/comments/ibhu89/editing_flac_and_mp3_metadata/
+
+
+---
+
+# Welche Angaben un Meta-Daten sind sinnvoll, wenn ich Audio-CDs oder Audio-CD-Alben in Jellyfin verwalten und präsentien will. Welche Infos benötige ich, welche Infos sind nice to have und in welcher Dateistruktur speichere ich die Daten (meine digitalisierte CD-Sammlung) am besten im Jellyfin Audio-Verzeichnis?
+
+Für Jellyfin zählen vor allem sauber gesetzte Tags in den Dateien und eine halbwegs konsistente Ordnerstruktur pro Album.[^2_1][^2_2]
+
+## Pflicht-Metadaten (für Jellyfin sinnvoll / „brauche ich“)
+
+Pro Track solltest du mindestens setzen:[^2_2][^2_1]
+
+- Titel (Title)
+- Artist (Interpret dieses Tracks)
+- Album
+- Album-Artist (für normale Alben = Artist, für Sampler z.B. „Various Artists“)[^2_3][^2_1][^2_2]
+- Tracknummer
+- Discnummer und Gesamtzahl Discs (Disc number, Total discs) – wichtig für Mehrfach‑CD‑Alben.[^2_4][^2_1]
+- Jahr oder Erscheinungsdatum (Date/Year) – für die zeitliche Sortierung.[^2_5][^2_2]
+- Eingebettetes Front-Cover (plus idealerweise `folder.jpg` im Albumordner) – für die Kachelansicht.[^2_1][^2_2]
+
+Ohne diese Felder wirkt die Jellyfin‑Ansicht schnell chaotisch (Alben nicht korrekt gruppiert, falsche Sortierung, keine Cover).[^2_6][^2_4][^2_1]
+
+## Sinnvolle Zusatzinfos („nice to have“)
+
+- Genre – ermöglicht Genre‑Filter und Smart Playlists.[^2_7][^2_2]
+- Komponist (Composer) – wichtig bei Klassik, Soundtracks.[^2_7][^2_3]
+- Album-Sort, Artist-Sort (sortierbare Varianten, z.B. „Beatles, The“) – für saubere A–Z‑Listen.[^2_3]
+- Originales Erscheinungsjahr getrennt von Re-Release‑Jahr (falls du beides pflegst).[^2_5]
+- Kommentar / Tagging-Notizen (z.B. Quelle, Remaster, eigene Kennzeichnung).[^2_7]
+- Mehrere Album-Artists bei Kollaborationen (durch Trenner wie „Artist1; Artist2“ im AlbumArtist‑Feld).[^2_8][^2_3]
+
+Jellyfin nutzt diese Felder nicht immer explizit für das UI, aber sie helfen dir bei Filtern, Suchen und konsistenter Darstellung.[^2_8][^2_1][^2_3]
+
+## Ordner- und Dateistruktur (empfohlen)
+
+Jellyfin: „Ein Album = ein Ordner“, der Rest erfolgt über Tags.[^2_2][^2_1]
+
+### Top-Level
+
+- `/media/music` als Jellyfin‑Bibliothek vom Typ „Musik“.[^2_2]
+
+
+### Album-Struktur
+
+Empfohlenes Schema:[^2_1][^2_2]
+
+- `/media/music/Artist/Album (Jahr)/Trackdateien`
+
+Beispiel:[^2_2]
+
+- `/media/music/Daft Punk/Random Access Memories (2013)/01 - Give Life Back to Music.flac`
+
+Wichtiges aus der Jellyfin‑Doku:[^2_1]
+
+- Ein Ordner enthält genau ein Album (bei Mehrfach‑CDs alle Discs dieses Albums).
+- Jellyfin verlässt sich primär auf die eingebetteten Tags; Dateinamen sind zweitrangig und werden nur genutzt, wenn Tags fehlen.[^2_9][^2_1][^2_2]
+
+
+### Mehrfach-CD-Alben
+
+Zwei praktikable Varianten, beide funktionieren, solange die Tags stimmen:[^2_1][^2_2]
+
+1. Alles in einem Ordner (empfohlen von Jellyfin):
+ - `/media/music/Artist/Album (Jahr)/101 - Track.flac`
+ - `/media/music/Artist/Album (Jahr)/201 - Track.flac`
+oder
+ - `/media/music/Artist/Album (Jahr)/CD1 - 01 - Track.flac`
+ - Tags: `discnumber=1/2`, `discnumber=2/2` usw.[^2_4][^2_1]
+2. Unterordner pro Disc (auch ok):
+ - `/media/music/Artist/Album (Jahr)/CD1/01 - Track.flac`
+ - `/media/music/Artist/Album (Jahr)/CD2/01 - Track.flac`
+ - Jellyfin nutzt trotzdem die Disc‑Tags zur Gruppierung der Discs, nicht den Ordnernamen.[^2_1]
+
+## Was darf „chaotisch“ sein?
+
+- Du kannst theoretisch alle Tracks in einem großen Ordner halten; Jellyfin kann über Tags trotzdem Alben und Artists bauen.[^2_10][^2_9]
+- Praktisch hilft dir selbst eine strukturierte Form `Artist/Album` enorm, z.B. bei Backups, manueller Nacharbeit und Tagging mit externen Tools.[^2_9][^2_2]
+
+
+## Kurze Empfehlung für dein Projekt
+
+1. Ripping: immer gleich mit Tags füllen (Titel, Album, Artist, Album‑Artist, Track‑/Discnummer, Jahr, Genre).[^2_7][^2_1]
+2. Albumordner: `Artist/Album (Jahr)/`, bei Multi‑CD optional `CD1/`, `CD2/` darunter.[^2_2][^2_1]
+3. In jeden Albumordner: eingebettetes Cover in allen Dateien + `folder.jpg` (Front), optional `back.jpg`.[^2_2][^2_1]
+
+Wenn du mir ein konkretes Beispiel‑Album gibst (z.B. 3‑CD‑Sammlung, Sampler, Klassik), skizziere ich dir genau, welche Tags du setzen solltest (inkl. Beispiel‑Werten) und wie die Ordner dafür aussehen könnten.
+[^2_11][^2_12][^2_13][^2_14][^2_15]
+
+⁂
+
+[^2_1]: https://jellyfin.org/docs/general/server/media/music/
+
+[^2_2]: https://diymediaserver.com/post/how-proper-organization-helps-jellyfin-automatically-fetch-metadata-and-display-content-correctly/
+
+[^2_3]: https://www.reddit.com/r/jellyfin/comments/o6ss8x/an_approach_to_tagging_collaboration_albums_for/
+
+[^2_4]: https://github.com/jellyfin/jellyfin/issues/9359
+
+[^2_5]: https://github.com/jellyfin/jellyfin/issues/2570
+
+[^2_6]: https://www.reddit.com/r/jellyfin/comments/1p0vcev/missing_embedded_metadata_for_flac_music_library/
+
+[^2_7]: https://www.reddit.com/r/audiophile/comments/wozzir/auto_tagging_flac_files/
+
+[^2_8]: https://github.com/jellyfin/jellyfin/issues/11411
+
+[^2_9]: https://www.reddit.com/r/jellyfin/comments/l8vhzv/using_playlistbased_music_folder_structure_and/
+
+[^2_10]: https://www.reddit.com/r/jellyfin/comments/mtfg1p/jellyfin_music_metadatafolder_structure/
+
+[^2_11]: https://www.reddit.com/r/jellyfin/comments/ikp2wc/help_me_to_organize_my_music_collection_for/
+
+[^2_12]: https://www.reddit.com/r/JellyfinCommunity/comments/1k64mki/how_are_you_organizing_your_media_folders/
+
+[^2_13]: https://www.reddit.com/r/jellyfin/comments/l4lpog/correct_media_folder_structure/
+
+[^2_14]: https://www.reddit.com/r/jellyfin/comments/hfh1m1/music_metadata_without_modifying_files/
+
+[^2_15]: https://www.youtube.com/watch?v=wBc7pZQ8bR0
+
+
+---
+
+# 1. Wie sehen typische Verzeichnis-Bäume für Klassik, Pop, Volksmusik und Sonstige bei Jellyfin aus ? 2. Was muss und kann ich in die Tags der Audio-Dateien schreiben ?
+
+Für Jellyfin kannst du für alle Musikrichtungen denselben Grundaufbau nutzen; Unterschiede liegen vor allem in den Tags (z.B. Komponist bei Klassik).[^3_1][^3_2]
+
+## 1. Typische Verzeichnisbäume
+
+Jellyfin‑Vorgabe: Ein Ordner enthält genau ein Album (bzw. eine Single‑Veröffentlichung).[^3_3][^3_1]
+
+### Pop / Rock / Volksmusik / „Sonstige“
+
+Übliche Struktur:
+
+```text
+/music
+└── Pop
+ ├── Abba
+ │ ├── 1976 - Arrival
+ │ │ ├── 01 - When I Kissed the Teacher.flac
+ │ │ ├── 02 - Dancing Queen.flac
+ │ │ └── folder.jpg
+ │ └── 1979 - Voulez-Vous
+ └── Queen
+ └── 1975 - A Night at the Opera
+```
+
+Oder ohne Genre‑Ebene:
+
+```text
+/music
+└── Abba
+ └── Arrival (1976)
+ ├── 01 - When I Kissed the Teacher.flac
+ └── folder.jpg
+```
+
+Beides funktioniert, da Jellyfin Alben primär über „ein Ordner = ein Album“ und Tags erkennt.[^3_4][^3_5][^3_1]
+
+### Klassik
+
+Hier lohnt sich eine Ebene für Komponisten bzw. Sammler:[^3_2][^3_6]
+
+Variante A – nach Komponist:
+
+```text
+/music-classical
+└── Beethoven, Ludwig van
+ ├── Symphonies - Karajan (DG, 1963)
+ │ ├── CD1
+ │ │ ├── 01 - Symphony No.1 - I.flac
+ │ │ └── 02 - Symphony No.1 - II.flac
+ │ └── folder.jpg
+ └── Piano Sonatas - Kempff
+```
+
+Variante B – nach Interpret/Ensemble:
+
+```text
+/music-classical
+└── Berliner Philharmoniker
+ └── Beethoven - Symphonies (Karajan, 1963)
+ ├── CD1
+ └── folder.jpg
+```
+
+Beide Varianten sind ok; Jellyfin gruppiert letztlich nach Tags (Composer, Album, AlbumArtist).[^3_1][^3_2]
+
+### Singles / EPs / Sampler
+
+- Singles als „Mini‑Album“ im eigenen Ordner ablegen, damit Jellyfin sie nicht zu Alben zusammenwirft.[^3_3]
+- Sampler/Compilations z.B.:
+
+```text
+/music
+└── Compilations
+ └── Bravo Hits 01 (1992)
+ ├── 01 - Track.flac
+ └── folder.jpg
+```
+
+Wichtig: AlbumArtist = „Various Artists“, Artist = je Track.[^3_7][^3_2]
+
+***
+
+## 2. Pflicht‑Tags für Jellyfin
+
+Diese Felder solltest du konsequent pro Track setzen, egal ob Klassik, Pop oder Volksmusik:[^3_8][^3_1]
+
+- **Title** – Tracktitel.
+- **Album** – Albumname exakt gleich für alle Tracks des Albums.[^3_1]
+- **Artist** – Interpret des Tracks (bei Klassik meist Solist/Ensemble/Dirigent, nicht der Komponist).[^3_6][^3_2]
+- **AlbumArtist** – „Haupt‑Artist“ des Albums (Band/Interpret, bei Samplern „Various Artists“; bei Kollabos mehrere Artists mit `;` oder `/`).[^3_7][^3_1]
+- **Track number** – reine Nummer (1, 2, 3 …).
+- **Disc number** und **Total discs** – wichtig für Mehrfach‑CD‑Alben.[^3_9][^3_1]
+- **Date/Year** – Erscheinungsjahr bzw. Release‑Datum.[^3_6][^3_1]
+- Eingebettetes **Cover (Front)** in jeder Datei; zusätzlich `folder.jpg` im Albumordner.[^3_10][^3_1]
+
+Ohne diese Tags gibt es z.B. falsch gruppierte Alben, fehlende Cover oder durcheinander sortierte Tracks.[^3_11][^3_9][^3_1]
+
+***
+
+## 3. Sinnvolle Zusatz‑Tags nach Genre
+
+### Für Pop / Rock / Volksmusik
+
+- **Genre** – möglichst konsistent (z.B. „Pop“, „Rock“, „Volksmusik“, nicht 20 Varianten).[^3_12][^3_13]
+- **Artist sort / AlbumArtist sort** – für „The Beatles“ → „Beatles, The“.[^3_2]
+- **Comment** – Herkunft (CD, Vinyl‑Rip, Remaster), eigene Kennzeichnung.
+- **Composer** – für Singer‑Songwriter oder wichtige Songwriter.[^3_6]
+- **Lyrics** – optional (LRC/embedded), Jellyfin kann Texte darstellen über Plugins.[^3_8][^3_6]
+
+
+### Speziell für Klassik
+
+Jellyfin hat (über MusicBrainz‑Mapping) Felder, die für Klassik nützlich sind:[^3_2][^3_6]
+
+- **Composer** – Komponist (z.B. „Beethoven, Ludwig van“).
+- **Conductor** – Dirigent.
+- **Ensemble/Orchestra** – Orchester/Chor.
+- **Performer** – Solisten (Violin, Piano etc.).
+- **Work** – Werk (z.B. „Symphony No.5 in C minor, Op.67“).
+- **Movement** – Satz (z.B. „I. Allegro con brio“).
+
+Viele Tagger (Picard, puddletag, Mp3tag) können diese Felder schreiben; Jellyfin zeigt sie in den Detailansichten und in „People“ an.[^3_2][^3_6]
+
+### Sampler / Compilations
+
+- **AlbumArtist** = „Various Artists“ (wichtig für Listen in Jellyfin).[^3_14][^3_7]
+- **Artist** = jeweiliger Interpretenname pro Track.
+- **Genre** – „Compilation“ oder das eigentliche Genre (z.B. „Pop“), je nach deiner Sortierlogik.[^3_13][^3_2]
+
+***
+
+## 4. Optionale Metadaten / Power‑User-Felder
+
+- Mehrere **Genres** getrennt durch `;`, `/` oder `\` (abhängig vom Tool); Plugins können das splitten.[^3_13]
+- **Mood / Style / Tags** (z.B. „melancholic“, „live“, „90s“), um später Sammlungen/Smart Playlists zu bauen.[^3_12][^3_13]
+- **Producer, Writer, Lyricist** – wird in „People“ angezeigt, wenn gesetzt.[^3_6]
+- **ReplayGain**‑Tags (track/album gain) – für konsistente Lautstärke; von Jellyfin‑Clients teils unterstützt.[^3_8]
+- Lokale `.nfo`‑Dateien pro Album/Artist, wenn du sehr spezifische Beschreibungen/Bilder pflegen willst.[^3_15]
+
+***
+
+## 5. Kurzempfohlene Bäume je Kategorie
+
+Nur als Illustration – passe es an deine Realität an:
+
+```text
+/media
+├── music-pop
+│ └── Artist/Year - Album/Tracks...
+├── music-classical
+│ └── Composer/Work - Conductor (Label, Year)/CDx/Tracks...
+├── music-folk
+│ └── Region or Artist/Year - Album/Tracks...
+└── music-misc
+ └── Thema oder Artist/Album/Tracks...
+```
+
+Die eigentliche „Intelligenz“ liegt in den Tags; die Bäume dienen dir vor allem zur menschlichen Orientierung und für getrennte Jellyfin‑Bibliotheken (z.B. Pop, Klassik, Hörbücher).[^3_16][^3_14][^3_1]
+
+Wenn du magst, können wir im nächsten Schritt ein konkretes Tag‑Schema für „Beispiel‑Klassik‑Album (3 CDs)“ vs. „Pop‑Album (1 CD)“ durchdeklinieren, inkl. Beispielwerten für alle relevanten Felder.
+[^3_17][^3_18]
+
+⁂
+
+[^3_1]: https://jellyfin.org/docs/general/server/media/music/
+
+[^3_2]: https://www.reddit.com/r/jellyfin/comments/gif45f/music_metadata_good_practices_and_tips/
+
+[^3_3]: https://www.reddit.com/r/selfhosted/comments/1ayt7z9/jellyfin_music_how_do_i_get_it_to_treat_singles/
+
+[^3_4]: https://www.reddit.com/r/jellyfin/comments/l8vhzv/using_playlistbased_music_folder_structure_and/
+
+[^3_5]: https://www.reddit.com/r/jellyfin/comments/mtfg1p/jellyfin_music_metadatafolder_structure/
+
+[^3_6]: https://github.com/jellyfin/jellyfin/issues/2570
+
+[^3_7]: https://www.reddit.com/r/jellyfin/comments/o6ss8x/an_approach_to_tagging_collaboration_albums_for/
+
+[^3_8]: https://www.reddit.com/r/jellyfin/comments/1qronof/jellyfin_music_basics/
+
+[^3_9]: https://github.com/jellyfin/jellyfin/issues/9359
+
+[^3_10]: https://diymediaserver.com/post/how-proper-organization-helps-jellyfin-automatically-fetch-metadata-and-display-content-correctly/
+
+[^3_11]: https://www.reddit.com/r/jellyfin/comments/1p0vcev/missing_embedded_metadata_for_flac_music_library/
+
+[^3_12]: https://lemmy.world/post/42221496
+
+[^3_13]: https://malidrive.com.tr/blog/enhance-musictag-genre-filtering-in
+
+[^3_14]: https://www.reddit.com/r/jellyfin/comments/z05w9h/music_artists_and_the_metadata/
+
+[^3_15]: https://jellyfin.org/docs/general/server/metadata/nfo/
+
+[^3_16]: https://www.reddit.com/r/jellyfin/comments/labgr2/is_there_a_way_to_use_a_folderfilesystem_view_for/
+
+[^3_17]: https://www.reddit.com/r/jellyfin/comments/ikp2wc/help_me_to_organize_my_music_collection_for/
+
+[^3_18]: https://www.youtube.com/watch?v=wBc7pZQ8bR0
+
+
+---
+
+# ja
+
+Dann machen wir zwei konkrete Beispiele: ein 3‑CD‑Klassik‑Album und ein 1‑CD‑Pop‑Album, jeweils mit Baum + Tags.
+
+## Beispiel 1: Klassik‑Album, 3 CDs
+
+### Verzeichnisbaum
+
+```text
+/music-classical
+└── Beethoven, Ludwig van
+ └── Symphonies - Karajan (DG, 1963)
+ ├── CD1
+ │ ├── 01 - Symphony No.1 - I.flac
+ │ ├── 02 - Symphony No.1 - II.flac
+ │ └── ...
+ ├── CD2
+ │ ├── 01 - Symphony No.3 - I.flac
+ │ └── ...
+ ├── CD3
+ │ ├── 01 - Symphony No.5 - I.flac
+ │ └── ...
+ └── folder.jpg
+```
+
+
+### Tags pro Track (Muss)
+
+Für alle Tracks des Albums:
+
+- Album: `Beethoven: Symphonies (Karajan, 1963)`
+- AlbumArtist: `Herbert von Karajan; Berliner Philharmoniker` (oder nur `Herbert von Karajan`, je nach Systematik)
+- Date/Year: `1963`
+- Genre: `Classical`
+
+Pro Track:
+
+- Title: `Symphony No.5 in C minor, Op.67: I. Allegro con brio`
+- Artist: `Berliner Philharmoniker; Herbert von Karajan`
+- Track number: `1`, `2`, `3` … (je CD neu beginnend)
+- Disc number: `1`, `2` oder `3`
+- Total discs: `3`
+- Embedded Cover: front cover als Bild im Tag
+
+
+### Tags pro Track (empfohlen für Klassik)
+
+- Composer: `Beethoven, Ludwig van`
+- Conductor: `Herbert von Karajan`
+- Ensemble/Orchestra: `Berliner Philharmoniker`
+- Work: `Symphony No.5 in C minor, Op.67`
+- Movement: `I. Allegro con brio`
+- Artist sort: `Karajan, Herbert von; Berliner Philharmoniker`
+- Composer sort: `Beethoven, Ludwig van`
+
+Damit kann Jellyfin sauber nach Komponist, Dirigent, Album und ggf. Werk sortieren.[^4_1][^4_2][^4_3]
+
+***
+
+## Beispiel 2: Pop‑Album, 1 CD
+
+### Verzeichnisbaum
+
+```text
+/music-pop
+└── Queen
+ └── A Night at the Opera (1975)
+ ├── 01 - Death on Two Legs.flac
+ ├── 02 - Lazing on a Sunday Afternoon.flac
+ ├── ...
+ └── folder.jpg
+```
+
+
+### Tags pro Track (Muss)
+
+Für alle Tracks des Albums:
+
+- Album: `A Night at the Opera`
+- AlbumArtist: `Queen`
+- Date/Year: `1975`
+- Genre: `Rock`
+
+Pro Track:
+
+- Title: `Bohemian Rhapsody`
+- Artist: `Queen`
+- Track number: `11` (oder entsprechend)
+- Disc number: `1`
+- Total discs: `1`
+- Embedded Cover: front cover
+
+
+### Tags pro Track (empfohlen)
+
+- Artist sort: `Queen` (oder bei „The Beatles“ z.B. `Beatles, The`)
+- Composer: `Freddie Mercury` (optional)
+- Comment: `CD rip 2026-02, remaster 2011` o.ä.
+- Mehrere Genres: z.B. `Rock; Classic Rock` (je nach Tagger‑Trenner)
+
+Damit erscheinen in Jellyfin Artist‑Ansicht, Album‑Ansicht und Sortierung so, wie man es von „normaler“ Pop‑Sammlung erwartet.[^4_2][^4_4][^4_1]
+
+***
+
+## Kurz: Was du dir als „Schema“ merken kannst
+
+- Klassik:
+ - Ordner: nach Komponist oder Ensemble, pro Aufnahme ein Albumordner, CD‑Unterordner optional.
+ - Tags: Composer, Conductor, Ensemble, Work, Movement + Standardfelder (Album, AlbumArtist, Track/Disc, Year, Genre).
+- Pop/Volksmusik/Sonstige:
+ - Ordner: `/Genre/Artist/Year - Album/Trackdateien`.
+ - Tags: Album, AlbumArtist, Artist, Track/Disc, Year, Genre, optional Composer, Artist sort.
+
+Wenn du willst, können wir als nächstes ein JSON‑Schema/Mapping definieren (z.B. wie dein Programm die Infos intern speichert und dann in Tags + Ordnernamen ausspielt).
+
+⁂
+
+[^4_1]: https://jellyfin.org/docs/general/server/media/music/
+
+[^4_2]: https://www.reddit.com/r/jellyfin/comments/gif45f/music_metadata_good_practices_and_tips/
+
+[^4_3]: https://github.com/jellyfin/jellyfin/issues/2570
+
+[^4_4]: https://malidrive.com.tr/blog/enhance-musictag-genre-filtering-in
+
+
+---
+
+# ja
+
+Dann skizziere ich dir ein schlankes JSON‑Schema, das du als interne „Album-Definition“ nutzen kannst; daraus kannst du Ordnernamen + Tags generieren.[^5_1][^5_2]
+
+## JSON‑Schema für ein Album
+
+```json
+{
+ "album_id": "beethoven-karajan-dg-1963",
+ "category": "classical",
+ "library": "music-classical",
+
+ "album": {
+ "title": "Beethoven: Symphonies (Karajan, 1963)",
+ "sort_title": "Beethoven: Symphonies (Karajan, 1963)",
+ "year": 1963,
+ "original_year": 1963,
+ "label": "Deutsche Grammophon",
+ "genres": ["Classical"],
+ "album_artist": "Herbert von Karajan; Berliner Philharmoniker",
+ "album_artist_sort": "Karajan, Herbert von; Berliner Philharmoniker"
+ },
+
+ "classical": {
+ "composer": "Beethoven, Ludwig van",
+ "composer_sort": "Beethoven, Ludwig van",
+ "conductor": "Herbert von Karajan",
+ "ensemble": "Berliner Philharmoniker"
+ },
+
+ "discs": [
+ {
+ "disc_number": 1,
+ "disc_title": "Symphonies Nos. 1 & 2",
+ "tracks": [
+ {
+ "track_number": 1,
+ "title": "Symphony No.1 in C major, Op.21: I. Adagio molto – Allegro con brio",
+ "work": "Symphony No.1 in C major, Op.21",
+ "movement": "I. Adagio molto – Allegro con brio",
+ "artists": ["Berliner Philharmoniker", "Herbert von Karajan"],
+ "soloists": [],
+ "comment": ""
+ }
+ ]
+ }
+ ],
+
+ "artwork": {
+ "front": "cover_front.jpg",
+ "back": "cover_back.jpg",
+ "booklet": ["booklet_01.jpg", "booklet_02.jpg"]
+ }
+}
+```
+
+Pop‑Album wäre dasselbe Schema, nur ohne `classical`‑Block und ohne `work/movement`, dafür ggf. `lyrics`, `writer` etc.[^5_2]
+
+## Mapping: JSON → Ordnername
+
+Aus obigem Objekt generierst du z.B.:
+
+```text
+/media//
+ Composer oder Artist /
+ " - "/
+ "CD"/"TT - Title.ext"
+```
+
+Klassik‑Beispiel:
+
+```text
+/media/music-classical/Beethoven, Ludwig van/
+ 1963 - Beethoven: Symphonies (Karajan, 1963)/
+ CD1/01 - Symphony No.1 in C major, Op.21- I. Adagio molto – Allegro con brio.flac
+```
+
+Pop‑Beispiel:
+
+```json
+"category": "pop",
+"library": "music-pop",
+"album": { "title": "A Night at the Opera", "year": 1975, "album_artist": "Queen" }
+```
+
+→
+
+```text
+/media/music-pop/Queen/1975 - A Night at the Opera/01 - Death on Two Legs.flac
+```
+
+
+## Mapping: JSON → Tag‑Felder
+
+Pro Track kannst du in deinem Tagger (mutagen oder ähnlich) so mappen:[^5_3][^5_2]
+
+Pflichtfelder:
+
+- `TITLE` ← `track.title`
+- `ALBUM` ← `album.title`
+- `ALBUMARTIST` ← `album.album_artist`
+- `ARTIST` ← `join(track.artists)`
+- `TRACKNUMBER` ← `track.track_number`
+- `DISCNUMBER` ← `disc.disc_number`
+- `DISCTOTAL` ← `len(discs)`
+- `DATE` / `YEAR` ← `album.year`
+- `GENRE` ← `join(album.genres)`
+
+Klassik‑Zusatz:
+
+- `COMPOSER` ← `classical.composer`
+- `CONDUCTOR` ← `classical.conductor`
+- `ENSEMBLE`/`ORCHESTRA` ← `classical.ensemble`
+- `WORK` ← `track.work`
+- `MOVEMENT` ← `track.movement`
+
+Sonstige:
+
+- `COMMENT` ← `track.comment` oder globaler Kommentar
+- `ALBUMSORT` ← `album.sort_title`
+- `ALBUMARTISTSORT`← `album.album_artist_sort`
+
+Cover:
+
+- Front‑Cover aus `artwork.front` als Embedded‑Bild + als `folder.jpg` im Albumordner.[^5_4][^5_1]
+
+Wenn du möchtest, formuliere ich dir im nächsten Schritt ein kleines Python‑Pseudomodul, das genau dieses JSON einliest und daraus Ordner + FLAC‑Tags erzeugt (inkl. Hook für dein Ripping‑Ergebnis).
+
+⁂
+
+[^5_1]: https://jellyfin.org/docs/general/server/media/music/
+
+[^5_2]: https://www.reddit.com/r/jellyfin/comments/gif45f/music_metadata_good_practices_and_tips/
+
+[^5_3]: https://github.com/jellyfin/jellyfin/issues/2570
+
+[^5_4]: https://diymediaserver.com/post/how-proper-organization-helps-jellyfin-automatically-fetch-metadata-and-display-content-correctly/
+
+
+---
+
+# ja
+
+Hier ist ein kompaktes Python‑Grundgerüst, das dein JSON einliest, Verzeichnisse anlegt und FLAC‑Tags mit Mutagen setzt.[^6_1][^6_2][^6_3][^6_4]
+
+## Annahmen
+
+- Du hast pro Album eine JSON‑Datei wie im letzten Beispiel.
+- Die gerippten FLACs liegen schon in einem „Roh“-Ordner (z.B. `input_disc1/track01.flac` usw.).
+- Dein Skript baut Zielordner + Dateinamen und schreibt alle Tags.
+
+
+## Beispielmodul `album_builder.py`
+
+```python
+from pathlib import Path
+import json
+from mutagen.flac import FLAC
+
+BASE_DIR = Path("/media") # Root für deine Libraries
+
+def load_album(json_path: Path) -> dict:
+ with json_path.open("r", encoding="utf-8") as f:
+ return json.load(f)
+
+def build_album_path(album_meta: dict) -> Path:
+ lib = album_meta["library"] # z.B. "music-classical"
+ cat = album_meta["category"] # "classical" / "pop" ...
+ album = album_meta["album"]
+ classical = album_meta.get("classical", {})
+
+ if cat == "classical":
+ composer = classical.get("composer_sort") or classical.get("composer") or "Unknown"
+ base = BASE_DIR / lib / composer
+ else:
+ artist = album.get("album_artist_sort") or album.get("album_artist") or "Unknown"
+ base = BASE_DIR / lib / artist
+
+ year = album.get("year")
+ title = album["title"]
+ folder_name = f"{year} - {title}" if year else title
+ return base / folder_name
+
+def build_track_filename(disc_number: int, track_number: int, title: str, ext="flac") -> str:
+ tn = f"{track_number:02d}"
+ return f"{tn} - {title}.{ext}"
+
+def tag_flac_file(
+ file_path: Path,
+ album_meta: dict,
+ disc_meta: dict,
+ track_meta: dict,
+ total_discs: int,
+ cover_data: bytes | None = None,
+):
+ album = album_meta["album"]
+ classical = album_meta.get("classical", {})
+
+ audio = FLAC(file_path)
+
+ # Grundtags
+ audio["TITLE"] = track_meta["title"]
+ audio["ALBUM"] = album["title"]
+ audio["ALBUMARTIST"] = album["album_artist"]
+ audio["ARTIST"] = "; ".join(track_meta.get("artists") or [album["album_artist"]])
+ audio["TRACKNUMBER"] = str(track_meta["track_number"])
+ audio["DISCNUMBER"] = str(disc_meta["disc_number"])
+ audio["DISCTOTAL"] = str(total_discs)
+
+ if "year" in album:
+ audio["DATE"] = str(album["year"])
+ if album.get("genres"):
+ audio["GENRE"] = "; ".join(album["genres"])
+
+ # Klassik‑Tags
+ if album_meta.get("category") == "classical":
+ if "composer" in classical:
+ audio["COMPOSER"] = classical["composer"]
+ if "conductor" in classical:
+ audio["CONDUCTOR"] = classical["conductor"]
+ if "ensemble" in classical:
+ audio["ENSEMBLE"] = classical["ensemble"]
+ if track_meta.get("work"):
+ audio["WORK"] = track_meta["work"]
+ if track_meta.get("movement"):
+ audio["MOVEMENT"] = track_meta["movement"]
+
+ # Sortierfelder
+ if album.get("sort_title"):
+ audio["ALBUMSORT"] = album["sort_title"]
+ if album.get("album_artist_sort"):
+ audio["ALBUMARTISTSORT"] = album["album_artist_sort"]
+
+ # Kommentar
+ if track_meta.get("comment"):
+ audio["COMMENT"] = track_meta["comment"]
+
+ # Cover einbetten (einheitliches Front‑Cover pro Album)
+ if cover_data is not None:
+ from mutagen.flac import Picture
+ pic = Picture()
+ pic.data = cover_data
+ pic.type = 3 # Front Cover
+ pic.mime = "image/jpeg"
+ pic.desc = "Cover"
+ audio.clear_pictures()
+ audio.add_picture(pic)
+
+ audio.save()
+
+def process_album(json_path: Path, input_root: Path):
+ album_meta = load_album(json_path)
+ target_album_dir = build_album_path(album_meta)
+ target_album_dir.mkdir(parents=True, exist_ok=True)
+
+ # Cover in Ordner kopieren + Bytes laden
+ artwork = album_meta.get("artwork", {})
+ front_name = artwork.get("front")
+ cover_bytes = None
+ if front_name:
+ cover_src = json_path.parent / front_name
+ if cover_src.is_file():
+ cover_bytes = cover_src.read_bytes()
+ cover_dst = target_album_dir / "folder.jpg"
+ if cover_src.suffix.lower() != ".jpg":
+ cover_dst.write_bytes(cover_bytes)
+ else:
+ if cover_src.resolve() != cover_dst.resolve():
+ cover_dst.write_bytes(cover_bytes)
+
+ discs = album_meta["discs"]
+ total_discs = len(discs)
+
+ for disc in discs:
+ disc_no = disc["disc_number"]
+ disc_title = disc.get("disc_title")
+
+ # CD‑Unterordner optional:
+ if total_discs > 1:
+ disc_dir = target_album_dir / f"CD{disc_no}"
+ disc_dir.mkdir(parents=True, exist_ok=True)
+ else:
+ disc_dir = target_album_dir
+
+ # Hier musst du definieren, wo die Roh‑Files liegen:
+ # z.B. input_root//disc/trackXX.flac
+ raw_disc_dir = input_root / album_meta["album_id"] / f"disc{disc_no}"
+
+ for track in disc["tracks"]:
+ src_file = raw_disc_dir / f"track{track['track_number']:02d}.flac"
+ filename = build_track_filename(disc_no, track["track_number"], track["title"])
+ dst_file = disc_dir / filename
+
+ dst_file.parent.mkdir(parents=True, exist_ok=True)
+ dst_file.write_bytes(src_file.read_bytes())
+
+ tag_flac_file(
+ dst_file,
+ album_meta=album_meta,
+ disc_meta=disc,
+ track_meta=track,
+ total_discs=total_discs,
+ cover_data=cover_bytes,
+ )
+
+if __name__ == "__main__":
+ import sys
+ if len(sys.argv) != 3:
+ print("Usage: album_builder.py ")
+ sys.exit(1)
+ json_path = Path(sys.argv[^6_1])
+ input_root = Path(sys.argv[^6_2])
+ process_album(json_path, input_root)
+```
+
+- Mutagen‑Tag‑Zugriff ist hier im FLAC‑Stil als dict‑Interface umgesetzt, wie in der Doku beschrieben.[^6_2][^6_1]
+- Verzeichnisanlage nutzt `Path.mkdir(parents=True, exist_ok=True)`, wie in üblichen Best‑Practices für verschachtelte Ordner gezeigt.[^6_3][^6_4]
+
+Wenn du mir ein konkretes JSON für ein reales Album und den aktuellen Speicherort deiner Roh‑Files schickst, passe ich dir Pfade, Dateinamensschema und Tag‑Mapping exakt darauf an.
+[^6_10][^6_11][^6_12][^6_13][^6_14][^6_15][^6_5][^6_6][^6_7][^6_8][^6_9]
+
+⁂
+
+[^6_1]: https://mutagen.readthedocs.io/en/latest/user/gettingstarted.html
+
+[^6_2]: https://linuxconfig.org/how-to-set-change-and-delete-music-tags-with-mutagen
+
+[^6_3]: https://www.geeksforgeeks.org/python/create-a-directory-in-python/
+
+[^6_4]: https://realpython.com/working-with-files-in-python/
+
+[^6_5]: https://www.geeksforgeeks.org/python/extract-and-add-flac-audio-metadata-using-the-mutagen-module-in-python/
+
+[^6_6]: https://github.com/berdario/mutagen/blob/master/TUTORIAL
+
+[^6_7]: https://github.com/dropcreations/flactagger
+
+[^6_8]: https://github.com/jellyfin/jellyfin/issues/8570
+
+[^6_9]: https://www.youtube.com/watch?v=I2wHz6Qm4Dg
+
+[^6_10]: https://www.reddit.com/r/jellyfin/comments/1054led/how_would_i_use_embedded_album_covers/
+
+[^6_11]: https://stackoverflow.com/questions/50747484/removing-single-tag-from-audio-files-mp3-and-flac-python-mutagen
+
+[^6_12]: https://www.reddit.com/r/jellyfin/comments/oyvtu2/music_artwork_does_not_update_from_embedded_tag/
+
+[^6_13]: https://stackoverflow.com/questions/12517451/automatically-creating-directories-with-file-output
+
+[^6_14]: https://mutagen.readthedocs.io
+
+[^6_15]: https://jellyfin.org/docs/general/server/media/music/
+
diff --git a/test_backcover_scan.py b/test_backcover_scan.py
new file mode 100644
index 0000000..4b9556a
--- /dev/null
+++ b/test_backcover_scan.py
@@ -0,0 +1,308 @@
+#!/usr/bin/env python3
+"""Backcover-Scanner-Test: Foto hochladen → Vision-LLM → strukturierte CD-Daten.
+
+Extrahiert alle sichtbaren Informationen vom Backcover und ordnet sie
+intelligent den Strukturdaten einer CD zu.
+
+Starten: python3 test_backcover_scan.py
+Beenden: Strg+C
+"""
+
+from __future__ import annotations
+
+import base64
+import json
+import re
+import textwrap
+from pathlib import Path
+
+import httpx
+from pydantic import BaseModel, Field
+
+from musiksammlung.scanner_server import ScannerServer, print_qr
+
+# ---------------------------------------------------------------------------
+# Konfiguration
+# ---------------------------------------------------------------------------
+
+UPLOAD_DIR = Path("/tmp/backcover_scan_test")
+PORT = 8765
+MODEL = "qwen3-vl:235b-cloud"
+BASE_URL = "http://localhost:11434"
+TIMEOUT_PHOTO = 300.0 # Sekunden warten auf Foto-Upload
+TIMEOUT_LLM = 180.0 # Sekunden warten auf LLM-Antwort
+
+# ---------------------------------------------------------------------------
+# Datenmodell
+# ---------------------------------------------------------------------------
+
+
+class TrackInfo(BaseModel):
+ number: int
+ title: str
+ artist: str | None = None # nur bei Abweichung vom Hauptkünstler
+ duration: str | None = None # "3:45"
+
+
+class DiscInfo(BaseModel):
+ disc_number: int
+ name: str | None = None # "CD 1", "Disc A", ...
+ tracks: list[TrackInfo] = Field(default_factory=list)
+
+
+class BackcoverData(BaseModel):
+ # Kern-Metadaten
+ title: str | None = None
+ artist: str | None = None
+ year: int | None = None
+
+ # CD-spezifisch
+ ean: str | None = None # NUR Ziffern
+ catalog_number: str | None = None # Katalognummer, z.B. "435 712-2"
+ quality: str | None = None # "DDD", "ADD", "AAD", "DDA" …
+ num_discs: int | None = None
+
+ # Klassik-Felder
+ composer: str | None = None
+ conductor: str | None = None
+ orchestra: str | None = None
+ soloists: list[str] = Field(default_factory=list)
+
+ # Produktion
+ label: str | None = None # Plattenlabel / Verlag
+ producer: str | None = None
+ recording_info: str | None = None # Ort und/oder Datum
+
+ # Tracklisten
+ discs: list[DiscInfo] = Field(default_factory=list)
+
+ # Alle weiteren Infos
+ additional: dict[str, str] = Field(default_factory=dict)
+
+
+# ---------------------------------------------------------------------------
+# LLM-Prompt
+# ---------------------------------------------------------------------------
+
+BACKCOVER_PROMPT = """\
+Du siehst das Foto einer CD-Hülle (Rückseite, Booklet oder beides).
+Extrahiere ALLE sichtbaren Informationen und ordne sie intelligent den Feldern zu.
+
+KERN-METADATEN:
+- title: Albumtitel
+- artist: Hauptkünstler / Interpret (bei Samplern: "Various Artists")
+- year: Erscheinungsjahr als Zahl (null wenn nicht sichtbar)
+
+CD-SPEZIFISCH:
+- ean: EAN-13 oder UPC-12 Barcode-Ziffern (NUR Ziffern, kein Leerzeichen; null wenn kein Barcode sichtbar)
+- catalog_number: Katalognummer des Labels (z.B. "435 712-2", "7243 5 55359 2 8")
+- quality: Aufnahme-/Abmischqualität falls angegeben (z.B. "DDD", "ADD", "AAD")
+- num_discs: Anzahl der CDs in der Box (1 wenn nicht angegeben)
+
+KLASSIK-FELDER (null / leer wenn nicht zutreffend):
+- composer: Komponist(en)
+- conductor: Dirigent
+- orchestra: Orchester oder Ensemble
+- soloists: Liste der Solisten, Format ["Vorname Name (Instrument)", ...]
+
+PRODUKTION:
+- label: Plattenlabel / Verlag (z.B. "Deutsche Grammophon", "EMI Classics", "Philips")
+- producer: Produzent(en) als Text
+- recording_info: Aufnahmeort und/oder -datum als Freitext
+
+TRACKLISTE:
+- discs: Liste aller CDs
+ Jede CD hat:
+ - disc_number: Nummer der CD (1, 2, ...)
+ - name: Name der CD falls angegeben, sonst null
+ - tracks: Liste der Tracks, jeder Track hat:
+ - number: Tracknummer (Zahl)
+ - title: Titel GENAU wie gedruckt (ohne Zeitangabe)
+ - artist: Interpret NUR wenn vom Hauptkünstler abweichend, sonst null
+ - duration: Laufzeit als "M:SS" oder "H:MM:SS" falls sichtbar, sonst null
+
+SONSTIGES:
+- additional: Alle weiteren Infos als Schlüssel-Wert-Paare, z.B.:
+ {"Copyright": "© 1985 Polydor", "Tonmeister": "Max Muster", "Vertrieb": "Universal"}
+
+WICHTIGE REGELN:
+- Lies ALLE Spalten vollständig von oben nach unten (Backcoverss haben oft 2–4 Spalten)
+- EAN-Ziffern ohne Trennzeichen
+- Zeitangaben aus Titeln weglassen — nur in "duration" erfassen
+- Deutsche Umlaute und Sonderzeichen korrekt übernehmen
+- Felder auf null setzen wenn Information nicht sichtbar
+
+Antworte NUR mit dem JSON-Objekt, kein weiterer Text. /no_think"""
+
+
+# ---------------------------------------------------------------------------
+# Hilfsfunktionen
+# ---------------------------------------------------------------------------
+
+
+def query_llm(image_path: Path) -> str:
+ """Schickt das Bild ans Vision-LLM, gibt Rohausgabe zurück."""
+ b64 = base64.b64encode(image_path.read_bytes()).decode()
+ response = httpx.post(
+ f"{BASE_URL}/api/chat",
+ json={
+ "model": MODEL,
+ "messages": [
+ {"role": "user", "content": BACKCOVER_PROMPT, "images": [b64]}
+ ],
+ "stream": False,
+ },
+ timeout=TIMEOUT_LLM,
+ )
+ response.raise_for_status()
+ return response.json()["message"]["content"]
+
+
+def extract_json(raw: str) -> str:
+ """Extrahiert JSON aus LLM-Antwort (bereinigt Think-Tags, Markdown)."""
+ # Think-Tags entfernen
+ raw = re.sub(r".*?", "", raw, flags=re.DOTALL).strip()
+ # Markdown-Codeblock
+ md = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw, re.DOTALL)
+ if md:
+ return md.group(1)
+ # Äußerstes JSON-Objekt
+ obj = re.search(r"\{.*\}", raw, re.DOTALL)
+ if obj:
+ return obj.group(0)
+ raise ValueError(f"Kein JSON in Antwort: {raw[:300]!r}")
+
+
+def print_result(data: BackcoverData) -> None:
+ """Gibt das extrahierte Album strukturiert aus."""
+ W = 60
+ print(f"\n{'═'*W}")
+ print(f" BACKCOVER-SCAN ERGEBNIS")
+ print(f"{'═'*W}")
+
+ def row(label: str, value: object) -> None:
+ if value is None or value == [] or value == {}:
+ return
+ label_str = f" {label:<22}"
+ val_str = str(value)
+ wrapped = textwrap.wrap(val_str, width=W - 24)
+ print(f"{label_str}{wrapped[0] if wrapped else val_str}")
+ for line in wrapped[1:]:
+ print(f" {' '*22}{line}")
+
+ print(f"\n ── Kern-Metadaten ──────────────────────────")
+ row("Titel", data.title)
+ row("Künstler", data.artist)
+ row("Jahr", data.year)
+ row("Qualität", data.quality)
+ row("Anzahl CDs", data.num_discs)
+ row("EAN", data.ean)
+ row("Katalognummer", data.catalog_number)
+
+ if any([data.composer, data.conductor, data.orchestra, data.soloists]):
+ print(f"\n ── Klassik ─────────────────────────────────")
+ row("Komponist", data.composer)
+ row("Dirigent", data.conductor)
+ row("Orchester", data.orchestra)
+ for s in data.soloists:
+ row("Solist", s)
+
+ if any([data.label, data.producer, data.recording_info]):
+ print(f"\n ── Produktion ──────────────────────────────")
+ row("Label", data.label)
+ row("Produzent", data.producer)
+ row("Aufnahme", data.recording_info)
+
+ if data.discs:
+ print(f"\n ── Trackliste ──────────────────────────────")
+ for disc in data.discs:
+ disc_label = disc.name or f"CD {disc.disc_number}"
+ print(f"\n [{disc_label}] ({len(disc.tracks)} Tracks)")
+ for t in disc.tracks:
+ dur = f" [{t.duration}]" if t.duration else ""
+ art = f" ({t.artist})" if t.artist else ""
+ print(f" {t.number:>2}. {t.title}{art}{dur}")
+
+ if data.additional:
+ print(f"\n ── Weitere Infos ───────────────────────────")
+ for k, v in data.additional.items():
+ row(k, v)
+
+ print(f"\n{'═'*W}")
+
+
+# ---------------------------------------------------------------------------
+# Hauptprogramm
+# ---------------------------------------------------------------------------
+
+
+def run() -> None:
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
+ server = ScannerServer(port=PORT, upload_dir=UPLOAD_DIR)
+ server.start()
+
+ print(f"\nModell: {MODEL}")
+ print_qr(server.url())
+ print(f"Upload-URL: {server.url()}\n")
+ print("Tipp: CD-Hülle so fotografieren, dass Barcode UND Trackliste sichtbar sind.")
+
+ runde = 0
+ try:
+ while True:
+ runde += 1
+ print(f"\n{'─'*60}")
+ print(f"Runde {runde} — Bitte Foto hochladen (URL: {server.url()})")
+ print(f"(Formular setzt sich nach 3s automatisch zurück)")
+
+ photo = server.get_photo(timeout=TIMEOUT_PHOTO)
+ if photo is None:
+ print("Timeout — kein Foto empfangen.")
+ print("Nochmal? (j/n) ", end="", flush=True)
+ if input().strip().lower() not in ("j", "ja", "y", "yes"):
+ break
+ continue
+
+ print(f"Foto empfangen: {photo} ({photo.stat().st_size:,} Bytes)")
+ print(f"Vision-LLM analysiert ({MODEL}) …", flush=True)
+
+ try:
+ raw = query_llm(photo)
+ except Exception as exc:
+ print(f"LLM-Fehler: {exc}")
+ else:
+ print(f"\nRohantwort ({len(raw)} Zeichen):")
+ print(textwrap.indent(raw[:800], " "))
+ if len(raw) > 800:
+ print(f" … (+{len(raw)-800} weitere Zeichen)")
+
+ try:
+ json_str = extract_json(raw)
+ data_dict = json.loads(json_str)
+ data = BackcoverData.model_validate(data_dict)
+ print_result(data)
+
+ # JSON-Datei speichern
+ out = UPLOAD_DIR / f"backcover_{runde:02d}.json"
+ out.write_text(
+ json.dumps(data_dict, indent=2, ensure_ascii=False),
+ encoding="utf-8",
+ )
+ print(f"\n Gespeichert: {out}")
+
+ except (ValueError, json.JSONDecodeError) as exc:
+ print(f"\n JSON-Fehler: {exc}")
+ print(f" Rohantwort vollständig:\n{raw}")
+
+ print("\nNochmal? (j/n) ", end="", flush=True)
+ if input().strip().lower() not in ("j", "ja", "y", "yes"):
+ break
+
+ except KeyboardInterrupt:
+ print("\nAbgebrochen.")
+ finally:
+ server.stop()
+ print("Server gestoppt.")
+
+
+if __name__ == "__main__":
+ run()
diff --git a/test_ean_scan.py b/test_ean_scan.py
new file mode 100644
index 0000000..ae31cfa
--- /dev/null
+++ b/test_ean_scan.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+"""Interaktiver EAN-Scan-Test: Foto hochladen → Vision-LLM → EAN ausgeben.
+
+Starten: python3 test_ean_scan.py
+Beenden: Strg+C
+"""
+
+import base64
+import re
+import sys
+from pathlib import Path
+
+import httpx
+
+from musiksammlung.scanner_server import ScannerServer, print_qr
+from musiksammlung.vision_llm import EAN_PROMPT
+
+UPLOAD_DIR = Path("/tmp/ean_scan_test")
+PORT = 8765
+MODEL = "qwen3-vl:235b-cloud"
+BASE_URL = "http://localhost:11434"
+TIMEOUT_PHOTO = 300.0 # Sekunden warten auf Foto-Upload
+TIMEOUT_LLM = 60.0 # Sekunden warten auf LLM-Antwort
+
+
+def query_llm(image_path: Path) -> str:
+ """Schickt das Bild ans Vision-LLM und gibt die Rohausgabe zurück."""
+ b64 = base64.b64encode(image_path.read_bytes()).decode()
+ response = httpx.post(
+ f"{BASE_URL}/api/chat",
+ json={
+ "model": MODEL,
+ "messages": [{"role": "user", "content": EAN_PROMPT, "images": [b64]}],
+ "stream": False,
+ },
+ timeout=TIMEOUT_LLM,
+ )
+ response.raise_for_status()
+ return response.json()["message"]["content"]
+
+
+def extract_ean(raw: str) -> str | None:
+ """Bereinigt Rohausgabe und extrahiert Ziffernfolge."""
+ cleaned = re.sub(r".*?", "", raw, flags=re.DOTALL).strip()
+ digits = re.sub(r"\D", "", cleaned)
+ return digits if digits else None
+
+
+def run() -> None:
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
+ server = ScannerServer(port=PORT, upload_dir=UPLOAD_DIR)
+ server.start()
+
+ print(f"\nModel: {MODEL}")
+ print(f"Prompt: {EAN_PROMPT!r}\n")
+ print_qr(server.url())
+ print(f"Upload-URL: {server.url()}\n")
+
+ runde = 0
+ try:
+ while True:
+ runde += 1
+ print(f"\n{'─'*50}")
+ print(f"Runde {runde}: Bitte Foto hochladen — URL: {server.url()}")
+ print(f"(Timeout {TIMEOUT_PHOTO:.0f}s — Formular auf dem Handy lädt sich nach 3s selbst zurück)")
+ photo = server.get_photo(timeout=TIMEOUT_PHOTO)
+
+ if photo is None:
+ print("Timeout — kein Foto empfangen. Nochmal? (j/n) ", end="", flush=True)
+ if input().strip().lower() not in ("j", "ja", "y", "yes"):
+ break
+ continue
+
+ print(f"Foto empfangen: {photo} ({photo.stat().st_size} Bytes)")
+ print(f"LLM-Anfrage läuft ({MODEL}) ...")
+
+ try:
+ raw = query_llm(photo)
+ except Exception as exc:
+ print(f"LLM-Fehler: {exc}")
+ continue
+
+ print(f"\nRohantwort:\n {raw!r}")
+
+ cleaned = re.sub(r".*?", "", raw, flags=re.DOTALL).strip()
+ if cleaned != raw.strip():
+ print(f"Nach Think-Tag-Entfernung:\n {cleaned!r}")
+
+ ean = extract_ean(raw)
+ if ean:
+ print(f"\n✓ EAN erkannt: {ean} ({len(ean)} Stellen)")
+ else:
+ print("\n✗ Kein Barcode erkannt")
+
+ print("\nNochmal? (j/n) ", end="", flush=True)
+ if input().strip().lower() not in ("j", "ja", "y", "yes"):
+ break
+
+ except KeyboardInterrupt:
+ print("\nAbgebrochen.")
+ finally:
+ server.stop()
+ print("Server gestoppt.")
+
+
+if __name__ == "__main__":
+ run()