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.
This commit is contained in:
2026-08-29 01:59:22 +01:00
parent e0e96d10d7
commit f63e6000f9
+18 -7
View File
@@ -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()