- Swift 99%
- Shell <1%
iPod Stamper #
A WIP


A native macOS app that reads listening history from a mounted iPod (tested with a 6th-gen iPod classic), queues teal.fm records in SwiftData, and publishes them to your PDS with MusicBrainz IDs attached. It is read-only toward the iPod.
Requirements #
- macOS 26.7 or newer
- Xcode 27 or newer
- A classic iPod (iPod shuffle, iPhone, and compressed
iTunesCDBlibraries are not supported)
Build and run #
open "IPod Stamper.xcodeproj"
Run the IPod Stamper scheme. The app is sandboxed and signed with the
development team configured in the project. For Open at login, move the
built app to /Applications first.
The core logic lives in the local Swift package:
cd IPodStamperKit
swift test
Live network tests are opt-in:
RUN_LIVE_MUSICBRAINZ=1 swift test --filter liveMusicBrainzLookup
ATPROTO_TEST_HANDLE=... ATPROTO_TEST_PASSWORD=... swift test --filter liveAtprotoRoundTrip
First run #
- Choose the iPod volume once with Add iPod. The app stores a security-scoped bookmark and asks for read-only access.
- Optionally sign in to Teal with a Bluesky handle and app password.
- Scan. The first scan is a snapshot of recent history, not a full play log.
- Review the queued plays before publishing. Auto-publish is enabled after the first reviewed publish.
The granted volume scans automatically when it connects while the app is running. Enable Open at login in Settings to keep the app available.
Where data lives #
Everything is inside the app's sandbox container:
~/Library/Containers/fm.teal.ipod-stamper/Data/Library/Application Support/iPod Stamper/
├── Stamper.store # SwiftData: devices, tracks, plays, ledger, MBID cache
├── volumes.json # security-scoped bookmarks for granted iPods
└── Snapshots/ # raw iTunesDB / Play Counts copies, 30 per kind
The atproto session is in the login Keychain (service
fm.teal.ipod-stamper.session). The app password itself is never stored.
MusicBrainz cache #
Publishing resolves IDs per play and caches the result. The MusicBrainz page can warm the cache for the whole scanned library first: it groups the library by album, looks each release up once, and writes one cache row per track. That is two requests per album instead of one request per track, and a warm cache makes publishing's MusicBrainz step local. Runs are resumable: tracks that already have a cache row are skipped, and partial progress is saved after every album.
Privacy #
The database, snapshots, and saved session contain listening history or bearer tokens. They stay in the app container and Keychain. The only outbound traffic is the atproto publish and, unless disabled, MusicBrainz lookups (artist, title, album, and duration only).
Behavior notes #
- iPods store a cumulative play count and only the latest play per track. Earlier plays get estimated timestamps.
- The first scan emits one snapshot record per played track in the window. Use backfill to include every played track.
- Play Counts is captured before the database read, with a guard against double counting when the macOS sync agent has already merged it.
- Record keys are deterministic TIDs, so retries overwrite instead of duplicating.
- The app must be running to scan. It does not install a background helper; it uses a login item and in-app volume monitoring.
- A different iPod needs one new folder grant.
Architecture #
IPod Stamper/— SwiftUI app, AppKit services, settings, menu bar extra.IPodStamperKit/— iTunesDB parser, iPod probe, listen diff, Teal record builder, atproto client, MusicBrainz client and cache warmer, publisher, snapshots, and the SwiftData store.- Tests cover parser fixtures, listen diff edge cases, record/fingerprint/TID goldens, MusicBrainz scoring, SwiftData idempotency, and an end-to-end scan of a fixture volume.