Skip to content

feat(lossless-tags): copy ID3 tags from lossy files onto lossless replacements#14

Open
deviationist wants to merge 2 commits into
mainfrom
feat/lossless-tags
Open

feat(lossless-tags): copy ID3 tags from lossy files onto lossless replacements#14
deviationist wants to merge 2 commits into
mainfrom
feat/lossless-tags

Conversation

@deviationist

Copy link
Copy Markdown
Owner

What & why

I'm replacing leftover lossy files (MP3) with lossless ones (AIFF) over time. When a fresh AIFF is dropped next to the old MP3 (same basename, same folder), the new file has poor/no tags. This adds a lossless-tags subcommand that copies the entire ID3 tag wholesale (all frames incl. APIC artwork) from the lossy file onto its lossless replacement, so curated metadata survives the swap — then the MP3 can be deleted.

Like aiff-titles, this edits the audio file on disk; it never pushes metadata via the Plex API.

How it works

  • Discovery: filesystem walk of a music root (--root / MUSIC_ROOT), pairing lossy↔lossless by basename in the same directory (utils/pairing.py).
  • Tag copy: mutagen copies the whole ID3 from MP3 → AIFF; AIFF.save() rewrites only the ID3 chunk, leaving the audio (SSND) byte-identical. The AIFF native NAME chunk is then aligned to the new title (reusing utils/aiff_chunks.rewrite_name) so Plex shows it right, or stripped with --remove-name.
  • Read-only / parity by default — prints the tags that would transfer. --write (behind the WRITE-TAGS token) backs up every lossless original first.
  • --show matched|unmatched|both lists the upgrade backlog; --delete-lossy, --refresh-plex, --ignore-case, --limit, --mirror-version.

Safety / edge cases

Only AIFF/AIFF-C targets are supported (FLAC/WAV skipped); ambiguous (>1 sibling), untagged-source, and zero-byte/broken-symlink pairings are skipped + reported. The lossy file is only deleted after a fully successful copy.

Tests

New tests/id3_tags_test.py + tests/LosslessTagCopy_test.py (incl. the real backup→copy→NAME-fix→delete write path). ruff/black/mypy clean; full suite green. Docs updated (CLAUDE.md, README, .env.example).

🤖 Generated with Claude Code

…lacements

New `lossless-tags` subcommand: walk a music root, pair each lossy file
(MP3) with a same-named lossless sibling (AIFF) in the same folder, and
copy the whole ID3 tag wholesale (all frames incl. APIC artwork) onto the
lossless file so curated metadata survives the swap. Read-only/parity by
default; --write backs up each lossless original, aligns the AIFF NAME
chunk to the new title, and optionally deletes the lossy source. --show
matched|unmatched|both lists the upgrade backlog. --refresh-plex queues a
partial Plex scan of affected dirs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wholesale ID3 copy already transfers TPE2 (albumartist) — and every
other frame — but the preview table only displayed Title/Artist/Album/Genre.
Add an AlbumArtist column so the dry-run shows it transferring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant