- Python 98.1%
- Shell 1.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| beetsplug/appleplaylists | ||
| ci | ||
| packaging/arch | ||
| tests | ||
| .gitignore | ||
| .python-version | ||
| AGENTS.md | ||
| pipeline.yaml | ||
| pyproject.toml | ||
| README.md | ||
| requirements-integration.txt | ||
| SPEC.md | ||
| uv.lock | ||
beets-appleplaylists
Sync configured Apple Music playlists into existing playlists in a beets library. apple-music-gateway owns Apple authentication and Apple API access; this plugin owns matching and local state; beets-playlistmanager owns local playlist persistence and rendering.
Apple Music is authoritative for playlist contents and order. Synchronization is one-way: the plugin never writes to Apple Music, creates or renames local playlists, changes playlist metadata, or writes audio-file tags.
SPEC.md is the source of truth for behavior.
Requirements
beets-playlistmanager, installed and configured alongside this plugin- a deployed
apple-music-gateway - a gateway service token
- an Apple Music session linked through the gateway web UI
The mapped playlistmanager targets must exist before synchronization.
Configuration
plugins: playlistmanager appleplaylists
playlistmanager:
database: playlist.db
appleplaylists:
apple_music:
service_url: https://apple-music-gateway.example
service_token: your-gateway-bearer-token
playlists:
- id: "p.6xZa3e0hYED62mz" # Apple library-playlist ID
name: "My Playlist" # existing local playlistmanager name
service_url and service_token are read only from YAML; there are no
environment-variable credential fallbacks. Keep the bearer token out of source
control. Playlist IDs and local names must be non-empty and unique. An empty
playlists: [] is a successful offline sync no-op.
The configured local name—not the Apple display name—selects the target. The plugin validates all selected mappings and local targets before fetching or mutating data.
Gateway setup
Deploy coop/apple-music-gateway with its Apple developer credentials, HTTPS
base URL, persistent gateway database, and SERVICE_TOKEN. Deployments are
published by the gateway repository's Concourse pipeline. Register its HTTPS
callback URL with the MusicKit Services ID, then visit the gateway web UI to
link the Apple Music session. Apple developer keys and the Apple user token
remain on the gateway and are never stored by this plugin.
The SERVICE_TOKEN value must match
appleplaylists.apple_music.service_token on the beets host.
Commands
All commands are under beet apple.
status
beet apple status
Reports gateway reachability, service-token acceptance, Apple-session presence and validity, and overall readiness. A non-ready status exits non-zero and prints the gateway URL used to link the Apple session.
list-playlists
beet apple list-playlists
Fetches every user-created Apple Music library playlist and shows its ID, display name, and whether it is managed by the current configuration. This is a read-only remote operation and fails rather than falling back to stale data.
list-tracks
beet apple list-tracks
Reads only the plugin state database and active beets library, so it works while the gateway is unavailable. It lists each Apple library track with a current managed-playlist membership once, including Apple metadata, local playlist names, associated beets items, resolution status, and ISRC conflicts.
sync [LOCAL_PLAYLIST_NAME]
beet apple sync
beet apple sync "My Playlist"
With no name, fetches and validates every configured mapping before mutation. With a name, fetches only the mapping targeting that existing local playlist. An unconfigured or missing target is an error.
The command replaces playlist contents with matched beets item IDs while
preserving Apple order and duplicate occurrences. Unmatched tracks are omitted
and reported as MISSING without failing the sync. Full syncs may prune stale
plugin membership rows after a complete remote and local view; named syncs
never prune other playlists. Track history and beets associations are retained.
Regenerate configured playlistmanager outputs separately with
beet playlist sync.
match
beet apple match
beet apple match i.apple-library-track-id 'id:123'
The no-argument form works offline from plugin state. It walks unresolved current tracks one at a time and accepts a beets query, a one-run skip, or a permanent skip. Each non-dry-run decision is committed immediately, so an interrupted run resumes from the remaining tracks.
The two-argument form requires the beets query to return exactly one item. It
replaces the track's existing association without confirmation, clears a
permanent skip, and copies the Apple ISRC into the beets isrc field only when
the local value is empty.
Dry run
Use the global flag with either mutating operation:
beet apple --dry-run sync
beet apple --dry-run sync "My Playlist"
beet apple --dry-run match
beet apple --dry-run match i.apple-library-track-id 'id:123'
Dry-run mode still validates, fetches, matches, and reports proposed changes, but it does not modify the plugin SQLite database, beets fields, or playlistmanager contents. Interactive dry-run choices are discarded.
Local state
The plugin stores appleplaylists.db beside the active beets library.db. It
contains retained Apple track metadata, resolution state, and ordered current
memberships for configured Apple playlists. Duplicate occurrences are stored by
position. Historical track rows remain after memberships disappear.
Schema changes use numbered, transactional, forward-only migrations. A newer unsupported schema fails clearly; the plugin never deletes and rebuilds the state database automatically.
Matching policy
Automatic resolution uses this precedence:
- an existing
apple_music_idassociation; - exactly one local ISRC candidate;
- exactly one exact normalized artist/title candidate within the duration tolerance when both durations are present;
- otherwise
unmatchedorambiguousfor manual resolution.
Local metadata wins. A non-empty local ISRC is never overwritten, and conflicts are reported separately from resolution state.
Security
Treat the gateway service token as a secret. The plugin never prints it. Apple developer credentials and Apple session data belong only on the gateway; the plugin stores neither.
Development and integration tests
Run the unit suite with uv sync --extra dev followed by uv run pytest -q.
The real playlistmanager integration suite uses the public PlaylistStore API
from beets-playlistmanager 0.1.0, pinned to revision
4726c63306d9da23bcc0ce0e6aada0629ba2ba97 in
requirements-integration.txt. To install it and run the complete suite:
uv sync --extra dev
uv pip install -r requirements-integration.txt
uv run pytest -q
Without that dependency, only tests/test_playlistmanager_integration.py is
skipped. Integration tests create temporary beets libraries, playlist databases,
and BEETSDIR directories; all Apple data is supplied locally without network
calls. They cover WAL snapshots, dry-run/execution parity, duplicate occurrences,
metadata preservation, transactional failure, and recovery.
Database contention is retried up to three attempts, with 50 ms and 100 ms backoff. Each attempt may also wait for the store's SQLite busy timeout (five seconds in the supported playlistmanager revision). Schema, constraint, permission, and persistent I/O failures are not retried. Errors identify the store and operation. Memberships and resolution decisions for one playlist commit together; separate stores can still complete partially, and a failed full sync leaves global pruning for the next successful run.
Arch Linux releases
pipeline.yaml builds the beets plugin as
beets-appleplaylists-VERSION-1-any.pkg.tar.zst and publishes it to the
coop Arch registry. It uses the same
arch repository group and ((coop-forgejo.token)) credential as coop/arch.
The token needs repository read access and package write access.
Install or update the pipeline in the coop team (this version of fly takes
--team after the subcommand):
fly -t ci set-pipeline --team=coop -p beets-appleplaylists-release -c pipeline.yaml --check-creds -n
fly -t ci unpause-pipeline --team=coop -p beets-appleplaylists-release
To release:
-
Update
project.versioninpyproject.toml, runuv lock, and commit and push the release source, including the packaging and CI files. -
Tag that commit with the matching stable version and push the tag:
git tag v1.2.3 git push origin v1.2.3
The Git resource matches v* tags; the job processes every detected version
serially. The build requires a stable vMAJOR.MINOR.PATCH tag matching the
Python version. Untagged commits, mismatched versions, and prerelease tags
cannot publish. Local tags only become visible to Concourse after they are
pushed. Apply pipeline configuration changes with set-pipeline before
releasing; the build scripts and PKGBUILD come from the tagged commit.
The task builds in archlinux:base-devel as an unprivileged user, runs the
unit tests, installs the resulting package, and checks beet apple --help
with an isolated BEETSDIR. The playlistmanager integration tests skip when
that separately distributed dependency is absent. Publishing runs only after
these checks pass. Uploads retry transient failures, but existing versions
are never deleted or replaced; a duplicate upload fails. Release a new version
for a correction. Forgejo manages the repository database and signatures.
On an Arch host already configured for the coop/arch pacman repository:
sudo pacman -Syu beets-appleplaylists
Install beets-playlistmanager separately and configure it as described above;
it is listed as an optional pacman dependency because it is not supplied by
this pipeline, but is required for synchronization. Apple authentication and
the gateway remain separate services. No license has been declared in this
project, so the PKGBUILD does not invent one.
To test just the package build in Concourse, use a clean local checkout with a matching release tag (the output directory is outside the checkout):
fly -t ci execute --team=coop -c ci/build-package.yaml \
-i source=. -o package=/tmp/beets-appleplaylists-package
This executes only the build task and does not upload to Forgejo.