Files
lintunes/lintunes/itc.py
T
travandClaude Opus 4.8 f6d35fa594 Import existing LinTunes project
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>
2026-06-26 21:12:01 -04:00

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())