From f63e6000f9f9cc6eca309be525aeafea9e9a8cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 01:59:22 +0100 Subject: [PATCH] slim-embedded-art: the audio hash excludes a trailing ID3v1 tag mutagen updates an existing v1 tag on save - it normalised the genre byte 0xFF to 0x0F on Shamrocks and Shenanigans and the guard refused a whole album of good rewrites over one metadata byte. The v1 block is metadata, not audio; the guard now hashes strictly the frames between the v2 tag and any trailing TAG block. --- scripts/slim-embedded-art.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/scripts/slim-embedded-art.py b/scripts/slim-embedded-art.py index 1cee63e..bb8f0b2 100755 --- a/scripts/slim-embedded-art.py +++ b/scripts/slim-embedded-art.py @@ -42,13 +42,12 @@ PROCESS_OVER = 100_000 def audio_region_hash(path, offset=None): - """SHA-256 of everything after the ID3v2 tag - the audio, byte-for-byte. + """SHA-256 of the audio: after the ID3v2 tag, before any trailing ID3v1 tag. - The offset comes from mutagen's OWN parse of the tag when available. Reading the header's - syncsafe size field ourselves disagreed with mutagen on files whose tagger wrote a - non-standard size (Shamrocks and Shenanigans, 2026-08-29): mutagen preserved the audio from - where IT parsed the tag to end, our hash read from where the header claimed, and the guard - refused a perfectly good rewrite. + The v2 offset comes from mutagen's own parse. The trailing 128-byte ID3v1 block (starts + "TAG") is EXCLUDED: mutagen updates an existing v1 tag on save - measured on Shamrocks and + Shenanigans (2026-08-29), it normalised the v1 genre byte 0xFF to 0x0F (Rap) and the guard + refused a whole album of perfectly good rewrites over that one metadata byte. """ if offset is None: try: @@ -62,11 +61,23 @@ def audio_region_hash(path, offset=None): offset = 10 + ((head[6] << 21) | (head[7] << 14) | (head[8] << 7) | head[9]) if head[5] & 0x10: # v2.4 footer offset += 10 + size = os.path.getsize(path) + end = size + if size - offset >= 128: + with open(path, "rb") as f: + f.seek(size - 128) + if f.read(3) == b"TAG": + end = size - 128 with open(path, "rb") as f: f.seek(offset) h = hashlib.sha256() - while chunk := f.read(1 << 20): + remaining = end - offset + while remaining > 0: + chunk = f.read(min(1 << 20, remaining)) + if not chunk: + break h.update(chunk) + remaining -= len(chunk) return h.hexdigest()