listenbrainz-scrobbler
A background service that submits Apple Music playback on macOS to ListenBrainz. It uses Music.app's scripting interface and a user LaunchAgent. No App Store purchase or developer signing certificate is required.
Setup
Install Python 3.13 or newer and uv, then run from this checkout:
uv sync --locked
uv run lb-scrobbler install --dry-run
uv run lb-scrobbler status
Allow the Python process to control Music when macOS asks. Terminal's Automation permission applies to a foreground probe, so the LaunchAgent may need a separate grant. The first query waits up to a minute for consent. If access is denied, enable it in System Settings → Privacy & Security → Automation, then reinstall the agent. status shows the observation age, current track, observed playback seconds, and any read error. Confirm the observations stay fresh and the position advances while playing.
After the dry run works, quit other scrobblers to avoid duplicate submissions. Save your ListenBrainz token in the login Keychain:
uv run lb-scrobbler auth
uv run lb-scrobbler install
For an existing SmashTunes setup, uv run lb-scrobbler auth --from-smashtunes explicitly transfers its stored ListenBrainz token into this service's Keychain entry after validating it. Neither command prints the token. Do not put tokens in command arguments, project files, or Git.
install starts the service immediately and at future logins. It records the current virtual environment's Python path, so keep the checkout and .venv in place. Rerun install after moving the project or recreating the environment. Updating the underlying Python executable may require fresh macOS Automation or Keychain consent.
Operation
uv run lb-scrobbler probe # Read Music without submitting
uv run lb-scrobbler status # Last observation and submission queue
uv run lb-scrobbler stop # Stop until reinstalled or the next login
uv run lb-scrobbler uninstall # Remove the launch agent
uv run lb-scrobbler retry # Retry retained failures after fixing their cause
uv run lb-scrobbler run --dry-run runs in the foreground. Stop the LaunchAgent first, since only one process may own the playback tracker.
State and logs live in ~/Library/Application Support/listenbrainz-scrobbler/. The service stores metadata and its SQLite queue there with user-only permissions. The submission token is held separately in login Keychain under xyz.hakula.listenbrainz-scrobbler. Uninstalling the agent preserves both the queue and Keychain entry.
A persistent scripting process samples playback every two seconds. It submits after observing half a track or four minutes of playback, whichever comes first, following ListenBrainz's submission rule. Pauses, seeks, and observation gaps longer than ten seconds do not contribute listening time. Starting the service halfway through a song does not credit playback it did not observe.
Qualified listens and playback checkpoints are committed together. A separate worker sends the queue and retains failures with the original listen timestamp. Network and server errors use delayed retries. Invalid payloads remain blocked for inspection through status and manual retry. After replacing an expired token, reinstall the agent so the worker loads it. Delivery is at least once: a crash after server acceptance but before local acknowledgement can resend the same payload.
Dry-run mode uses a separate queue that is never sent. There is no historical backfill or iPhone library synchronization, and no optional playing-now submissions. The service captures playback observed on this Mac.
Limits
Polling cannot distinguish a natural repeat from manually seeking from the very end to the very beginning. It also cannot identify different recordings with identical title, artist, and album metadata. Track-start timestamps are estimated from the observed position, so seeking before the first observation can skew that estimate. Missing metadata is skipped. These limits need to be considered when comparing the history with another scrobbler.
Development
The source is split into playback tracking, persistence, Music observation, API submission, credentials, and service lifecycle modules. Runtime dependencies are httpx and macOS keyring. No Scroblebler source is copied into this project.
nix develop
pre-commit run --all-files
uv run ruff check .
uv run ruff format --check .
uv run mypy src/lb_scrobbler
uv run pytest -q
uv build
nix flake check
Tests cover playback thresholds, interruptions, repeats, restart persistence, and HTTP retry behavior without contacting ListenBrainz. Validate real streaming playback and Automation access separately on macOS before enabling a new installation.
Nix / Home Manager
nix build builds the macOS package with Python 3.13. The flake exports homeManagerModules.default so nix-darwin configurations using Home Manager can manage the service declaratively. Add this repository as a flake input, then import the module in your Home Manager configuration:
imports = [ inputs.listenbrainz-scrobbler.homeManagerModules.default ];
services.listenbrainz-scrobbler.enable = true;
Home Manager replaces the manual installer's LaunchAgent under the same label while preserving the queue and Keychain token. Activate Home Manager, then check lb-scrobbler status to confirm the agent loaded and observations are fresh. The package path changes, so macOS may request Automation or Keychain access again. Use lb-scrobbler auth if no token is stored yet.
The LaunchAgent starts at user login after a reboot, when Music and the login Keychain are available. Home Manager owns its lifecycle after migration. Disable the module and reactivate Home Manager to remove it. The existing Keychain entry keeps credentials outside the Nix store.
The development shell generates pre-commit configuration from the flake and installs the Git hooks. CI checks hooks and locked Python dependencies on Linux and macOS, and builds the package and a Home Manager configuration on macOS. The Home Manager check builds the activation script without activating it.
Releases
GitHub releases provide a Python wheel, source distribution, and SHA-256 checksums. The same version tag is a Nix flake reference, for example github:hakula139/listenbrainz-scrobbler/v0.1.0. Pin that reference when adding the Home Manager module to another flake.
To release, update the package version in pyproject.toml and the submission client version in tracking.py, refresh uv.lock, and commit after checks pass. Push an annotated v<version> tag with the release notes in its annotation. The release workflow reruns Linux and macOS CI, checks that the tag matches the package version, builds the distributions, and publishes their checksums and the tag's notes to GitHub. It does not publish to PyPI.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file listenbrainz_scrobbler-0.1.0.tar.gz.
File metadata
- Download URL: listenbrainz_scrobbler-0.1.0.tar.gz
- Upload date:
- Size: 56.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
035144acb26fe6f07e890be58af324f02fdd68586672429eb6a8c2609cdff992
|
|
| MD5 |
b3fb3f0b3fcaf830d1781334719003bd
|
|
| BLAKE2b-256 |
bfb8fd4f679548b208b557d5106975f29cfc657123b2ed73f07e7c5f1a6d2201
|
File details
Details for the file listenbrainz_scrobbler-0.1.0-py3-none-any.whl.
File metadata
- Download URL: listenbrainz_scrobbler-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76fecbe62a3dc1587a8ee7f7773685ee84d3d81c01858749e2e141262bc8d757
|
|
| MD5 |
98cd5e144c06fd3586d10cca34d90b0d
|
|
| BLAKE2b-256 |
71880d738436ddcab0b464121dd09645fc4d0ec093ee5ffaa35033f4ab04236f
|