packages feed

monatone-0.4.0.0: CHANGELOG.md

# Changelog for `monatone`

## [0.4.0.0] - 2026-07-24

### Added
- **OGG/Opus tag writing**: Vorbis and Opus files can now be updated like
  every other format. The comment packet is rebuilt and repaginated,
  following pages are renumbered and their CRCs recomputed, and audio
  data is copied verbatim; the encoder's vendor string is preserved.
  Album art travels as METADATA_BLOCK_PICTURE comments (kept, set, or
  removed like the other formats). Verified against ffmpeg-generated
  files (tags readable by ffprobe, streams decode cleanly).
- **OGG/Opus duration** is computed from the final page's granule
  position (accounting for Opus pre-skip)
- **Writes are durable**: the temp file is fsynced before the atomic
  rename (unix only), so a power loss cannot leave a half-written file
- Multiple album art pictures survive updates: writers take an explicit
  art instruction (keep / set / remove), and "keep" carries all existing
  FLAC PICTURE blocks, MP3 APIC frames, and M4A covr atoms through
  verbatim
- MP3 binary frames (USLT lyrics, PRIV, GEOB, SYLT, POPM, URL frames,
  foreign UFIDs) are carried through updates verbatim; unmapped ID3v2.2
  text frames are translated to their v2.4 equivalents
- The FLAC writer preserves the original encoder's vendor string
- ID3v1/Winamp genre table: ID3v2.3 "(nn)" references and ID3v2.4 bare
  numeric genre strings resolve to genre names, and the ID3v1 genre byte
  is read as a fallback

### Changed
- **Breaking**: `writeMetadata` and the format writers take an
  `AlbumArtUpdate` (`KeepExistingArt` / `SetAlbumArt` / `RemoveAlbumArt`)
  instead of `Maybe AlbumArt`, whose `Nothing` conflated "no change"
  with "remove"
- `updateMetadata` detects the format from file content instead of the
  extension, and no longer reloads album art through the parser (whose
  failure used to silently delete artwork)
- **Breaking**: `rawTags` is now `HashMap Text [Text]` so multi-valued
  tags are represented faithfully. Scalar metadata fields (`artist`,
  `genre`, ...) expose the first value. On write, if a scalar field still
  matches the first stored value the whole list is written back; editing
  the field replaces the list.

### Fixed
- ALAC audio properties (bit depth, sample rate, channels) are read from
  the alac extension box; the extension-parsing branch was unreachable,
  so 24-bit files reported the sample-entry header's 16-bit claim, and
  the (dead) alac parser read the wrong offset for the sample rate
- MP3 UFID frames are parsed properly (owner + identifier) instead of
  being garbled by the text-frame parser; the MusicBrainz recording id
  is read from and written to UFID:http://musicbrainz.org per Picard
  convention (previously mapped to the wrong field and never matched)
- An existing ID3v1 tag is rewritten on update instead of keeping stale
  pre-update fields visible to v1-only readers
- ID3v2 header flags are now honoured: whole-tag unsynchronisation is
  reversed, the extended header is skipped (v2.3 and v2.4 layouts), and
  the v2.4 footer is accounted for when locating audio data; v2.4
  per-frame unsynchronisation and data-length indicators are handled too
- UTF-16 COMM/TXXX/USLT frames with descriptions decode correctly: the
  terminator search is now aligned to UTF-16 code units
- Xing/Info VBR headers are found in MPEG2/2.5 files (side-info offsets
  differ from MPEG1), fixing missing duration/bitrate for MPEG2 VBR
- ID3v1 fields decode as Latin-1 per spec instead of UTF-8; plain v1.0
  tags no longer lose the last 2 comment characters
- An MP3 frame sync spanning a 4096-byte scan-chunk boundary is no longer
  missed
- The MP3 writer replaces an existing tag's footer instead of leaving 10
  bytes of it before the audio
- Multi-valued tags (e.g. several ARTIST Vorbis comments) are no longer
  silently collapsed to one value
- Null-separated values in ID3v2.4 text frames are parsed as separate
  values instead of being concatenated into one string, and the MP3
  writer emits multi-valued frames null-separated per ID3v2.4 (one frame
  per ID, since duplicate frame IDs are invalid)
- Vorbis bitrate fields are read as signed, so the -1 "unset" sentinel is
  no longer reported as a ~4 Gbps bitrate
- Opus sample rate is reported as 48kHz (the decode rate) rather than the
  encoder's informational input rate
- FLAC album art loading continues past a corrupt Picture block to a
  later one instead of giving up
- Mixed-case file extensions (.Mp3, .Flac) are accepted for updates
- M4A disk atom is written with the 6-byte iTunes layout (trkn keeps its
  trailing reserved word); track/disc numbers are clamped to 16 bits
  instead of silently wrapping
- **Malformed-input hardening**: crafted or truncated files can no longer
  hang or crash the parsers and writers
  - M4A atoms with sizes smaller than their own header (including zero
    64-bit sizes) no longer cause infinite loops in the parser or writer
  - Truncated FLAC blocks and lying length fields inside Vorbis comments
    return clean parse results instead of throwing from pure code
  - An MP3 sync pattern with fewer than 4 bytes before EOF no longer
    crashes the frame-header parser; truncated Xing headers are read
    safely
  - M4A tag payload reads are clamped to the file size, so a lying atom
    size cannot trigger a huge allocation
  - FLAC writer errors out cleanly on truncated source blocks and on
    metadata blocks exceeding the 16MiB block-length limit instead of
    writing a corrupt stream
  - MP3 writer rejects content beyond ID3v2's 256MiB syncsafe size limit
    instead of silently truncating the size field
- **OGG packets are now reassembled across pages**: comment packets larger
  than one page (~64KB, i.e. any file with embedded album art) were
  previously truncated and all tags silently dropped
- Opus tags (`OpusTags` packet) are now parsed; previously Opus files
  returned no tags and the parser scanned the entire file
- OGG album art with base64 `=` padding no longer fails to decode
- OGG parser now populates `rawTags` and stops reading after the two
  metadata packets

## [0.3.0.0] - 2026-07-20

### Fixed
- **M4A writer corrupted moov-first ("fast start") files**: stco/co64 chunk
  offsets are now adjusted when the moov atom changes size, so audio stays
  playable after a tag update
- M4A writer no longer silently discards errors from the file rewrite
- **Writes are now atomic**: metadata is written to a temporary sibling file
  which is renamed over the original, so a failed write or crash can no
  longer corrupt or truncate the audio file
- **Updates no longer drop tags the writer does not map**: MusicBrainz IDs
  and AcoustID tags (MP3), track/disc totals (MP3, FLAC), release
  status/type (M4A), unmapped ID3 text/TXXX frames, Vorbis comments, and
  iTunes text/freeform atoms are all preserved across updates
- FLAC updates preserve SEEKTABLE, APPLICATION, CUESHEET and unknown
  metadata blocks instead of discarding them
- Single date field per file: `setYear` no longer produces duplicate
  TDRC frames (MP3) or DATE comments (FLAC)
- FLAC comment values containing `=` are no longer truncated
- MP3 parser now reads disc number and track/disc totals from TRCK/TPOS
- FLAC parser now populates `rawTags`; M4A parser now reads release
  status/type
- FLAC vendor string now tracks the package version automatically

### Changed
- **Breaking**: removed `writeMetadataToFile` from `Monatone.Writer`; use
  `writeMetadata`, which is now atomic and no longer needs the `.backup`
  mechanism

## [0.2.1.1] - 2025-12-10

### Fixed
- Added *.m4a to extra-source-files to include M4A test fixture in sdist tarball

## [0.2.1.0] - 2025-12-10

### Fixed
- Fixed Hackage description formatting to display properly with multiline text
- Escaped forward slashes in format names to prevent markdown italicization
- Fixed M4A round-trip test to properly skip when fixture unavailable
- Checked in test fixtures (MP3, FLAC) to support Nix builds without ffmpeg

### Changed
- Tests no longer require ffmpeg at build time (only for regenerating fixtures)

## [0.2.0.0] - 2025-12-10

### Added
- **M4A/AAC/ALAC Support**: Complete read/write support for M4A audio files
  - Full metadata parsing with iTunes-style tags (©nam, ©ART, aART, etc.)
  - Audio properties extraction (duration, sample rate, channels, bit depth)
  - AAC and Apple Lossless (ALAC) codec detection and parsing
  - Album art extraction (JPEG, PNG, BMP)
  - Track/disc number parsing with iTunes binary format
  - Complete metadata writing with atom reconstruction
  - **Freeform atom support** for MusicBrainz-style fields:
    - Record label (LABEL)
    - Catalog number (CATALOGNUMBER)
    - Barcode (BARCODE)
    - Release country (MusicBrainz Album Release Country)
    - All MusicBrainz IDs (recording, release, artist, album artist, release group, work, disc)
    - Acoustid fingerprint and ID
  - Integration with unified Writer API
  - Comprehensive test coverage

### Technical Details
- Pure Haskell MP4 atom parser with hierarchical structure support
- Efficient streaming implementation (no full file loading)
- Support for both 32-bit and 64-bit atom sizes
- Version-aware parsing (mvhd v0/v1, etc.)
- Safe file writing with temporary file approach
- Freeform (----) atom parsing and writing for custom metadata
- Zero compiler warnings

## [0.1.0.0] - Initial Release

### Added
- FLAC read/write support with Vorbis comments
- MP3 read/write support with ID3v1, ID3v2.3, and ID3v2.4 tags
- OGG/Vorbis and Opus basic support
- Album art handling
- Pure Haskell implementation