Skip to content

Configuration

beetkeeper does not have its own config file. It reads its settings from an optional top-level beetkeeper: section inside your existing beets config. A plain beets config without that section is still a valid beets config — beetkeeper only requires the section when you run the server.

You tell beetkeeper which beets config to read via either:

  • the BEETSDIR environment variable — the directory holding your beets config.yaml (beets' own convention), or
  • the --beetsdir/-b CLI flag — the same directory, passed explicitly (see the CLI).

Example

# ... your usual beets config (directory, library, plugins, ...) ...

beetkeeper:
  logging:
    log_level: INFO
  server:
    hostname: 0.0.0.0
    port: 8337
  database:
    # beetkeeper's own SQLite db (separate from the beets library db); created automatically on first run.
    sqlite_path: /beets/beetkeeper.db
  # Optional: require a login for all access (off by default).
  auth:
    enable_login_protection: true
    username: admin
    password: change-me
  # Optional: let beetkeeper ask your download client where a library entry came from (off by default).
  downloader_hook:
    base_url: http://qbittorrent:8080
    search_endpoint_path: /api/v2/torrents/info
    beet_field_to_dl_search_field:
      album: name
    filepath_json_key: content_path
    replace_downloader_paths_prefix: /data/torrents/complete

Settings

Key Type Default Description
logging.log_level string INFO One of CRITICAL, DEBUG, ERROR, INFO, NOTSET, WARNING.
logging.log_filepath path — Write logs to this file instead of stdout. The file is created if missing; its directory must exist.
logging.log_rotation_max_bytes size 100MB Max size of the log file before it is rotated (only used with logging.log_filepath). Accepts units, e.g. 50MiB.
server.hostname string 127.0.0.1 Interface to bind (e.g. 0.0.0.0 to listen on all interfaces; the default binds loopback only).
server.port int 8337 Port the server listens on (must be > 0).
server.forwarded_allow_ips string — Reverse-proxy addresses (comma-separated IPs and/or CIDR networks like 192.168.40.0/24, or "*" for all) whose X-Forwarded-* headers the server trusts. The UI renders root-relative URLs and works through any proxy without this; set it behind a reverse proxy so the request scheme and client address reflect the real client (accurate logs, correct absolute URLs anywhere one is ever emitted). Hostnames are not supported — uvicorn compares the raw peer IP. Note that setting this replaces the loopback default, so include 127.0.0.1 if a local proxy is also in play. Unset, uvicorn's default applies (the FORWARDED_ALLOW_IPS env var, else loopback only).
database.sqlite_path path — Path to beetkeeper's own SQLite db (created automatically on first run).
database.auto_upgrade bool true Apply pending schema migrations automatically when the server starts (the db file is backed up first). When false, a stale schema fails startup until you run beetkeeper db upgrade.
auth.enable_login_protection bool false Opt-in login protection: when true, every page and API route requires a logged-in session.
auth.username string — Login username. Required when enable_login_protection is true.
auth.password string — Login password. Required when enable_login_protection is true.
auth.session_ttl_hours int 168 How long a login session stays valid before a new login is required.
downloads_path path /downloads The pre-import staging folder beetkeeper imports from (the container's /downloads mount). The import page's forms and clean-slate sources are pinned to it, and downloader hook paths are mapped into it.
downloader_hook.base_url URL — Base URL (with port) of your download client's REST API. Setting it enables the hook; the next three settings are then required.
downloader_hook.search_endpoint_path string — Endpoint (relative to base_url) beetkeeper sends GET search requests to.
downloader_hook.beet_field_to_dl_search_field map — beets fields of the library entry being looked up → the query param name each is sent as (e.g. album: name). Empty fields are left out. A clean-slate import also carries these fields (flexible attributes only) from the removed entry onto the fresh one.
downloader_hook.filepath_json_key string — JSON key in each search result holding the download's path (e.g. qBittorrent's content_path). beetkeeper uses the first result.
downloader_hook.replace_downloader_paths_prefix string "" Path prefix the download client reports which beetkeeper replaces with downloads_path — needed when the two run in containers with different mounts. Leave empty when both see the same paths.
downloader_hook.api_key string — Bearer token sent in the Authorization header, if the client's API needs one.

Login protection

beetkeeper is single-user: enabling auth.enable_login_protection gates the whole app behind the one configured username/password pair (there are no per-route permissions — a logged-in client can do everything).

  • Browsers are redirected to a /login page; a successful login stores the session in an HttpOnly cookie, and a Log out button appears in the navigation bar.
  • API clients exchange the credentials for a bearer token via POST /api/auth/login, then send it as an Authorization: Bearer <token> header (revoke it with POST /api/auth/logout).
  • The login endpoints, API docs, /api/health, and static assets stay reachable without a session.

Sessions are stored (hashed) in beetkeeper's database, so they survive restarts.

Downloader hook

beetkeeper only knows where a library entry was imported from when the beetkeeper beets plugin reported that import. Entries imported before beetkeeper was set up show Not recorded on the search page — and without a source folder they cannot be clean-slated (say, to recover a track file beets dropped).

The optional downloader_hook section lets beetkeeper ask your download client instead. It is deliberately generic — any client with a REST search endpoint can be wired up through config alone:

  1. beetkeeper looks the entry up in the beets library and takes the fields named in beet_field_to_dl_search_field, renaming each to its query param name.
  2. It sends GET <base_url><search_endpoint_path>?<params> (with Authorization: Bearer <api_key> when set). The full request shape is documented as the search-missing-source-path webhook in the API docs.
  3. The client answers with a JSON list of results (or a single object). beetkeeper reads filepath_json_key from the first one and maps the path onto downloads_path via replace_downloader_paths_prefix.

The mapping's keys have a second role: a clean-slate import carries those fields (flexible attributes only, say a torrent_hash) from the removed entry onto the fresh one, so it can still be looked up afterwards. The preview lists them under Carried over onto the new import.

Run a lookup through POST /api/import/find_missing_source_path, or the Find via downloader button on unrecorded search rows. A match is stored as the entry's inferred source path: the search page shows it labelled (inferred via downloader) — for the album and each of its tracks — with a Clean-slate import from here link, and a later lookup replaces it. Inferred paths are kept in their own table, apart from source paths recorded from the beetkeeper plugin's events; a recorded path always takes precedence. After a clean slate the inference follows the entry to its new beets id (or is dropped if nothing was imported).

beets config edits need a restart

beetkeeper applies your beets config file (and loads the plugins it lists) once, when the server first opens the library. Edit the file, then restart beetkeeper run for the change to take effect.

Authoritative source

The table above is a friendly summary. For the exact field definitions, validation, and defaults, see the beetkeeper.settings.user_config models in the source.

Separate database

database.sqlite_path is beetkeeper's own bookkeeping database — not your beets library database. It is created (and kept schema-current) automatically by beetkeeper run, or manually via beetkeeper db upgrade (see the CLI).

The beets plugin (beetkeeper_plugin)

The beetkeeper-plugin package provides the beets-side plugin that pushes library events (imports, removals) to the server's /api/events endpoints — this is what populates the Beets Events page. It ships inside the Docker image and the standalone binary; a beet command installed separately needs pip install beetkeeper-plugin in its own environment (see Installation). Enable it like any beets plugin:

plugins:
  - beetkeeper_plugin

Its own (optional) config section:

beetkeeper_plugin:
  # Where to push events. Defaults to `http://127.0.0.1:8337` — matching the beetkeeper server's own
  # defaults, right for the usual same-container/same-host setup — so most installs need no section at all.
  server_url: http://127.0.0.1:8337
  # Only needed when the server runs with `auth.enable_login_protection`; leave unset otherwise.
  api_token: your-token-here
Key Type Default Description
server_url url http://127.0.0.1:8337 Base URL of the beetkeeper server to push events to. Must be a valid http(s):// URL.
api_token string — Bearer token for the push requests, for servers running with login protection. When unset (or blank), pushes carry no auth at all.

The section is validated when beets loads the plugin (see the _bk_plugin_settings pydantic models): a malformed server_url fails plugin load with a clear validation error instead of silently pushing events nowhere, and blank values are treated as unset (their defaults apply).

Push failures are logged and swallowed — an unreachable beetkeeper server never breaks the beets operation that fired the event.