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:
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user