A SwiftUI Mac app that creates teal.fm wreckchords from your listen history on your iPod.
README.md

iPod Stamper #

A WIP

Dashboard screen

Stats screen

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 iTunesCDB libraries 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 #

  1. Choose the iPod volume once with Add iPod. The app stores a security-scoped bookmark and asks for read-only access.
  2. Optionally sign in to Teal with a Bluesky handle and app password.
  3. Scan. The first scan is a snapshot of recent history, not a full play log.
  4. 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.