Snapshot of the existing codebase before working through the TASKS.md backlog. Real library data (data/) and the iTunes import fixture (itunes-test-library/) are gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
2.1 KiB
Python
57 lines
2.1 KiB
Python
"""Decode iTunes `.itc` album-artwork cache files.
|
|
|
|
`.itc` files (the unencrypted kind iTunes 12 writes under
|
|
`iTunes/Album Artwork/{Cache,Download,Custom,...}/`) are a tiny atom container:
|
|
|
|
[4-byte big-endian frame size]["itch"][...]["artw"]["item"]...<image bytes>
|
|
|
|
The header carries no track/album identifier (that mapping lives only in the
|
|
binary `iTunes Library.itl`); the link to a track is the *filename* token. The
|
|
payload is a plain JPEG or PNG for the vast majority of entries. A minority use
|
|
Apple's raw `ARGb`/`PNGf` pixel formats with no magic bytes — those we can't
|
|
recover here and report as unsupported.
|
|
|
|
This module is intentionally dependency-free (no Pillow) so it can run as part of
|
|
a migration script and be unit-tested with a synthesized blob.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
_JPEG_SOI = b"\xff\xd8\xff"
|
|
_JPEG_EOI = b"\xff\xd9"
|
|
_PNG_SIG = b"\x89PNG\r\n\x1a\n"
|
|
_PNG_IEND = b"IEND\xae\x42\x60\x82"
|
|
|
|
|
|
def decode_itc(data: bytes) -> tuple[bytes, str] | None:
|
|
"""Extract the embedded image from `.itc` bytes.
|
|
|
|
Returns ``(image_bytes, mime)`` for JPEG/PNG payloads, or ``None`` when the
|
|
payload is a raw Apple bitmap (no JPEG/PNG magic) that we can't recover.
|
|
"""
|
|
soi = data.find(_JPEG_SOI)
|
|
png = data.find(_PNG_SIG)
|
|
|
|
# Prefer whichever magic appears first (a header field name could in theory
|
|
# collide, but the real image is always the earliest magic in practice).
|
|
candidates = [(soi, "jpeg"), (png, "png")]
|
|
candidates = [(pos, kind) for pos, kind in candidates if pos != -1]
|
|
if not candidates:
|
|
return None
|
|
pos, kind = min(candidates)
|
|
|
|
if kind == "jpeg":
|
|
end = data.rfind(_JPEG_EOI)
|
|
image = data[pos:end + 2] if end > pos else data[pos:]
|
|
return image, "image/jpeg"
|
|
# png
|
|
end = data.find(_PNG_IEND, pos)
|
|
image = data[pos:end + len(_PNG_IEND)] if end != -1 else data[pos:]
|
|
return image, "image/png"
|
|
|
|
|
|
def decode_itc_file(path: str | Path) -> tuple[bytes, str] | None:
|
|
"""Read an `.itc` file and decode its image (see :func:`decode_itc`)."""
|
|
return decode_itc(Path(path).read_bytes())
|