Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

BeetstreamNext logo

BeetstreamNext

BeetstreamNext exposes your Beets music library over the OpenSubsonic API, letting you stream your music to any Subsonic/OpenSubsonic/Navidrome-compatible client.

Essentially all of the Subsonic/OpenSubsonic API specification is supported, including its extensions. Only the video-streaming functionalities are excluded (see API coverage).

BeetstreamNext also reaches beyond your Beets library to provide metadata augmentation (artist biographies and pictures, Discogs ratings, etc…) and additional reliability/security tweaks.

See Features for the full detailed view.

Docs overview

Project history

BeetstreamNext originated as a fork of Beetstream. It has since fully separated and shares almost no code with the original, but it’s still worth crediting :)

License

BeetstreamNext is MIT-licensed. See the LICENSE file for details.

Features overview

Zero-file-modification by default

BeetstreamNext is designed for people who manage metadata inside Beets but don’t want tags written back to disk. Every “save”-style setting defaults to off, and disk writes in general are additionally gated behind the allow_disk_writes setting.

OpenSubsonic API: Full coverage

BeetstreamNext implements all of the Subsonic/OpenSubsonic REST API, including its extensions. The only endpoints it doesn’t support are the video-related ones (BeetstreamNext is an audio server, and Beets doesn’t manage video anyway). See API coverage for the full endpoint-by-endpoint checklist, and Tested clients for apps confirmed to work.

BeetstreamNext’s additional features

Beets is a tagger and library manager: it only knows what’s tagged in your files (or in MusicBrainz, if you tagged from there). A lot of what shows up in a BeetstreamNext client, artist photos, biographies, community ratings, “songs like this one”, isn’t in your Beets library at all.

AreaAdditions over BeetsDetails
StreamingDirect play, on-the-fly transcoding, adaptive-bitrate HLS, server-side ReplayGain, self-healing streamsStreaming & audio quality
Metadata & artworkArtist bios, top tracks, similar artists/songs, album/artist ratings, lyrics, album editions, sonic similarityLibrary augmentation
Multi-userMulti-user auth, user roles, bookmarks/ratings/favorites/queues that sync across devices, chatAccounts, permissions & chat
Non-library audioInternet radio stations, podcast subscriptions & downloadsRadio & podcasts
PlaylistsBeetstreamNext’s own per-user playlists, plus read/write access to Beets’ playlist/smartplaylist filesPlaylists
SharingPublic landing pages and download links (no client or account required)Sharing
SecurityIP allow/deny lists, adaptive rate-limitingSecurity
Jukebox modeOpenSubsonic’s jukebox mode, augmented by Sonos and Chromecast support, remote-controlled by your client appJukebox mode
Advanced Beets queriesPrefix any search from your client app with beets: or b: to run a complex Beets query instead of simple searchLibrary augmentation

External data sources

Everything below is fetched live from third-party services (entirely optional, and off by default):

DataSourceEnabled by
Artist biographyLast.fm (if lastfm_api_key is set), or Wikipediafetch_artists_biographies
Artist top tracks & similar artists/songsLast.fmlastfm_api_key
Artist imagesDeezer’s public search APIfetch_artists_images
Missing album artCover Art Archivefetch_artists_images/save_album_art
Album/artist community ratingDiscogs’ public ratingdiscogs_ratings
Album edition/version info (e.g. “Deluxe Edition”)MusicBrainzfetch_album_version
Acoustic similarity & playlist path-findingAudioMuse-AI (your own self-hosted instance)audiomuse_url and audiomuse_api_token
Radio station / podcast faviconsThe station/feed’s own site, falling back to DuckDuckGo’s icon proxyautomatic, when an icon isn’t supplied
Internet radio station discoveryRadio Browserenable_radio_discovery
Podcast channel discoveryPodcast Indexenable_podcast_discovery
Song LyricsUses Beets’ lyrics plugin’s configured external sourcesfetch_lyrics/save_lyrics

Note: None of these ever get written back into Beets or your files unless the matching save_* setting is also enabled. See Library augmentation for more detail.


See the configuration reference for any of the settings mentioned here.

Library augmentation

Beets stores what’s tagged in your files (artist, album, MusicBrainz IDs, and whatever else your import config pulled in). BeetstreamNext can add a fair amount of extra, live-fetched information on top of that, none of it required, and none of it written anywhere (unless you explicitly want to).

Every data point on this page marked (external) comes from a third-party service, not from your Beets library. See Features overview for the full list.

Metadata enrichment

  • Artist biographies: From Last.fm if lastfm_api_key is set, otherwise from Wikipedia if fetch_artists_biographies is enabled. (external)
  • Artist top tracks and similar artists/songs: From Last.fm, using lastfm_api_key. (external)
  • Album/artist ratings: Either your users’ own local ratings, or Discogs’ public community rating via discogs_ratings (off / fallback / prefer). (external, opt-in)
  • Lyrics: Served from Beets’ own stored lyrics if present, or fetched on-the-fly using Beets’ lyrics plugin (fetch_lyrics), optionally written back into Beets with save_lyrics.
  • Album version/edition info (e.g. “Deluxe Edition”, “Japanese Expanded Edition”): From MusicBrainz, via fetch_album_version, optionally saved with save_album_version. (external)

Artwork

  • Album art / artist images are served from your Beets library’s local art path when available: nothing external needed here.
  • When missing, BeetstreamNext can fetch:
    • Album art from Cover Art Archive. (external)
    • Artist images from Deezer’s public search API. (external)
    • Controlled by fetch_artists_images. save_artists_images/save_album_art optionally persist what was fetched to disk.
  • follow_playlist_embedded_urls additionally lets it pull cover art from a foreign playlist’s #EXTALBUMARTURL lines, for albums that have no local art. (external) - only enable for playlist sources you trust!

Sonic similarity

To expose OpenSubsonic’s sonicSimilarity extension, BeetstreamNext integrates with AudioMuse-AI: a self-hosted audio-analysis service. (external)

For acoustic similarity (based on the audio, not tags or genre): finding similar songs, and path-finding a playlist between two tracks based on their audio embeddings.

Point audiomuse_url at your AudioMuse-AI instance and set audiomuse_api_token to enable it. The admin panel can trigger a full-library fingerprinting run on AudioMuse-AI directly.

Advanced search: Beets query passthrough

Any search request can be turned into a raw Beets query by prefixing it with beets: or b:, for example b:length:..3:30 to find tracks under 3:30. This runs Beets’ own complex query DSL (regex, field-specific, fuzzy matching) straight from your client’s search bar, instead of a normal search.


See the configuration reference for any of the settings mentioned here.

Accounts, permissions & chat

Authentication

Both authentication schemes from the Subsonic/OpenSubsonic spec are supported:

  • API-key authentication (recommended): Always available. A user’s API key is shown once, on account creation.
  • Legacy MD5-token / cleartext password authentication: For older clients. Can be enabled server-wide via legacy_auth if you need it.

Multi-user system

Every user account has their own bookmarks, ratings, favorites, play statistics, and play queues, all synced across whatever devices/clients they use.

BeetstreamNext stores this data separately from your Beets library, since Beets itself has no concept of multi-user data. However, you can have one user’s likes/ratings persist inside the Beets library (so they survive outside BeetstreamNext and are visible to other Beets tools): set ratings_writeback_user to that username.

Use roles

Users permissions are as defined by the OpenSubsonic/Subsonic spec: controlled by a set of independent role flags. These can be set from the admin panel’s Users tab (or --update-user/update-user on the CLI):

RoleDefaultAllows
AdminoffFull access to the admin panel and all settings
SettingsonChanging their own personal settings and password
StreamonPlaying files
DownloadoffDownloading files
PlaylistsonCreating and deleting their own playlists
CommentsonCreating and editing comments and ratings
PodcastsoffManaging their own podcast subscriptions
SharingoffGenerating public shares (see Sharing)
JukeboxoffControlling jukebox-mode playback (see Jukebox)
ScrobblingonKeeping track of what they listen on BeetstreamNext

A new user account starts with the defaults above unless overridden at creation.

Note: A few roles from the spec (coverArtRole, uploadRole, videoConversionRole) are exposed for client/spec compatibility but do currently nothing in BeetstreamNext.

Chat

BeetstreamNext implements the standard Subsonic chat endpoints (getChatMessages/addChatMessage). Any client with a chat feature can use it as a simple message board.

In addition, you can post server announcements from the admin panel, and edit or delete any user’s chat message for moderation. See Web UI usage.


See the configuration reference for any of the settings mentioned here.

Streaming & audio quality

Direct play & transcoding

BeetstreamNext serves files directly when a client can play them as-is, and transcodes on the fly via FFmpeg otherwise (bitrate limit requested by the client, unsupported format, etc.). You can set never_transcode to always stream the original file regardless of what the client asks for.

Direct play means fully lossless, bit-perfect streaming: FLAC, ALAC, and other lossless formats are sent to the client byte-for-byte exactly as they sit in your library.

Adaptive-bitrate HLS

For clients that request it, BeetstreamNext supports OpenSubsonic’s HLS spec and can serve AAC HLS with adaptive bitrate, allowing the client’s player switch quality on the fly as network conditions change.

ReplayGain

BeetstreamNext can apply ReplayGain normalization server-side during transcoding, independent of whether the client itself supports ReplayGain or not:

  • replaygain_enabled turns this on.
  • replaygain_preamp adds extra gain (dB) on top of the tag value.
  • replaygain_fallback is the gain applied to tracks that have no ReplayGain tag in Beets.
  • audio_peak_limit prevents the result from clipping (peaks capped at 0 dB) regardless of the gain applied.

Encoding errors detection & self-healing streams

A background scan (also triggerable on-demand from the admin panel) probes files for decode errors. If a track is flagged, it is automatically routed through a transcode pass instead of direct play, so a corrupt/broken file doesn’t just fail to play.

This healing pass keeps the original container/codec (when the client doesn’t ask for something else): a flagged FLAC or ALAC file is re-encoded through FFmpeg’s decoder rather than streamed byte-for-byte, but the output stays lossless.

Reverse-proxy file offloading

Direct (non-transcoded) file serving can be offloaded to the reverse proxy instead of streaming bytes through the Python process, via X-Accel-Redirect (Nginx) or X-Sendfile (Apache). See Reverse proxy & CORS) for how to setup.


See the configuration reference for any of the settings mentioned here.

Playlists

BeetstreamNext reads and writes playlists from several sources:

  1. BeetstreamNext’s own playlists: Created via the standard Subsonic playlist endpoints, stored per-user under playlist_dir. Leave playlist_dir empty to disable BeetstreamNext-managed playlists entirely.
  2. Beets’ playlist plugin directory: Regular .m3u-style playlist files managed by Beets.
  3. Beets’ smartplaylist plugin directory: Query-generated playlists. These are read-only in BeetstreamNext, because they’re automatically regenerated by Beets.

Permissions

  • Any user with playlistRole can create/edit/delete their own BeetstreamNext-native playlists.
  • Any user listed under external_playlists_editors can additionally edit/delete playlists from the Beets playlist plugin directory. Nobody can edit smartplaylist-generated playlists, only read.

Note: BeetstreamNext-native playlists are written to the disk as .m3u files like Beets’ playlists.


See playlist_dir and external_playlists_editors in the configuration reference.

Security

Access controls

  • IP allow/deny lists: ip_whitelist/ip_blacklist, they accept individual IPs or CIDR ranges.
  • Admin panel host restriction: admin_hostname restricts the admin panel to a specific hostname (loopback is always allowed regardless).
  • Trusted Host headers: trusted_hosts controls which Host header values are accepted.
  • Legacy authentication: legacy_auth controls whether the older MD5-token/cleartext Subsonic authentication mode is accepted. Many clients still need it. API-key auth authentication works regardless of this setting.

Adaptive login rate-limiting

Two independent buckets slow down brute-force attempts:

  • Per-pair of (IP, username): rate_limit_max_failures failed attempts within rate_limit_block_window seconds blocks that specific (IP, username) pair.
  • Per-IP: rate_limit_ip_max_failures failed attempts from an IP (using any username) within rate_limit_ip_block_window seconds blocks that IP. This catches attackers rotating usernames to dodge the per-user limit above.

Both buckets’s states are visible and can be cleared from the admin panel’s Security tab.


See the configuration reference for any of the settings mentioned here.

Public shares

Users with the shareRole permission can generate public share links for a song, album, or playlist. BeetstreamNext creates a public landing page with a secure download endpoint, accessible without needing the recipient to have an account or Subsonic client.

Active shares can be revoked at any time from the admin panel’s Shares tab.

Public hostname

If BeetstreamNext is reachable from the internet under a different hostname than what it binds to locally (e.g. behind a reverse proxy), set external_hostname so that share links use the correct public URL instead of the internal bind address:

beetstreamnext:
  external_hostname: music.example.com

See external_hostname in the configuration reference.

Radio & podcasts

Internet radios and Podcasts do not come from your Beets library, these are managed entirely by BeetstreamNext.

Internet radio

BeetstreamNext implements the Subsonic internet radio endpoints (getInternetRadioStations, createInternetRadioStation, updateInternetRadioStation, deleteInternetRadioStation).

Radio stations can be managed from the admin panel or any client that supports them.

  • Station discovery (external): Set enable_radio_discovery to search Radio Browser, a community-maintained directory of internet radio streams.
  • Icons: BeetstreamNext tries to scrape icons from the station homepage’s <link rel="icon">/apple-touch-icon tags, or falls back to DuckDuckGo’s icon proxy. (external)

Podcasts

BeetstreamNext implements the full Subsonic podcast feature set: subscribing to RSS feeds, browsing channels and episodes, and downloading episodes for offline playback in a client.

  • Subscriptions are per-user: Each user with the podcastRole (disabled by default) manages their own subscriptions.
  • Channel discovery (external): Set enable_podcast_discovery and configure podcastindex_api_key/podcastindex_api_secret (a free account at podcastindex.org) to search for podcasts from the admin panel.
  • Storage and auto-download:
    • podcast_storage_dir controls where downloaded episode audio is stored. Leave it empty to use the default cache location.
    • podcast_auto_download_count controls how many of a channel’s most recent episodes are automatically downloaded when a new channel is added. Set it to 0 to disable auto-download and only fetch episodes on request (see Note below).
  • OPML Import/Export: Supports importing/exporting existing podcast subscription

Note: Many clients simply do not expose any “Download” button for individual episodes. Because of this, BeetstreamNext triggers a download automatically when it receives an episode streaming request: the audio is written to disk and forwarded to the client at the same time.


See the configuration reference and enable_radio_discovery for these settings.

Jukebox mode

Subsonic’s Jukebox mode lets playback happen directly on the server, with the Subsonic client app acting as a remote control instead of streaming audio itself.

BeetstreamNext exposes (in addition to the server’s own hardware), two network backends, Sonos and Chromecast, allowing the audio to be played through a speaker on the server’s local network.

Backend choice

Set jukebox_backend to one of:

BackendPlays onExtra dependency
server_hardware (default)The server’s own audio output, via mpvmpv binary on PATH
sonosA Sonos speaker on the local network, via SoCopip install beetstreamnext[sonos]
chromecastA Chromecast device on the local network, via pychromecastpip install beetstreamnext[chromecast]

Jukebox mode must be explicitly allowed with the server-wide jukebox_allowed setting, and per-user via that user’s jukeboxRole (see Accounts, permissions & chat).

Selecting a device

You can set which device will play the audio with jukebox_hardware_device, which can be:

  • for server_hardware: an mpv --audio-device value (e.g. alsa/hw:0,0 on Linux, coreaudio/BuiltInSpeakerDevice on macOS, etc), or empty for the system default
  • for sonos: the speaker’s IP address
  • for chromecast: the device’s UUID

The admin panel also allows to scan for what devices are available and lets you pick one.

Running in Docker

Both network device-discovery mechanisms (SSDP/UPnP for Sonos, mDNS/Zeroconf for Chromecast) rely on LAN multicast, which does not cross Docker’s default bridge network. If you’re containerizing BeetstreamNext:

  • Use network_mode: host if you want the “Discover devices” scan to work.
  • Otherwise, set jukebox_hardware_device directly in the Web UI or in config.yaml.

The server_hardware backend additionally needs a real audio output device passed into the container (e.g. /dev/snd + ALSA, or a bind-mounted PulseAudio/PipeWire socket).

Note: Jukebox mode has been tested on bare-metal installs for all three backends, but Docker networking/audio passthrough is still being worked out.

Other planned backends

Snapcast, DLNA/UPnP AV, and AirPlay support are planned but not yet implemented.

Installation

BeetstreamNext can run in two modes:

  • Plugin mode: Attaches to an existing Beets install and reuses its config.yaml. This is the original, most-tested way to run it.
  • Standalone mode: BeetstreamNext runs in its own process, pointed directly at a library.db (still beets-managed, just not invoked as a Beets plugin). Useful when Beets itself lives in a different container or host than BeetstreamNext, e.g. a shared library managed by Betanin.

Both existing modes share the same feature set and settings, only how you configure and launch the server differ slightly.

Requirements

  • Python 3.13+

  • Plugin mode needs a working Beets install with a library you already import music into. Standalone mode needs beets available as a Python package (for its query engine and file-path handling).

  • Optionally (but like, highly recommended), ffmpeg installed and available on PATH.

  • Optionally, mpv installed and available on PATH for jukebox mode using the server’s own audio hardware (not needed for the sonos/chromecast jukebox backends, or if you don’t use jukebox mode. See Jukebox info).

  • Both binaries can instead be pointed to explicitly via the ffmpeg_path/mpv_path settings if they aren’t on your PATH.

1. Clone and install

git clone https://github.com/FlorentLM/BeetstreamNext.git
cd BeetstreamNext
pip install .

Optional extras pull in Python dependencies for specific features:

pip install .[wiki]              # Wikipedia artist-biographies (using wikipedia-api)
pip install .[podcasts]          # Podcast support (using feedparser for the RSS feeds)
pip install .[podcast-discovery] # Podcast channel discovery (using the Podcast Index API)
pip install .[radio-discovery]   # Internet radio station discovery (using the Radio Browser API)
pip install .[sonos]             # Sonos speaker jukebox backend (using SoCo)
pip install .[chromecast]        # Chromecast jukebox backend (using pychromecast)
pip install .[all]               # Installs all optional dependencies

(With uv, use for instance uv sync --extra podcasts, or uv sync --extra all)

This installs a beetstreamnext console command (used by standalone mode) alongside the beetstreamnext Beets plugin (used by plugin mode). These are two separate entrypoints sharing the same codebase.

2. Configure and run

Plugin mode

Add beetstreamnext to the plugins line in Beets’ config.yaml, and put any BeetstreamNext settings under a beetstreamnext: block in that file. See the configuration reference for the full list of available settings:

plugins: beetstreamnext

beetstreamnext:
  port: 8080

Then just run it through beet’s own subcommand:

beet beetstreamnext

Other plugin-mode flags: --create-user, --update-user USERNAME, --delete-user USERNAME, --password USERNAME, --list-users, --clear-cache, plus --host/--port/--threads/--debug to override those settings for a single run. See CLI usage for the full command reference.

Standalone mode

Standalone mode resolves its Beets library path (library-db) and music root (music-root) following a specific order:

  • library_db:

    `--library-db` CLI flag > BEETS_LIBRARY_DB env var > `library_db` in YAML > beets.config['library'] (only if `beets_config_path` was resolved) > Error
    
  • music_root:

    `--music-root` CLI flag > MUSIC_ROOT env var > `music_root` in YAML > beets.config['directory'] (only if `beets_config_path` was resolved) > WebUI setting > Error
    

Every other setting follows the (roughly similar) order defined in Configuration and can also be set from the Admin panel (except library_path, see Note below).

Note: Since --library-db/BEETS_LIBRARY_DB/library_db (in the YAML) is required on every run just to locate BeetstreamNext’s own database, there’s no scenario where it isn’t explicitly set, so it is currently never editable in the WebUI (I might revise this).

At minimum, point it at your library.db and music root:

beetstreamnext run --library-db /path/to/library.db --music-root /path/to/music

Or via a YAML config file. Pass --config, or drop it at the default location for your platform:

  • Docker: /config/beetstreamnext.yaml
  • Linux/macOS: $XDG_CONFIG_HOME/beetstreamnext/beetstreamnext.yaml (usually ~/.config/beetstreamnext/beetstreamnext.yaml)
  • Windows: %APPDATA%\beetstreamnext\beetstreamnext.yaml
# beetstreamnext.yaml
library_db: /path/to/library.db
music_root: /path/to/music
port: 8080
beetstreamnext run --config /path/to/beetstreamnext.yaml

Or via environment variables (handy for containers):

export BEETS_LIBRARY_DB=/path/to/library.db
export MUSIC_ROOT=/path/to/music
beetstreamnext run

Other standalone subcommands: create-user, update-user USERNAME, delete-user USERNAME, passwd USERNAME, list-users, clear-cache. See CLI usage for the full flag list (--bsn-db, --beets-config, --host, --port, --threads, --debug, --force-trust-host).

Note: If a Beets config file passed via --beets-config contains its own beetstreamnext: section, it is ignored in standalone mode. Put those settings in a dedicated BeetstreamNext --config YAML file, or BSN_* environment variables, or set them via the Admin WebUI instead.

Docker

There is a Dockerfile available at the repository root. There’s no published image yet, so you need build it yourself:

docker build -t beetstreamnext .

Build-time options (--build-arg):

  • EXTRAS (default all): which optional feature sets to install, comma-separated. Same list as above: wiki, podcasts, podcast-discovery, radio-discovery, sonos, chromecast, or all.
  • BEETS_VERSION: install this exact version of beets instead of whatever pyproject.toml would otherwise pick.
  • WITH_MPV (default false): also install mpv. Only needed for the server_hardware jukebox backend, which isn’t fully set up for Docker yet (see below), so leave this off unless you’re experimenting.
  • WITH_DEBUG_TOOLS (default false): also install curl, wget, ping, dig/nslookup, nc, and ip/ss, for poking at networking issues from inside the container (e.g. docker exec -it beetstreamnext curl ...). Off by default to keep the image lean; rebuild with --build-arg WITH_DEBUG_TOOLS=true when you actually need them.
  • PYTHON_VERSION (default 3.13): Python version to build against.

Run it like this:

docker run -d --name beetstreamnext \
  --restart unless-stopped \
  -p 8080:8080 \
  -e PUID=1000 -e PGID=1000 \
  -v /path/to/config:/config \
  -v /path/to/library.db:/data/library.db:ro \
  -v /path/to/music:/music:ro \
  -e BEETS_LIBRARY_DB=/data/library.db \
  -e MUSIC_ROOT=/music \
  beetstreamnext
  • /config is where beetstreamnext.yaml, the .env file (holding BEETSTREAMNEXT_KEY, see Encryption key), and BeetstreamNext’s own database (beetstreamnext.db) all get created.
  • /cache is scratch space. It’s not necessary to mount, but you can (if you want the cache to survive restarts).
  • PUID/PGID (default 1000/1000) should match the user that owns your library/music files on the host.

Note: BeetstreamNext never runs as root. The root user is only used at container start, to chown /config and /cache to that PUID/PGID before dropping to it for the rest of the process’s life.

Note: Don’t mount your library/music paths at /config or /cache, or the startup chown will recursively re-own them. If you want to control the folders’ ownership yourself, you can run the container as a specific user directly (use docker run --user UID:GID (in which case also make sure /config and /cache are already owned by that user), and the entrypoint will notice and won’t try to switch users itself.

docker-compose

There’s a docker-compose.yml at the repository root equivalent to the docker run command above (BeetstreamNext only, built locally from the Dockerfile). Edit the two host paths in it, then:

docker compose up -d

Example stack: BeetstreamNext + Betanin

A small stack pairing BeetstreamNext with Betanin, a web UI that drives beet import. Betanin owns the beets config and library.db, BeetstreamNext only ever reads them. The two containers need to share:

  • The beets home directory (config.yaml + library.db): needs read-write for Betanin, can be read-only for BeetstreamNext
  • The music directory: read-only for both, since neither needs to write into it

Note: Concurrent SQLite access to library.db is only reliable on a real shared filesystem or bind-mount (same host, sharing a named volume). If Betanin and BeetstreamNext ever end up on different hosts, don’t mount library.db itself over NFS/SMB from both sides, SQLite’s file locking isn’t reliable over most network filesystem protocols.

This assumes it’s saved as docker-compose.yml at the root of a BeetstreamNext checkout (build: . needs the Dockerfile there).

services:
  betanin:
    image: sentriz/betanin
    container_name: betanin
    restart: unless-stopped
    ports:
      - "9393:9393"
    environment:
      UID: "1000"
      GID: "1000"
    volumes:
      - betanin-data:/b/.local/share/betanin
      - betanin-config:/b/.config/betanin
      - beets-home:/b/.config/beets   # shared with BeetstreamNext
      - /path/to/music:/music
      - /path/to/downloads:/downloads

  beetstreamnext:
    # image: ghcr.io/florentlm/beetstreamnext:latest  # once a published image exists, prefer this over `build:`
    build: 
      context: /home/florent/BeetstreamNext
      args:
        BEETS_VERSION: 2.11.0    # Betanin currently uses beets 2.11.0 so it's safer to use the same one
    container_name: beetstreamnext
    restart: unless-stopped
    depends_on:
      - betanin
    ports:
      - "8080:8080"
    environment:
      PUID: "1000"
      PGID: "1000"
      BEETS_LIBRARY_DB: /beets/library.db
      MUSIC_ROOT: /music
    volumes:
      - beetstreamnext-config:/config
      - beets-home:/beets:ro          # same volume as Betanin's /b/.config/beets (read-only here)
      - /path/to/music:/music:ro      # same host path as Betanin's /music (read-only here)

volumes:
  betanin-data:
  betanin-config:
  beets-home:
  beetstreamnext-config:

Note:: You can run docker exec betanin beet version (or whatever your betanin container is called) to see which version of beets it is using.

Edit the two /path/to/... host paths, then docker compose up -d. Open Betanin at :9393 to configure/run imports, then BeetstreamNext at :8080.

Note: Because the music folder and library are mounted :ro for BeetstreamNext here, the admin panel will show a red “read-only” pill next to any setting that would try to write to them (see Configuration reference).

Other standalone subcommands work by overriding the container’s command. For example, an unattended first run:

docker run --rm \
  -e BSN_ADMIN_USER=admin -e BSN_ADMIN_PASSWORD=hunter2 \
  -e BEETS_LIBRARY_DB=/data/library.db \
  -v /path/to/config:/config \
  -v /path/to/library.db:/data/library.db:ro \
  beetstreamnext create-user --noinput

Note: -v /path/to/config:/config must point at the same host path as the main run container above (the encryption key and the user this creates both get stored under /config, so the two runs need to share it to see the same user/key).

Note: When using Docker, you probably want to use Docker secrets. You can add the BSN_NO_KEY_FILE=1 to that command to prevent it from writing the .env file (see unattended first run).

Who runs beet?

BeetstreamNext only reads the library. Something still has to run beet import or any other thing you want to do with Beets. Two ways to do that:

  • Use this container: It already has beets installed, so you can use that directly. The image sets BEETSDIR=/config/beets, so beet (ran inside the container) reads/writes /config/beets/config.yaml.

    • BeetstreamNext itself picks up that same file automatically too (as a fallback --beets-config/BSN_BEETS_CONFIG), so as long as your beets config.yaml lives there, both tools agree on the library/music paths with no extra flags. Just create/edit config.yaml at /path/to/config/beets/config.yaml on the host (same volume as the one mounted at /config above).
    • I recommend adding alias beet="docker exec -it --user beetstream beetstreamnext beet" to your host’s .bashrc / .zshrc for convenience. The --user beetstream is important here, since docker exec runs as root by default and you don’t want beet writing root-owned files into your library/music paths.
    • If you’d rather keep an existing beets config file elsewhere instead, you can point --beets-config/BSN_BEETS_CONFIG (and, if you also invoke beet in the container, docker exec’s BEETSDIR or beet --config) at it explicitly, which overrides the /config/beets default.
  • Use a separate beets install: Point BeetstreamNext at a library.db managed elsewhere (your own machine, or in another container like Betanin) by mounting the same files into both.

3. Encryption key

User passwords aren’t hashed, they’re stored reversibly encrypted, because Subsonic’s legacy MD5-token auth requires the server to recompute md5(password + salt) on every login, which needs the plaintext password to be recoverable… The server key BEETSTREAMNEXT_KEY is that database encryption key.

This is meant to protect against the database file leaking on its own: a backup that includes library.db/beetstreamnext.db but not the dotfiles, a misconfigured endpoint serving the db file, a db copied to a new host without also copying its key, …this sort of thing. It does not protect against a fully compromised filesystem: anyone who can read both the database and wherever the key lives can decrypt everything… but in a situation like this, your BeetstreamNext data is probably going to be the least of your worries :D

Given that, how the server key is provisioned matters:

  • You have real secrets management (Docker secrets, systemd LoadCredential, Vault, a k8s Secret, etc): set BEETSTREAMNEXT_KEY as an environment variable yourself, however your setup injects secrets, before the first run. BeetstreamNext detects it and uses it: it’s never written to disk.
  • You don’t: on first run, BeetstreamNext generates one, prints it once, and saves it to a .env file next to your database. Keep that file safe: set restrictive permissions if your platform doesn’t already (it’s created with 0600), and make sure it’s excluded from anywhere the database itself isn’t equally protected (e.g. don’t back up one without the other, etc.).

Either way: if the key is lost, stored passwords become unrecoverable and you’ll need to delete the database and set up again.

Note: You also need this server key during the first-run admin account creation if using the Web UI (see below).

4. First run

The first time the server starts with no users in the database, it walks you through creating the initial admin account:

  • Running interactively in a terminal (the common case for both beet beetstreamnext and beetstreamnext run): you’re prompted right there for a username and password, and the account is created automatically as an admin.
  • Running non-interactively (a service manager, a container, anything without a TTY attached): Accessing the Web UI directs you to a setup wizard. That page asks for an admin username and password, plus the BEETSTREAMNEXT_KEY from the step above (to avoid letting anyone set up the account before you do).
  • Fully unattended (for instance a Docker container with no TTY and nobody to click through a setup page): before starting the server, run create-user --noinput (with BSN_ADMIN_USER/BSN_ADMIN_PASSWORD set) as a separate, one-time step — see below.

Either way, the admin user account’s API key is shown once, on creation. Save it. It’s what you’ll enter into a Subsonic client instead of a password when using API-key authentication.

Once at least one user exists, use --create-user/create-user (or the admin panel’s Users tab) to create other user accounts (admins or not).

Unattended first run

You can setup the first admin account in a completely unattended way.

Use --create-user --noinput (for plugin mode) or create-user --noinput (for standalone mode)

This reads the BSN_ADMIN_USER/BSN_ADMIN_PASSWORD env vars, creates that one admin account, prints its API-key, and exits (it doesn’t start the server).

You can also add BSN_NO_KEY_FILE=1 to prevent it from writing tne .env file containing the server key (BEETSTREAMNEXT_KEY). It will only print it for you to copy into your secrets manager.

BSN_ADMIN_USER=admin BSN_ADMIN_PASSWORD=hunter2 beetstreamnext create-user --noinput --library-db /path/to/library.db
BSN_ADMIN_USER=admin BSN_ADMIN_PASSWORD=hunter2 beet beetstreamnext --create-user --noinput

(obviously replace admin and hunter2 by your chosen admin username and password)

Note: This is a one-time step. It will refuse to run if any user account already exists.

5. Normal startup

By default the server listens on 0.0.0.0:8080. Open http://<host>:8080 to see the public homepage, where you can log into the admin dashboard (see Web UI usage).

Point any Subsonic/OpenSubsonic/Navidrome client (see tested clients) at that same address. You can use the admin account directly, or a non-admin user account you create afterwards if you wish.


See the configuration reference for any of the settings mentioned here.

CLI

BeetstreamNext can be started via two different entrypoints (via beet beetstreamnext in plugin mode, or via beetstreamnext in standalone mode, see Installation), but the command-line surface underneath is the same in both modes (user management, cache clearing, server flags).

Plugin mode

In plugin mode, everything goes through Beets’ own beet beetstreamnext subcommand:

beet beetstreamnext [options]
FlagEffect
(none)Start the server
--host HOST[,HOST...]Override host for this run (comma-separated for multiple)
--port PORTOverride port for this run
--threads NOverride how many threads the Waitress worker uses for this run
--debugRun in Flask debug mode
--force-trust-hostForce debug mode even when not bound to localhost (NOT recommended)
-c, --create-userCreate a new user (interactive prompts)
--noinputWith --create-user: non-interactive, see Unattended bootstrap below
-u, --update-user USERNAMEUpdate an existing user’s roles (interactive prompts)
-d, --delete-user USERNAMEDelete a user (asks for confirmation)
-p, --password USERNAMEChange a user’s password (interactive prompts)
--list-usersList all registered users and their roles
--clear-cacheClear the thumbnail and HTTP caches

A user-management or cache flag runs that action and exits (it doesn’t start the server).

Settings not overridden by a flag come from Beets’ config.yaml (under the beetstreamnext: key) as usual, see the configuration reference.

Standalone mode

beetstreamnext [command] [username] [options]
CommandEffect
runStart the server (default)
create-userCreate a new user (interactive prompts)
update-user [username]Update an existing user’s roles (interactive prompts)
delete-user [username]Delete a user (asks for confirmation)
passwd [username]Change a user’s password (interactive prompts)
list-usersList all registered users and their key roles
clear-cacheClear the thumbnail and HTTP caches

Note: command defaults to run if omitted



FlagsEnv varEffect
--config PATHBeetstreamNext YAML config file (default location is OS-dependent, see here)
--library-db PATHBEETS_LIBRARY_DBPath to the Beets library.db
--music-root PATHMUSIC_ROOTMusic root directory (where song paths are relative to)
--bsn-db PATHBSN_DB_PATHPath to BeetstreamNext’s own database (default: alongside library.db, or /config in Docker)
--beets-config PATHBSN_BEETS_CONFIGOptional Beets config file, for path formats/plugins only (default: $BEETSDIR/config.yaml, if set and present)
--host HOST[,HOST...]BSN_HOSTHost(s) to listen on
--port PORTBSN_PORTPort to listen on
--threads NBSN_THREADSWaitress worker threads
--debugRun in Flask debug mode
--force-trust-hostForce debug mode even when not bound to localhost (NOT recommended)
--noinputWith create-user: non-interactive, see Unattended bootstrap below

See the configuration reference for the full settings precedence rules, or Installation for more on library-db/music-root specifically.

Interactive prompts

create-user/--create-user, update-user/--update-user, passwd/--password, and delete-user/--delete-user all prompt on the terminal rather than taking everything as flags:

  • Create: asks for a username (flags invalid characters and offers a sanitized alternative), a password (enforcing a minimum length), and asks you if the new user should be an admin or not.
  • Update: walks through every role one at a time, showing its current state — press Enter to leave a role unchanged, or y/n to enable/disable it.
  • Password change: asks for the new password.
  • Delete: asks for a confirmation before removing the account.

This is deliberate: these commands touch credentials, and user input is mandatory. There is one exception to this, the unattended first run.


Web UI

The Web UI is mostly self-explanatory so this is just a quick tour, plus the handful of things that are only reachable through it (that is, not exposed to Subsonic clients).

Public homepage

Your configured http://<host>:<port>/ (or your external_hostname) shows a public homepage.

Note: The “Login” button will only show up on the host allowed by admin_hostname if this setting is set.

If public_now_playing is enabled, a card displays the currently playing track (off by default).

🖼️ Screenshot: public homepage

Public shares

TODO

Admin dashboard

The dashboard is organized into tabs. Most settings are editable live, while some require a server restart to take effect. A setting that’s already pinned by a CLI flag, environment variable, or config.yaml shows as locked/disabled here — see Configuration reference for the precedence rules.

Users

Create/update/delete users, toggle their roles, regenerate an API key, and manage avatars.

Server

Every Server & Network related setting.

🖼️ Screenshot: Server settings page

Library

Every Library & Metadata related setting.

🖼️ Screenshot: Library settings page

Audio

Every Audio & Jukebox related setting.

🖼️ Screenshot: Audio settings page

Security

Live view of the current IP allow/deny lists and rate-limit state (see Security). Allows adding/removing entries, and clearing rate-limit buckets.

🖼️ Screenshot: Security tab

Podcasts & radio

Add/refresh/delete podcast subscriptions and internet radio stations server-wide including radio station discovery when enable_radio_discovery is on.

🖼️ Screenshot: Radios

🖼️ Screenshot: Podcasts

Shares

View and revoke any active public share.

Chat moderation

View the full chat log, edit/delete any user’s message for moderation, or post server announcements.

🖼️ Screenshot: chat moderation panel

Beets

Interact with the Beets installation: view/update Beets’ config.yaml, trigger a Beets library rescan and view its output log.

Maintenance

Trigger a library health scan (see Encoding errors detection & self-healing streams), cleanup BeetstreamNext’s database, clear disk caches, and view the server logs.

Configuration reference

BeetstreamNext settings can be set from various ways:

  1. Explicit sources:
    • via CLI flag
    • via an environment variable
    • in plugin mode: via Beets’ config.yaml, under a beetstreamnext: block (see Installation)
    • in standalone mode: via **BeetstreamNext’s own config YAML (see Installation)
  2. The Admin WebUI: Only for settings that aren’t already pinned by one of the explicit sources above. When a setting is pinned by any of those sources, its field in the Admin panel shows as locked/disabled.

Settings resolve order (first one set takes precedence):

  • in plugin mode:

      CLI flag > environment variable > Beets' config's `beetstreamnext:` block > WebUI setting > Built-in default
    
  • in standalone mode:

      CLI flag > environment variable > BeetstreamNext's YAML config file > Beets' config file (if you point it at one, and only for `library-db`/`music-root`) > WebUI setting > Built-in default
    

List-based settings (like for example ip_whitelist/ip_blacklist) are a bit more flexible: only the individual entries coming from an explicit source are pinned, but you can still add/remove other entries on top of them from the Admin panel.

Settings marked requires restart only take effect after the server is restarted. Settings marked standalone only are not used when running as a Beets plugin.

Note: BeetstreamNext checks at startup whether the music folder and the beets library database are writable on disk. If either isn’t (for example when they’re bind-mounted :ro, as in the Betanin example stack), the WebUI will show a red “read-only” pill next to any setting that would need to write there.

Server & network

admin_hostname

If set, the admin panel will only be accessible when visited via this hostname (e.g. beetstreamnext.internal.example.com).
Loopback is always allowed.

type: str

default: (empty)

env: BSN_ADMIN_HOSTNAME


external_hostname

Your external, public hostname (e.g. music.example.com).

type: str

default: (empty)

env: BSN_EXTERNAL_HOSTNAME


host

Host(s) to listen on (comma-separated for multiple).

requires restart

type: list[str]

default: 0.0.0.0

env: BSN_HOST


port

Port to listen on.

requires restart

type: int

default: 8080

env: BSN_PORT


threads

Worker threads for serving requests.

requires restart

type: int

default: 16

env: BSN_THREADS


channel_timeout

Seconds of inactivity allowed on a connection before Waitress closes it. Lower this on low-resource environments to free up connections faster.

requires restart

type: int

default: 120


connection_limit

Maximum number of simultaneous connections Waitress will accept. Lower this on low-resource environments to cap memory/socket usage.

requires restart

type: int

default: 100


cors_origins

Allowed CORS origins (comma-separated, * for all). Empty to disable CORS.

requires restart

type: str

default: (empty)

env: BSN_CORS_ORIGINS


cors_supports_credentials

Allow CORS requests with credentials (cookies, HTTP auth).

requires restart

type: bool

default: False

env: BSN_CORS_CREDENTIALS


reverse_proxy

Server is behind a reverse proxy (Nginx, Caddy, Traefik, etc.).

requires restart

type: bool

default: False

env: BSN_REVERSE_PROXY


proxy_hops

Number of trusted reverse proxies in front of the server. Only used if reverse_proxy is enabled

requires restart

type: int

default: 1

env: BSN_PROXY_HOPS


sendfile_method

Offload direct (non-transcoded) file serving to the reverse proxy instead of streaming bytes through Python. Use x-accel-redirect for Nginx, x-sendfile for Apache.
Only takes effect when reverse_proxy is enabled and the proxy is configured to honor the header.

type: str

default: off, choices: off, x-accel-redirect, x-sendfile


sendfile_internal_prefix

Internal URI prefix your Nginx config maps, via an internal-only location block, to the music root directory.
Only used when sendfile_method is x-accel-redirect.

type: str

default: /_bsn_internal


trusted_hosts

Allowed Host headers (domain names/IPs, comma-separated).
If empty, all hosts are allowed.
Loopback is always allowed.

type: str

default: (empty)

env: BSN_TRUSTED_HOSTS


legacy_auth

Allow legacy MD5 token / cleartext password authentication. API-key authentication always works.

type: bool

default: False

env: BSN_LEGACY_AUTH


public_now_playing

Show the currently playing song on the public home page.

type: bool

default: False


Show the “View on GitHub” link on the public home page.

type: bool

default: True


Show the “Read the docs” link on the public home page.

type: bool

default: True


homepage_connect_hint

Show the “Connect your favourite client using this URL” hint and server URL on the public home page.

type: bool

default: True


music_root

Root directory your beets library’s file paths are relative to.

standalone only, requires restart

type: str

default: (empty)


library_path

Path to the beets library.db to serve.

standalone only, requires restart

type: str

default: (empty)


library_remote_path

Optional, mount point used by another container that accesses the beets library (e.g. Betanin), if it differs from where ‘music_root’ mounts it in the BeetstreamNext container. Leave empty if both containers mount the music volume at the same path.

standalone only

type: str

default: (empty)


playlist_dir

Directory for BeetstreamNext’s own playlists. Leave empty to disable.

requires restart

type: str

default: (empty)

env: BSN_PLAYLIST_DIR


strict_beets_version_check

Refuse to start on beets version mismatch (see Server Info) instead of just warning.

standalone only, requires restart

type: bool

default: False

Library & metadata

allow_disk_writes

Required to allow beet import scans to modify content on disk (writing tags in the files or copying/moving files). Not required if the loaded Beets config has write/copy/move all disabled.

type: bool

default: False

env: BSN_ALLOW_DISK_WRITES


never_transcode

Never transcode files, always stream the original.

type: bool

default: False

env: BSN_NEVER_TRANSCODE


lastfm_api_key

Last.fm API key for fetching metadata.

sensitive

type: str

default: (empty)

env: BSN_LASTFM_API_KEY


fetch_artists_images

Fetch missing artist images from external services.

type: bool

default: False

env: BSN_FETCH_ARTISTS_IMAGES


save_artists_images

Save fetched artist images to disk.

type: bool

default: False

env: BSN_SAVE_ARTISTS_IMAGES


fetch_artists_biographies

Fetch artist short biography from Wikipedia.

type: bool

default: False


save_album_art

Save fetched album art on disk, alongside music files.

type: bool

default: False

env: BSN_SAVE_ALBUM_ART


follow_playlist_embedded_urls

Fetch external images from foreign playlists’ #EXTALBUMARTURL lines (for albums that have no art locally). Only enable this if you trust the source of your imported playlists.

type: bool

default: False


fetch_lyrics

Fetch missing song lyrics using Beets’ Lyrics plugin.

type: bool

default: False


save_lyrics

Save fetched lyrics to the beets library database.

type: bool

default: False


fetch_album_version

Fetch album version info (“Deluxe Edition”, “Japanese Expanded Edition”, etc.) from MusicBrainz.

type: bool

default: False


save_album_version

Save fetched album version info to the beets database.

type: bool

default: False


discogs_ratings

Use Discogs’ public community rating for an album’s average rating.

fallback: Only use Discogs when nobody on this server has rated the album locally. It never overrides a local rating.

prefer: Always uses Discogs when available (falling back to the local average when it isn’t).

type: str

default: off, choices: off, fallback, prefer


ignored_articles

Space-separated articles (across any language) to ignore when sorting artists alphabetically (for instance, “The Beatles” -> B).

type: str

default: The A An Der Die Das Ein Eine El La Los Las Un Una Le Les Il Lo Gli Uno O Os As Um Uma De Het Den Det


ratings_writeback_user

Commit this user’s Likes and Ratings into the Beets library so they survive outside BeetstreamNext. Beets has no concept of per-user data, so only one user’s changes can be committed this way.
Leave unset to disable.

type: str

default: (empty)


external_playlists_editors

Who can rename/edit/delete non-BeetstreamNext playlists (from Beets’ playlist plugin directory).

smartplaylist-generated playlists are always read-only.

type: list[str]

default: (empty)


enable_radio_discovery

Enable Radio Browser API for station discovery.

type: bool

default: False


fetch_radio_images

Automatically fetch station icon when adding a new radio station.

type: bool

default: True


audiomuse_api_token

API token for your AudioMuse-AI instance.

sensitive

type: str

default: (empty)

env: BSN_AUDIOMUSE_API_TOKEN


audiomuse_url

URL to your AudioMuse-AI instance (e.g. http://localhost:8000) to enable sonic similarity endpoints.

type: str

default: (empty)

Podcasts

podcast_storage_dir

Directory to store downloaded podcast episode audio. Leave empty to use the default cache location.

type: str

default: (empty)


podcast_auto_download_count

Number of episodes to download of a channel’s most recent episodes when added. Set to 0 to disable and only download episodes on request.

type: int

default: 3


enable_podcast_discovery

Enable Podcast Index API for channel discovery.

type: bool

default: False


podcastindex_api_key

API key for the Podcast Index, used for podcast channel discovery.

sensitive

type: str

default: (empty)

env: BSN_PODCASTINDEX_API_KEY


podcastindex_api_secret

API secret for the Podcast Index.

sensitive

type: str

default: (empty)

env: BSN_PODCASTINDEX_API_SECRET

Audio & jukebox

replaygain_enabled

Apply ReplayGain normalization on the server side.

type: bool

default: False


replaygain_preamp

Additional gain (dB) to apply.

type: int

default: 0


replaygain_fallback

Gain (dB) to apply to tracks without ReplayGain tags in beets’ library.

type: int

default: -6


audio_peak_limit

Always prevent audio peaks from exceeding 0 dB (prevent clipping).

type: bool

default: False


ffmpeg_path

Path to the ffmpeg binary, if it isn’t on the system PATH (e.g. /usr/local/bin/ffmpeg).
Leave empty to auto-detect from PATH.

type: str

default: (empty)


jukebox_allowed

Allow jukebox mode: the server can play audio on its own hardware, or on a Sonos/Chromecast compatible speaker (client apps act as remote controls).

type: bool

default: False


jukebox_backend

Defines where jukebox mode will play audio from.
server_hardware plays on this server’s own audio hardware using mpv.
sonos or chromecast stream to a speaker on the local network.

type: str

default: server_hardware, choices: server_hardware, sonos, chromecast


jukebox_hardware_device

Which hardware jukebox mode plays audio on.
For server_hardware, this is the audio output device as mpv’s –audio-device expects (e.g. alsa/hw:0,0 or coreaudio/BuiltInSpeakerDevice), or empty to use the system default.
For sonos, the speaker’s IP address.
For chromecast, the device’s UUID.

type: str

default: (empty)


mpv_path

For server_hardware backend only. Path to the mpv binary, if it isn’t on the system PATH (e.g. /usr/local/bin/mpv).
Leave empty to auto-detect from PATH.

type: str

default: (empty)

Security

ip_whitelist

Allowed IPs (empty = allow all except blacklist).

type: list[str]

default: (empty)

env: BSN_IP_WHITELIST


ip_blacklist

Banned IPs.

type: list[str]

default: (empty)

env: BSN_IP_BLACKLIST


rate_limit_max_failures

Failed attempts before an IP is rate-limited.

type: int

default: 5


rate_limit_block_window

Seconds before failures roll off.

type: int

default: 300


rate_limit_ip_max_failures

Failed attempts from a single IP (across any usernames tried) before that IP is blocked outright. Catches attackers rotating usernames to dodge the per-user limit above.

type: int

default: 20


rate_limit_ip_block_window

Seconds before an IP-wide failure count rolls off.

type: int

default: 3600

Reverse proxy & CORS

BeetstreamNext reads standard HTTP headers to determine the original client’s IP, so putting it behind a reverse proxy is straightforward. Enable the reverse_proxy option (see configuration) so it trusts those forwarded headers, and set the proxy_hops to the number of trusted proxies in front of it.

Nginx

location /beetstreamnext {
    proxy_pass http://127.0.0.1:8080;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # If hosting in a subfolder, tell BeetstreamNext what the subfolder is!
    proxy_set_header X-Forwarded-Prefix /beetstreamnext;
}

Caddy 2

Caddy passes all the required headers by default:

example.com {
    reverse_proxy 127.0.0.1:8080
}

Offloading file serving to the proxy

Instead of streaming raw file bytes through the Python process, the reverse proxy can serve them directly. This only takes effect when reverse_proxy is enabled, and only helps for direct (non-transcoded) playback/downloads, transcoded streams always go through Python regardless.

Set sendfile_method to x-accel-redirect (Nginx) or x-sendfile (Apache).

Nginx (x-accel-redirect)

Add an internal-only location block aliased to your music root, and point sendfile_internal_prefix at it (must match on both sides):

location /_bsn_internal/ {
    internal;
    alias /path/to/your/music/;
}
beetstreamnext:
    sendfile_method: x-accel-redirect
    sendfile_internal_prefix: /_bsn_internal

/_bsn_internal is the default for sendfile_internal_prefix, so you only need to set it explicitly if you use a different prefix.

Apache (x-sendfile, via mod_xsendfile)

XSendFile On
XSendFilePath /path/to/your/music/
beetstreamnext:
    sendfile_method: x-sendfile

sendfile_internal_prefix isn’t used with x-sendfile (it’s Nginx-specific).

Web clients and CORS

By default, CORS is disabled. Native mobile/desktop apps usually ignore CORS entirely, so you probably don’t need to change anything for those.

However, if you want to use a web-based Subsonic player hosted on a different domain than BeetstreamNext, your browser will block the connection unless you explicitly allow that origin:

beetstreamnext:
    cors_origins: 'https://music.example.com' # comma-separated list, or '*' for all

If you’re behind an SSO gateway (Authelia, Authentik, etc.), or the web player is a bit quirky, you might also need:

beetstreamnext:
    cors_supports_credentials: true

Warning: Never set cors_origins: '*' together with cors_supports_credentials: true. Doing so would allow any website you visit to silently interact with your BeetstreamNext server in the background.

API coverage

See Features for what BeetstreamNext adds on top of the spec itself :)

BeetstreamNext implements essentially the entire Subsonic/OpenSubsonic REST API. The only unsupported endpoints are video-related:

Full endpoint-by-endpoint checklist

Note: Following the reference Subsonic specification, error responses are returned with an HTTP 200 status. The actual outcome is in the response body’s status field ("ok" or "failed", with an error code/message on failure). Don’t rely on the HTTP status alone to detect a failed call.

Extensions

Alongside the base spec, BeetstreamNext implements every OpenSubsonic extension.

ExtensionDescription
apiKeyAuthenticationAuthenticating with an API key alone, no password
formPostAccepting requests as application/x-www-form-urlencoded POST bodies
getPodcastEpisodeRetrieving a single podcast episode’s metadata by ID
indexBasedQueueSetting/reading the play queue by index instead of only by ID
playbackReportClients reporting their playback timeline back to the server
songLyricsSynchronized, multi-language lyrics, retrievable by song ID
topSongsByArtistIdRetrieving an artist’s top songs by artist ID
transcodeOffsetStarting a transcode from a given time offset
transcodingClients making their own transcoding decisions and requesting transcoded streams directly
sonicSimilarityAcoustic similarity and playlist path-finding (see Library augmentation)

Note: All these extensions are always advertised by the server except sonicSimilarity, which only appears if an AudioMuse-AI instance is configured.

Authentication

Both authentication schemes from the spec are supported:

  • API-key authentication (recommended): Always available.
  • Legacy MD5-token / cleartext password authentication: For older clients. Can be enabled server-wide via legacy_auth if your client needs it.

Tested clients

BeetstreamNext should be compatible with virtually any Subsonic/OpenSubsonic/Navidrome client.

It has been specifically tested and confirmed working with:

Android

iOS

Desktop

Note: Not seeing your client here doesn’t mean it won’t work, this is just what I have manually verified. If you confirm another client works well, consider opening a PR or an Issue to let me know :)