Skip to main content

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

listenbrainz_scrobbler-0.1.0.tar.gz (56.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

listenbrainz_scrobbler-0.1.0-py3-none-any.whl (15.1 kB view details)

Uploaded Python 3

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

Hashes for listenbrainz_scrobbler-0.1.0.tar.gz
Algorithm Hash digest
SHA256 035144acb26fe6f07e890be58af324f02fdd68586672429eb6a8c2609cdff992
MD5 b3fb3f0b3fcaf830d1781334719003bd
BLAKE2b-256 bfb8fd4f679548b208b557d5106975f29cfc657123b2ed73f07e7c5f1a6d2201

See more details on using hashes here.

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

Hashes for listenbrainz_scrobbler-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 76fecbe62a3dc1587a8ee7f7773685ee84d3d81c01858749e2e141262bc8d757
MD5 98cd5e144c06fd3586d10cca34d90b0d
BLAKE2b-256 71880d738436ddcab0b464121dd09645fc4d0ec093ee5ffaa35033f4ab04236f

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.0

2 files

0.1.1

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page