Compare commits
3 commits
1ca88b0d6d
...
5de6caba3a
| Author | SHA1 | Date | |
|---|---|---|---|
| 5de6caba3a | |||
| 6a7602387a | |||
| c791812755 |
1 changed files with 447 additions and 1 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Refactoring-Plan: Workflow-Phasen in ripper.py
|
# Refactoring-Plan: Workflow-Phasen in ripper.py
|
||||||
|
|
||||||
*Stand: 2026-02-19 — Entwurf zur Diskussion*
|
*Stand: 2026-02-20 — Entwurf zur Diskussion*
|
||||||
|
|
||||||
## Aktueller Zustand
|
## Aktueller Zustand
|
||||||
|
|
||||||
|
|
@ -75,3 +75,449 @@ Soll die Disc-Schleife (Phase 4) eine einheitliche Schnittstelle haben, die
|
||||||
beide Pfade bedient? Im MB-Pfad ist die Disc-Anzahl bekannt, im Fallback-Pfad
|
beide Pfade bedient? Im MB-Pfad ist die Disc-Anzahl bekannt, im Fallback-Pfad
|
||||||
offen (while-Schleife mit "Nächste CD?"). Das ist der größte strukturelle
|
offen (while-Schleife mit "Nächste CD?"). Das ist der größte strukturelle
|
||||||
Unterschied.
|
Unterschied.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5 im Detail: Intelligente Metadaten-Zusammenführung (merger.py)
|
||||||
|
|
||||||
|
### Ziel
|
||||||
|
|
||||||
|
Statt "Winner-takes-all" (bisher: Vision > MB > CDDB) eine **feldweise
|
||||||
|
Zusammenführung** aller verfügbaren Quellen. Jedes Feld wird aus der besten
|
||||||
|
verfügbaren Quelle befüllt. Zusätzlich werden bisher verworfene technische
|
||||||
|
Daten (Disc-ID, Tracklängen) persistiert.
|
||||||
|
|
||||||
|
### Neues Modul: `src/musiksammlung/merger.py`
|
||||||
|
|
||||||
|
Einzige öffentliche Funktion:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def merge_album(
|
||||||
|
mb_album: Album | None, # Phase 1: MusicBrainz-Treffer
|
||||||
|
cddb_results: list[CddbResult], # Phase 4: ein CddbResult pro Disc
|
||||||
|
vision_album: Album | None, # Phase 3: Vision-LLM-Ergebnis
|
||||||
|
disc_ids: list[str], # Phase 4: cd-discid-Output pro Disc
|
||||||
|
user_album_name: str | None, # Phase 1/4: manuell eingetippter Name
|
||||||
|
) -> Album:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datenmodell-Erweiterungen (models.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Track(BaseModel):
|
||||||
|
track_number: int
|
||||||
|
title: str
|
||||||
|
artist: str | None = None
|
||||||
|
duration_ms: int | None = None # NEU: aus MB (ms) oder TOC berechnet
|
||||||
|
|
||||||
|
class Disc(BaseModel):
|
||||||
|
disc_number: int
|
||||||
|
name: str | None = None
|
||||||
|
tracks: list[Track]
|
||||||
|
disc_id: str | None = None # NEU: 8-Hex CDDB Disc-ID aus cd-discid
|
||||||
|
```
|
||||||
|
|
||||||
|
`Album.genre` ist bereits vorhanden, wird aber bisher nie befüllt — das
|
||||||
|
wird mit dieser Änderung behoben.
|
||||||
|
|
||||||
|
### Feldweise Priorisierung
|
||||||
|
|
||||||
|
| Feld | Priorität | Begründung |
|
||||||
|
|------|-----------|------------|
|
||||||
|
| `album.artist` | MB > Vision > CDDB | MB normalisiert (z.B. "Bach, J.S.") |
|
||||||
|
| `album.album` | MB > Vision > CDDB | MB autoritativ |
|
||||||
|
| `album.year` | MB > CDDB > Vision | MB hat exaktes Release-Datum |
|
||||||
|
| `album.genre` | CDDB (einzige Quelle) | MB und Vision liefern nie Genre |
|
||||||
|
| `disc.name` | Vision (einzige Quelle) | MB und CDDB kennen kein disc.name |
|
||||||
|
| `disc.disc_id` | cd-discid (einzige Quelle) | Physischer Fingerprint der CD |
|
||||||
|
| `track.title` | Vision > MB > CDDB | Vision bereinigt (ohne Zeitangaben) |
|
||||||
|
| `track.artist` | MB > CDDB > Vision | MB normalisiert Track-Artists |
|
||||||
|
| `track.duration_ms` | MB > TOC-berechnet | MB in ms; TOC aus Sektor-Offsets |
|
||||||
|
|
||||||
|
### Tracklängen: zwei Quellen
|
||||||
|
|
||||||
|
**Quelle A — MusicBrainz** (`track.length` in Millisekunden):
|
||||||
|
- Zuverlässigste Quelle, von MB-Redakteuren gepflegt
|
||||||
|
- Wird in `_parse_release()` (musicbrainz.py) bereits ignoriert → dort ergänzen
|
||||||
|
|
||||||
|
**Quelle B — TOC aus cd-discid** (Sektor-Offsets):
|
||||||
|
- cd-discid liefert: `discid ntrks offset_1 offset_2 ... total_sectors`
|
||||||
|
- Laufzeit Track n = `(offset_{n+1} - offset_n) / 75` Sekunden
|
||||||
|
- Für letzten Track: `(total_sectors - offset_n) / 75`
|
||||||
|
- Genau, weil direkt vom physischen Medium gemessen
|
||||||
|
- Erfordert: `disc_ids`-Liste als strukturierte Offsets parsen (heute nur als
|
||||||
|
Rohstring gespeichert)
|
||||||
|
|
||||||
|
### Track-Matching zwischen Quellen
|
||||||
|
|
||||||
|
Wenn Quellen unterschiedlich viele Tracks liefern (z.B. Vision-LLM liest
|
||||||
|
12 Tracks, MB hat 13 wegen Hidden Track):
|
||||||
|
|
||||||
|
```
|
||||||
|
Merge-Strategie: track_number als Primary Key
|
||||||
|
for track_number in union(alle Quellen):
|
||||||
|
title = erste verfügbare Quelle nach Priorität
|
||||||
|
artist = erste verfügbare Quelle nach Priorität
|
||||||
|
duration_ms = MB[track_number] oder TOC[track_number] oder None
|
||||||
|
```
|
||||||
|
|
||||||
|
Tracks die nur in einer Quelle existieren, werden übernommen (kein
|
||||||
|
stilles Verwerfen).
|
||||||
|
|
||||||
|
### Cover-Strategie (Phase 2 — Änderung gegenüber heute)
|
||||||
|
|
||||||
|
```
|
||||||
|
front.jpg / back.jpg Priorität:
|
||||||
|
1. CAA (Cover Art Archive via MBID) ← bevorzugt, standardisiert
|
||||||
|
2. Handy-Foto ← nur wenn CAA nicht verfügbar
|
||||||
|
|
||||||
|
Handy-Fotos back.jpg:
|
||||||
|
- Primärer Zweck: Text-Extraktion durch Vision-LLM (ephemer)
|
||||||
|
- Als back.jpg speichern: nur wenn kein CAA-Cover vorhanden
|
||||||
|
- Qualitätsproblem: Handy-Foto ≠ standardisiertes Cover-Artwork
|
||||||
|
|
||||||
|
Handy-Fotos front.jpg:
|
||||||
|
- Nie nötig, wenn CAA verfügbar
|
||||||
|
- Scanner-Upload für front.jpg entfällt bei MB-Treffern komplett
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zu erfassende Daten (Änderungen in anderen Modulen)
|
||||||
|
|
||||||
|
| Modul | Änderung | Zweck |
|
||||||
|
|-------|----------|-------|
|
||||||
|
| `musicbrainz.py` | `track.get("length")` → `Track.duration_ms` | Laufzeiten aus MB |
|
||||||
|
| `cddb.py` | `disc_id` und Offsets aus discid_line parsen | TOC-Laufzeiten berechnen |
|
||||||
|
| `ripper.py` | `discid_line` pro Disc speichern (heute verworfen) | Weitergabe an merger |
|
||||||
|
| `cddb.py` | EXTT-Felder parsen (oft leer, aber opportunistisch) | Laufzeiten als Fallback |
|
||||||
|
|
||||||
|
### Testbarkeit
|
||||||
|
|
||||||
|
`merger.py` hat keine Abhängigkeit zu Hardware, Netzwerk oder Subprocess.
|
||||||
|
Input: Python-Objekte. Output: `Album`. Vollständig unit-testbar mit
|
||||||
|
Fixture-Daten — kein Mocking erforderlich.
|
||||||
|
|
||||||
|
Testvorgaben:
|
||||||
|
- MB-Album + CDDB-Ergebnis + Vision-Album → korrekte Feldauswahl
|
||||||
|
- Genre immer aus CDDB übernommen
|
||||||
|
- Tracklängen aus MB, falls vorhanden
|
||||||
|
- Track-Matching bei unterschiedlicher Track-Anzahl
|
||||||
|
- disc_id korrekt pro Disc zugeordnet
|
||||||
|
- Fehlende Quellen (None) robust behandelt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallelisierung: Informationsbeschaffung vor dem Rippen
|
||||||
|
|
||||||
|
### Kernidee
|
||||||
|
|
||||||
|
Die CD muss für zwei Operationen physisch im Laufwerk liegen, aber diese
|
||||||
|
brauchen sich nicht zu überlappen:
|
||||||
|
|
||||||
|
```
|
||||||
|
Operation A: TOC lesen (cd-discid) ~2 Sekunden
|
||||||
|
Operation B: Audio rippen (cdparanoia) 20–60 Minuten pro Disc
|
||||||
|
```
|
||||||
|
|
||||||
|
Heute sind beide in einem monolithischen Durchlauf verschmolzen. Das
|
||||||
|
Umordnen kostet nichts — die CD darf ruhig zweimal im Laufwerk liegen.
|
||||||
|
|
||||||
|
### Neuer Ablauf (Drei Phasen A / B / C)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase A: Informationsbeschaffung (~30s – 5 min) │
|
||||||
|
│ │
|
||||||
|
│ 1. CD einlegen (einmalig oder pro Disc bei Multi-Disc) │
|
||||||
|
│ 2. cd-discid → TOC: disc_id, Offsets, Laufzeiten │
|
||||||
|
│ 3. CDDB-Lookup → artist, album, year, genre, tracks │
|
||||||
|
│ 4. EAN-Scan → MusicBrainz → Album + MBID │
|
||||||
|
│ 5. CAA-Download → front.jpg, back.jpg │
|
||||||
|
│ 6. back.jpg → Vision-LLM (läuft parallel, ~1–3 min) │
|
||||||
|
│ 7. merge_album() → album.json (preliminary) │
|
||||||
|
│ 8. "album.json bereit — bitte prüfen." │
|
||||||
|
│ 9. User editiert album.json (optional, zweites │
|
||||||
|
│ Terminal oder nach Prompt) │
|
||||||
|
│ 10. "Rippen starten? [Enter]" │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase B: Audio-Ripping (~20–60 min pro Disc) │
|
||||||
|
│ │
|
||||||
|
│ abcde läuft im Vordergrund (Terminal zeigt Progress) │
|
||||||
|
│ Während des Rippens im zweiten Terminal: │
|
||||||
|
│ - album.json editieren (jederzeit möglich) │
|
||||||
|
│ - Vision-LLM-Ergebnis (falls noch ausstehend) │
|
||||||
|
│ wird nach Abschluss nachgemergt │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Phase C: Apply (~30s) │
|
||||||
|
│ │
|
||||||
|
│ Liest finales album.json (evtl. editiert während B) │
|
||||||
|
│ organize → tag → embed → playlist │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Was der Hauptgewinn ist
|
||||||
|
|
||||||
|
```
|
||||||
|
Heute: Metadaten ── Rippen (60 min) ── album.json sehen ── editieren
|
||||||
|
Neu: Metadaten ── album.json sehen ── [editieren] ── Rippen (60 min)
|
||||||
|
└── Vision-LLM läuft parallel zum Rippen
|
||||||
|
```
|
||||||
|
|
||||||
|
Der User sieht die Metadaten **vor** dem langen Warten. Editing erfolgt
|
||||||
|
mit vollständiger Information, nicht blind im Nachhinein.
|
||||||
|
|
||||||
|
### Warum kein Hintergrund-Ripping
|
||||||
|
|
||||||
|
Hintergrund-Ripping (subprocess mit PID-Datei) ist technisch machbar,
|
||||||
|
erhöht aber die Komplexität (PID-Management, Fehlerbehandlung, Status-
|
||||||
|
Tracking) ohne entscheidenden Mehrwert: Der User kann während des
|
||||||
|
Vordergrund-Rippings jederzeit ein zweites Terminal öffnen und album.json
|
||||||
|
dort editieren. `$EDITOR album.json` reicht.
|
||||||
|
|
||||||
|
### Abcde: keine Änderung nötig
|
||||||
|
|
||||||
|
abcde hat Aktions-Flags (`-a cddb,read,encode,tag`), die eine Trennung
|
||||||
|
erlauben würden. Da wir CDDB und TOC aber bereits **vor** abcde über
|
||||||
|
eigene Module abfragen (`lookup_by_discid`, `cd-discid`), kann abcde
|
||||||
|
unverändert für Phase B genutzt werden — nur mit dem Wissen, dass seine
|
||||||
|
CDDB-Ausgabe bereits in album.json konsolidiert ist.
|
||||||
|
|
||||||
|
### Multi-Disc-Verhalten
|
||||||
|
|
||||||
|
```
|
||||||
|
Disc 1: Phase A (~2 min) → "Rippen starten?" → Phase B Disc 1 (~45 min)
|
||||||
|
Disc 2: Phase A (~2 min) → "Rippen starten?" → Phase B Disc 2 (~45 min)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Jede Disc wird sequenziell behandelt (ein Laufwerk). Phase A aller Discs
|
||||||
|
könnte theoretisch vorab gebündelt werden (alle Discs kurz einlegen, TOC
|
||||||
|
lesen), ist aber als optionale Optimierung einzustufen.
|
||||||
|
|
||||||
|
### Änderungsbedarf in ripper.py
|
||||||
|
|
||||||
|
Die heutige `interactive_rip`-Funktion wird aufgeteilt in:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def gather_metadata(disc_num, config, scanner) -> DiscMetadata:
|
||||||
|
"""Phase A: TOC, CDDB, MB, Vision-LLM — kein Rippen."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def rip_disc(disc_num, config) -> Path:
|
||||||
|
"""Phase B: abcde — kein Netzwerk, kein LLM."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def interactive_rip(config):
|
||||||
|
"""Orchestriert: für jede Disc gather_metadata → confirm → rip_disc."""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
`DiscMetadata` ist ein internes Dataclass/NamedTuple das alle Rohdaten
|
||||||
|
einer Disc bis zum Merge trägt:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class DiscMetadata:
|
||||||
|
disc_number: int
|
||||||
|
discid_line: str # Rohstring aus cd-discid
|
||||||
|
cddb_result: CddbResult | None
|
||||||
|
mb_album: Album | None # nur bei erster Disc sinnvoll
|
||||||
|
mb_mbid: str | None
|
||||||
|
vision_album: Album | None # Ergebnis des Background-Threads
|
||||||
|
uploaded_photo: Path | None
|
||||||
|
```
|
||||||
|
|
||||||
|
Nach dem letzten `rip_disc`-Aufruf ruft `interactive_rip` einmalig
|
||||||
|
`merge_album()` auf und schreibt album.json.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wiederholbares Apply (Re-Apply in-place)
|
||||||
|
|
||||||
|
### Ziel
|
||||||
|
|
||||||
|
`apply` soll beliebig oft wiederholbar sein — auch nachträglich im
|
||||||
|
Jellyfin-Zielverzeichnis. Der User editiert album.json und ruft apply
|
||||||
|
erneut auf; Dateinamen, Tags und Playlist werden aktualisiert.
|
||||||
|
|
||||||
|
### Invariante: album.json bleibt immer erhalten
|
||||||
|
|
||||||
|
`apply` berührt album.json nie. Sie ist die einzige persistente
|
||||||
|
Wahrheitsquelle für alle Metadaten. Selbst wenn Tags oder Dateinamen
|
||||||
|
abweichen, definiert album.json den Sollzustand.
|
||||||
|
|
||||||
|
### Stabilität durch Tracknummer-Präfix
|
||||||
|
|
||||||
|
Der stabile Anker beim Re-Apply ist die führende Tracknummer im
|
||||||
|
Dateinamen:
|
||||||
|
|
||||||
|
```
|
||||||
|
01_-_Alter_Titel.flac → nach Edit → 01_-_Neuer_Titel.flac
|
||||||
|
```
|
||||||
|
|
||||||
|
`discover_audio_files()` identifiziert Tracks bereits nach dem
|
||||||
|
numerischen Präfix (unabhängig vom Titelanteil). Re-Apply ist damit
|
||||||
|
robust gegenüber beliebigen Titeländerungen.
|
||||||
|
|
||||||
|
### Was apply bei jedem Aufruf tut (idempotent)
|
||||||
|
|
||||||
|
1. Audiodateien per Tracknummer-Präfix identifizieren
|
||||||
|
2. Umbenennen gemäß aktuellem album.json (in-place)
|
||||||
|
3. Alle Audio-Tags neu schreiben (überschreiben)
|
||||||
|
4. Cover neu einbetten (überschreiben)
|
||||||
|
5. Playlist neu generieren (überschreiben, gleicher Dateiname)
|
||||||
|
6. album.json unangetastet lassen
|
||||||
|
|
||||||
|
### Verzeichnisumbenennung bei Albumname-Änderung
|
||||||
|
|
||||||
|
Wenn sich `album` oder `year` in album.json ändern, ändert sich der
|
||||||
|
korrekte Verzeichnisname. Im Jellyfin-Ordner ist das heikel (Jellyfin
|
||||||
|
erkennt das Album ggf. als neu).
|
||||||
|
|
||||||
|
Vorschlag: Verzeichnisumbenennung nur mit explizitem Flag:
|
||||||
|
```
|
||||||
|
musiksammlung apply --in-place --rename-dir <album_dir>
|
||||||
|
```
|
||||||
|
Ohne Flag: Dateien und Tags werden aktualisiert, Verzeichnisname bleibt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cleanup nach dem Ripping
|
||||||
|
|
||||||
|
abcde hinterlässt Arbeitsverzeichnisse unterhalb der CDn-Ordner:
|
||||||
|
|
||||||
|
```
|
||||||
|
Album/
|
||||||
|
CD1/
|
||||||
|
abcde.XXXXX/ ← Temp-Verzeichnis von abcde
|
||||||
|
track01.wav ← evtl. noch vorhandene WAV-Zwischendateien
|
||||||
|
status
|
||||||
|
mbid.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wann aufräumen
|
||||||
|
|
||||||
|
**Automatisch am Ende von Phase B** (nach erfolgreichem Ripping):
|
||||||
|
|
||||||
|
```python
|
||||||
|
for abcde_dir in cd_dir.glob("abcde.*"):
|
||||||
|
shutil.rmtree(abcde_dir)
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ als expliziter Befehl für manuelles Aufräumen:
|
||||||
|
```
|
||||||
|
musiksammlung cleanup <album_dir>
|
||||||
|
```
|
||||||
|
|
||||||
|
Beide Varianten sollten implementiert werden: automatisch als Default,
|
||||||
|
manuell als Fallback wenn Phase B mit Fehler abgebrochen wurde.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Neuer CLI-Befehl: gen_json
|
||||||
|
|
||||||
|
### Zweck
|
||||||
|
|
||||||
|
Rekonstruiert album.json aus dem vorhandenen Dateibaum, wenn die Datei
|
||||||
|
versehentlich gelöscht wurde. Kann auch für Alben genutzt werden, die
|
||||||
|
mit anderen Tools gerippt wurden.
|
||||||
|
|
||||||
|
```
|
||||||
|
musiksammlung gen_json <album_dir>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Namenskonvention (verbindlich festgelegt)
|
||||||
|
|
||||||
|
**Album-Verzeichnis:**
|
||||||
|
```
|
||||||
|
Sanitized_Artist_-_Sanitized_Album_Title_(YYYY)/
|
||||||
|
Sanitized_Album_Title_(YYYY)/ ← wenn Artist "Various Artists"
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiele:
|
||||||
|
```
|
||||||
|
Pink_Floyd_-_The_Wall_(1979)/
|
||||||
|
Bach_-_Brandenburg_Concertos_(1967)/
|
||||||
|
Various_Artists_-_Now_Thats_What_I_Call_Music_(2001)/
|
||||||
|
```
|
||||||
|
|
||||||
|
**CDn-Unterverzeichnis** (bei Multi-Disc):
|
||||||
|
```
|
||||||
|
CD1/ CD2/ ...
|
||||||
|
```
|
||||||
|
Bei Single-Disc: Audiodateien liegen direkt im Album-Verzeichnis oder
|
||||||
|
in `CD1/` — beides wird akzeptiert.
|
||||||
|
|
||||||
|
**Track-Dateiname:**
|
||||||
|
```
|
||||||
|
NN_-_Sanitized_Track_Title.flac
|
||||||
|
NN_-_Sanitized_Track_Title_-_Sanitized_Track_Artist.flac
|
||||||
|
```
|
||||||
|
|
||||||
|
`NN` ist zweistellig mit führender Null (`01`, `02`, ..., `99`).
|
||||||
|
`_-_` (Unterstrich-Bindestrich-Unterstrich) ist der strukturelle
|
||||||
|
Trenner — kein einfacher `-` innerhalb eines Feldes ist `_-_`.
|
||||||
|
|
||||||
|
### gen_json Algorithmus
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Verzeichnisname parsen:
|
||||||
|
"Pink_Floyd_-_The_Wall_(1979)" →
|
||||||
|
artist = "Pink Floyd" (vor erstem _-_)
|
||||||
|
album = "The Wall" (zwischen _-_ und _(YYYY))
|
||||||
|
year = 1979 (aus _(YYYY))
|
||||||
|
|
||||||
|
2. CDn-Unterverzeichnisse → disc_number (Zahl aus "CD1", "CD2" etc.)
|
||||||
|
Keine CDn-Dirs → Single-Disc, disc_number = 1
|
||||||
|
|
||||||
|
3. Pro Disc: Audiodateien nach Tracknummer-Präfix sortieren:
|
||||||
|
"01_-_In_the_Flesh.flac" → track_number=1, title="In the Flesh"
|
||||||
|
"02_-_The_Thin_Ice_-_Roger_Waters.flac"
|
||||||
|
→ track_number=2, title="The Thin Ice",
|
||||||
|
artist="Roger Waters"
|
||||||
|
|
||||||
|
4. Audio-Tags lesen (mutagen) — höhere Priorität als Dateiname:
|
||||||
|
- title, artist: aus Tags übernehmen wenn vorhanden
|
||||||
|
(Tags enthalten Original-Strings ohne Sanitizing)
|
||||||
|
- duration_ms: aus Audiodatei (exakt, besser als MB oder TOC)
|
||||||
|
- genre: nur aus Tags verfügbar, nirgends im Dateinamen kodiert
|
||||||
|
|
||||||
|
5. album.json schreiben (fehlende Felder als null)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daten-Rangfolge in gen_json
|
||||||
|
|
||||||
|
| Feld | Quelle 1 (bevorzugt) | Quelle 2 (Fallback) |
|
||||||
|
|------|---------------------|---------------------|
|
||||||
|
| title | Audio-Tag | Dateiname (desanitized) |
|
||||||
|
| artist | Audio-Tag | Dateiname / Verzeichnis |
|
||||||
|
| album | Audio-Tag | Verzeichnisname |
|
||||||
|
| year | Audio-Tag | Verzeichnisname _(YYYY) |
|
||||||
|
| genre | Audio-Tag | — (bleibt null) |
|
||||||
|
| duration_ms | Audiodatei (mutagen) | — |
|
||||||
|
| disc_number | CDn-Verzeichnis | — |
|
||||||
|
| track_number | Dateiname-Präfix | Audio-Tag |
|
||||||
|
|
||||||
|
### Was gen_json nicht rekonstruieren kann
|
||||||
|
|
||||||
|
- `disc.disc_id` (physischer TOC-Fingerprint — nicht in Dateien)
|
||||||
|
- `disc.name` (z.B. "Live in Berlin" — nicht im Dateinamen kodiert)
|
||||||
|
- `genre` wenn Audio-Tags fehlen
|
||||||
|
|
||||||
|
### Klassik: Komponist vs. Interpret
|
||||||
|
|
||||||
|
Beide Felder werden in den Audio-Tags gespeichert (COMPOSER-Tag bei
|
||||||
|
FLAC/MP3). `gen_json` liest COMPOSER aus den Tags und kann Komponist
|
||||||
|
von Interpret trennen — ohne spezielle Namenskonvention im Dateinamen.
|
||||||
|
|
||||||
|
Der Dateinamen-Titel enthält bei Klassik typischerweise Werk + Satz:
|
||||||
|
```
|
||||||
|
01_-_Symphony_No_5_Op_67_I_Allegro_con_brio.flac
|
||||||
|
```
|
||||||
|
Komponist steht im Verzeichnisnamen (`Bach_-_...`) und im COMPOSER-Tag.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue