- test_ean_scan.py: interactive EAN barcode scanning test via scanner server
- test_backcover_scan.py: comprehensive backcover metadata extraction test
- docs/Grundsaetzliche_Audio_Koventionen_und_Methoden_Jellyfin.md: notes on
audio conventions and naming methods for Jellyfin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Re-apply: idempotent apply using track number prefix as stable anchor;
album.json never touched; optional --rename-dir flag for dir renames
- Cleanup: auto-remove abcde.* temp dirs after ripping + manual command
- gen_json: reverse-engineer album.json from file tree using fixed naming
convention; audio tags take priority over filenames for all fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Describes the restructured workflow where metadata gathering (TOC, CDDB,
MB, Vision-LLM) happens before ripping starts, so the user can review and
edit album.json before committing to the long rip — not after.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Concrete plan for Option B: new merger.py module with field-by-field
priority merging, duration_ms/disc_id model extensions, cover strategy,
and track-matching logic for sources with differing track counts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shorter, cleaner filenames consistent with Jellyfin conventions.
Updated all references in source, tests, and documentation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use GNU readline (arrow keys, backspace, history, Ctrl-A/E) for all user
prompts via input(). Replace the shared reader thread with select()-based
non-blocking polling in _input_or_scan() — eliminates dangling threads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ripper: replace per-call stdin daemon threads with a shared module-level
reader (_stdin_queue + _read_line), preventing orphan threads from stealing
stdin input after photo uploads; all 8 input() calls in interactive_rip
now use _read_line()
- ripper: _stream_abcde return type annotation fixed (2-tuple → 3-tuple)
- ripper: disc retry rejection now breaks gracefully instead of raising
unhandled RuntimeError that crashed the program
- ripper: int() on disc number input wrapped in try/except
- cddb: multi-line DTITLE/TTITLE values are now concatenated instead of
only keeping the last line (per CDDB/xmcd protocol spec)
- cli: removed unreachable dead code block in apply command
- scanner_server: upload form auto-resets after 3s for repeated uploads
- tests: _scanner_patches() updated to mock _read_line alongside
_input_or_scan (225 tests passing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove CLAUDE.md from .gitignore and add it to version control
so project instructions are shared across all Claude sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BEDIENUNGSANLEITUNG: new Variante E section with usage examples,
updated flowchart to include option E
- README: Schnellstart shows --from-photo as Variante A2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- scan: new --from-photo <img> option extracts EAN via Vision-LLM,
then falls through to existing MusicBrainz barcode lookup
- ripper: MB disc loop now retries the same disc on rip failure instead
of printing "Bitte Album neu starten"; user decline raises RuntimeError
- .gitignore: suppress temp/ directory
- tests: 4 new tests for scan --from-photo (225 total)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If Cover Art Archive has no backcover, the scanner server (already running
since EAN prompt) is now also monitored at each disc insert prompt via
_input_or_scan. On photo upload, Vision-LLM starts in the background and
the photo is saved as backcover.jpg after ripping. Phone photo takes
priority over any CAA cover.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New features:
- EAN/Barcode can now be entered by typing or by photographing the CD sleeve;
Vision-LLM (extract_barcode_from_image) reads the barcode from the photo
- Scanner server (port 8765) starts at the beginning of every album loop,
serving both EAN barcode scanning and back cover upload via QR code
- Vision-LLM analyses back cover in background thread while ripping;
priority: Vision-LLM > MusicBrainz > CDDB
- _find_abcde_mbid reads MBID from abcde temp dirs for CAA cover download
even when the CD barcode is not linked in MusicBrainz
- Concrete copy-paste apply commands shown after each album in 'Next steps'
- _sanitize_name: whitelist approach (removes brackets and punctuation)
- qrcode added as dependency for terminal QR code display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- README: mention EAN-first auto-rip in quickstart
- BEDIENUNGSANLEITUNG: rewrite workflow diagram and interactive example
for EAN-first flow (auto-rip on MB hit, fallback on miss), document
genre field in album.json, update multi-disc and tips sections
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EAN is now asked before the album name. On MusicBrainz hit, the ripper
enters an auto-rip flow (no album name prompt, no CDDB confirm, disc
count from MB data). On miss/empty EAN, the previous fallback flow
(album name → CDDB confirm) is preserved.
GnuDB responses now parse DYEAR and DGENRE fields into a new CddbResult
NamedTuple. Album model gains an optional genre field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Same fix as ripper: when track.artist and album.artist are both empty,
the filename now ends with just the title instead of '_-_.flac'.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
album.json was written to a separately computed album_root that could
differ from the actual disc_dir parent (e.g. when CDDB returned a
different album name). Now album.json is always saved in disc_dir.parent
where the audio files actually reside. Also adopts CDDB album name when
the user accepted the default name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Regular (non-compilation) tracks had an empty artist producing
trailing '_-_.flac'. Now artist suffix is only appended when non-empty.
Also added single quote to _sanitize_name's removed characters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Classical titles like 'Sonate: I. Largo - Allegro' were incorrectly split
at the movement-separator dash, producing wrong artist/title pairs.
Now only ' / ' (CDDB compilation standard) is treated as artist-title
separator; ' - ' is always part of the title.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#EXTINF:0,Title was missing the artist, causing VLC and other players
to show only the track title without the performer. Standard M3U
extended format is '#EXTINF:<duration>,<Artist> - <Title>'.
Falls back to album.artist when no per-track artist is set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- organizer: skip move when src == dst (in-place re-apply produces no
log spam); log only filename instead of full path
- cover: demote 'Kein Front-Cover gefunden' from WARNING to DEBUG —
copy_covers is not the final authority on cover availability
- cli apply: emit user-visible hint only when cover is truly absent
after all fallbacks (including parent-dir search for CD subdirs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When applying in-place with input_dir=CD1, cover files naturally live
in the album root (parent). Fall back to searching the parent when
album_dir matches the CD\d+ pattern and no cover is found in-place.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_rename_album_dir_inplace now returns the updated input_dir path
(e.g. .../Golden_Oldies_Vol_11/CD1 instead of .../Album1/CD1).
apply uses the return value so the final 'Fertig!' message and any
subsequent operations reference the correct, post-rename path.
Also fix CDDB header album name extraction (regex search between
---- markers instead of stripping leading/trailing dashes).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The header line can have a prefix before the dashes, e.g.:
"#1 (Musicbrainz): ---- Artist / Album ----"
Use regex search for content between ---- markers instead of
stripping leading/trailing dashes from the full line.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- interactive_rip: after CDDB lookup, show album name + tracklist and ask
'Treffer korrekt? (j/n)' before renaming files; rip_disc gains rename=False
option for deferred renaming
- interactive_rip: CD number prompt now shows disc_counter as default
instead of always showing [1]
- _rip_with_abcde: when CDDB fails and cd-discid is not installed, print
a visible hint with install command instead of silently doing nothing
- _stream_abcde: extract album name from CDDB header line (---- DTITLE ----)
and return it as part of the result tuple
- _rename_files: early return when output directory does not exist
- check command (cli.py): already present from previous session
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MusicBrainz always returns HTTP 200; an empty result set is definitive.
Retrying would never yield a different outcome.
- lookup_by_barcode(): retries parameter removed, random import removed
- Removed 3 retry-related tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- _parse_cddb_lines now handles both 'Artist - Title' and 'Artist / Title'
(slash separator used by abcde for compilation albums like Various Artists)
- _stream_abcde collects grab-progress lines (track N: Artist / Title)
as a fallback TrackInfo source when no CDDB lines are found
- New _parse_grab_tracks() splits grab titles on ' / ' into artist+title
- 5 new tests (TestParseCddbLines.test_compilation_slash_separator,
TestParseGrabTracks.*)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New module cddb.py: direct GnuDB/FreeDB HTTP lookup using CDDB protocol,
with same retry+random-delay logic as MusicBrainz barcode lookup
- get_discid() reads disc fingerprint via cd-discid before ripping
- If abcde returns no CDDB track data, lookup_by_discid() queries GnuDB
directly (up to 3 retries, 2-6 s random pause between attempts)
- TrackInfo moved from ripper.py to models.py to break circular import
(cddb.py and ripper.py both use TrackInfo)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Up to 3 retries with 2–6 s random wait between attempts, as MusicBrainz
occasionally returns no results on the first try. retries parameter is
configurable (default: 3).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After all file operations (rename, tag, cover, playlist), apply now
renames the album root directory to match album.json metadata:
- input_dir = CD1/CD2 etc.: parent directory is renamed automatically
e.g. Kärntner_Doppelsextett/ → Du_Berührst_Mi_20_Jahre_Kärntner_Doppelsextett/
- input_dir = album root: a hint with the mv command is printed instead
(avoids renaming an actively used path)
- Existing directory with target name: warning, no rename
Also: _sanitize_filename() in organizer.py made public (sanitize_filename),
used consistently across organizer, playlist and cli modules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Catch ValueError (barcode not found) and httpx.HTTPError (network error)
in _scan_to_album and print a user-friendly message with hint instead of
a raw Python traceback. Also remove unused `call` import in test_ripper.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- README: Schnellstart shows --barcode as fastest option
- Bedienungsanleitung:
- Workflow diagram updated (EAN path, Varianten A-D)
- Interactive rip example shows EAN prompt with MusicBrainz output
- New Variante D: scan --barcode (no image, no OCR, no local LLM)
- Variante C: corrected default model to qwen3-vl:235b-cloud
- Tipps: barcode as first/fastest option, updated CDDB fallback hints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New module musicbrainz.py: lookup_by_barcode() via EAN-13/UPC-12,
two-step API (barcode search → release detail with recordings),
respects 1 req/s rate limit with User-Agent header
- cli.py: scan command gets --barcode option as highest-priority mode
(no images needed); _scan_to_album() dispatches to MusicBrainz first
- ripper.py: interactive_rip() prompts for optional EAN after album name;
MusicBrainz data (incl. year) takes priority over CDDB for album.json;
album_root.mkdir() added so JSON can be written even when MB changes dir
- tests: test_musicbrainz.py (16 tests), test_ripper.py +6 barcode tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wenn das LLM keinen Albumtitel erkennt (z.B. nur Ensemblename auf
dem Backcover), gibt es "album": null zurück. Statt mit
ValidationError abzubrechen, wird null jetzt in "" konvertiert.
Der Nutzer kann den leeren Titel in album.json manuell ergänzen.
Geändert:
- Album.album: str = "" (statt str ohne Default)
- field_validator mode="before", None → ""
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- tagger.py: embed_cover() unterstützt jetzt .opus (Vorbis-Comment
METADATA_BLOCK_PICTURE) und .m4a (MP4Cover); imports ergänzt
- test_tagger.py: 2 neue Tests für Opus/M4A; minimale Audio-Fixtures
als base64-Konstanten (176 B Opus, 856 B M4A)
- test_cover.py: TestPrepareCover (5 Tests) und TestCopyCovers (6 Tests)
für prepare_cover() und copy_covers()
- test_ocr.py: 13 Tests für run_ocr(), _detect_and_fix_rotation()
und ocr_images(); Tesseract via subprocess.run gemockt
144 Tests, 0 Fehler
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- tests/test_tagger.py: 20 Tests für tag_file, tag_album,
_scale_cover_for_embed, embed_cover (FLAC + MP3), embed_album_cover
- tests/test_cli.py: 14 Tests für apply (in-place, disc-mismatch,
dry-run, playlist, multi-disc), check und scan (via Mock)
- tagger.py: embed_cover für MP3 fängt ID3NoHeaderError ab und
erstellt einen neuen ID3-Tag wenn keiner vorhanden ist
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ruff: Import-Sortierung, unused imports, Zeilenlängen behoben
- cli.py: _check_disc_counts_or_exit() extrahiert; auch process-Befehl
prüft jetzt Disc-Anzahlen vor dem Umbenennen
- .forgejo/workflows/ci.yml: ruff + pytest auf push/PR gegen main
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Neue Hilfsfunktion _scale_cover_for_embed() skaliert das Coverbild
mit Pillow auf max. 500px (EMBED_COVER_MAX_SIZE) und kodiert es
als JPEG quality=85 in-memory. embed_cover() liest nicht mehr die
rohen Bytes der Originaldatei, sondern nutzt das skalierte Bild.
Ergebnis: eingebettete Cover ~40–100 KB statt 200–500 KB des
1200px-Originals, auf HiDPI-Displays noch scharf erkennbar.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
cover: find_cover() sucht frontcover.jpg/.png und backcover.jpg/.png;
copy_covers() speichert als frontcover.jpg / backcover.jpg
tagger: embed_album_cover() bettet Frontcover in alle Tracks ein
cli: apply und process rufen embed_album_cover() nach copy_covers() auf
tests: TestFindCover mit 7 Tests (jpg, png, Symlink, Priorität, Negativ)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
organizer: Separator vor Titel angleichen (war: 01_Titel_-_K., neu: 01_-_Titel_-_K.)
playlist: Glob-Pattern und Fallback auf neues Schema angepasst
tests: Assertions aktualisiert
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
ripper: nach erfolgreichem CDDB-Rip album.json im Album-Verzeichnis
speichern (Artist, Titel, alle Discs mit Track-Künstlern) — Workflow-
Lücke zwischen rip und apply geschlossen.
llm_parser, vision_llm: Prompts erklären das optionale Track-artist-
Feld; LLM setzt es nur wenn Track-Interpret vom Album-Künstler abweicht.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Track model: add optional artist field (None = fall back to album artist)
- organizer: append _-_<artist> to each filename
- tagger: use track.artist over album.artist for the 'artist' tag
- playlist: widen glob pattern to match new _-_<artist> suffix
- tests: update assertions + add test for track-artist override
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>