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
- Features: full API coverage (including internet radio, podcasts, playlists, public shares, multi-user accounts & permissions, jukebox mode…), along with BeetstreamNexts’s own additional features.
- Installation: Install as a Beets plugin or as a standalone server.
- Usage: The CLI commands (plugin mode and standalone mode) and a quick tour of the Web UI.
- Configuration reference: Every setting by category, plus instructions on reverse proxy & CORS setup.
- Reference: Details of API coverage and tested clients.
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.
| Area | Additions over Beets | Details |
|---|---|---|
| Streaming | Direct play, on-the-fly transcoding, adaptive-bitrate HLS, server-side ReplayGain, self-healing streams | Streaming & audio quality |
| Metadata & artwork | Artist bios, top tracks, similar artists/songs, album/artist ratings, lyrics, album editions, sonic similarity | Library augmentation |
| Multi-user | Multi-user auth, user roles, bookmarks/ratings/favorites/queues that sync across devices, chat | Accounts, permissions & chat |
| Non-library audio | Internet radio stations, podcast subscriptions & downloads | Radio & podcasts |
| Playlists | BeetstreamNext’s own per-user playlists, plus read/write access to Beets’ playlist/smartplaylist files | Playlists |
| Sharing | Public landing pages and download links (no client or account required) | Sharing |
| Security | IP allow/deny lists, adaptive rate-limiting | Security |
| Jukebox mode | OpenSubsonic’s jukebox mode, augmented by Sonos and Chromecast support, remote-controlled by your client app | Jukebox mode |
| Advanced Beets queries | Prefix any search from your client app with beets: or b: to run a complex Beets query instead of simple search | Library augmentation |
External data sources
Everything below is fetched live from third-party services (entirely optional, and off by default):
| Data | Source | Enabled by |
|---|---|---|
| Artist biography | Last.fm (if lastfm_api_key is set), or Wikipedia | fetch_artists_biographies |
| Artist top tracks & similar artists/songs | Last.fm | lastfm_api_key |
| Artist images | Deezer’s public search API | fetch_artists_images |
| Missing album art | Cover Art Archive | fetch_artists_images/save_album_art |
| Album/artist community rating | Discogs’ public rating | discogs_ratings |
| Album edition/version info (e.g. “Deluxe Edition”) | MusicBrainz | fetch_album_version |
| Acoustic similarity & playlist path-finding | AudioMuse-AI (your own self-hosted instance) | audiomuse_url and audiomuse_api_token |
| Radio station / podcast favicons | The station/feed’s own site, falling back to DuckDuckGo’s icon proxy | automatic, when an icon isn’t supplied |
| Internet radio station discovery | Radio Browser | enable_radio_discovery |
| Podcast channel discovery | Podcast Index | enable_podcast_discovery |
| Song Lyrics | Uses Beets’ lyrics plugin’s configured external sources | fetch_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_keyis set, otherwise from Wikipedia iffetch_artists_biographiesis 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’
lyricsplugin (fetch_lyrics), optionally written back into Beets withsave_lyrics. - Album version/edition info (e.g. “Deluxe Edition”, “Japanese Expanded Edition”): From MusicBrainz, via
fetch_album_version, optionally saved withsave_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_artoptionally persist what was fetched to disk.
follow_playlist_embedded_urlsadditionally lets it pull cover art from a foreign playlist’s#EXTALBUMARTURLlines, 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_authif 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):
| Role | Default | Allows |
|---|---|---|
| Admin | off | Full access to the admin panel and all settings |
| Settings | on | Changing their own personal settings and password |
| Stream | on | Playing files |
| Download | off | Downloading files |
| Playlists | on | Creating and deleting their own playlists |
| Comments | on | Creating and editing comments and ratings |
| Podcasts | off | Managing their own podcast subscriptions |
| Sharing | off | Generating public shares (see Sharing) |
| Jukebox | off | Controlling jukebox-mode playback (see Jukebox) |
| Scrobbling | on | Keeping 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_enabledturns this on.replaygain_preampadds extra gain (dB) on top of the tag value.replaygain_fallbackis the gain applied to tracks that have no ReplayGain tag in Beets.audio_peak_limitprevents 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:
- BeetstreamNext’s own playlists: Created via the standard Subsonic playlist endpoints, stored per-user under
playlist_dir. Leaveplaylist_dirempty to disable BeetstreamNext-managed playlists entirely. - Beets’
playlistplugin directory: Regular.m3u-style playlist files managed by Beets. - Beets’
smartplaylistplugin directory: Query-generated playlists. These are read-only in BeetstreamNext, because they’re automatically regenerated by Beets.
Permissions
- Any user with
playlistRolecan create/edit/delete their own BeetstreamNext-native playlists. - Any user listed under
external_playlists_editorscan additionally edit/delete playlists from the Beetsplaylistplugin directory. Nobody can editsmartplaylist-generated playlists, only read.
Note: BeetstreamNext-native playlists are written to the disk as
.m3ufiles 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_hostnamerestricts the admin panel to a specific hostname (loopback is always allowed regardless). - Trusted
Hostheaders:trusted_hostscontrols whichHostheader values are accepted. - Legacy authentication:
legacy_authcontrols 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_failuresfailed attempts withinrate_limit_block_windowseconds blocks that specific (IP, username) pair. - Per-IP:
rate_limit_ip_max_failuresfailed attempts from an IP (using any username) withinrate_limit_ip_block_windowseconds 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_discoveryto 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-icontags, 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_discoveryand configurepodcastindex_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_dircontrols where downloaded episode audio is stored. Leave it empty to use the default cache location.podcast_auto_download_countcontrols how many of a channel’s most recent episodes are automatically downloaded when a new channel is added. Set it to0to 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:
| Backend | Plays on | Extra dependency |
|---|---|---|
server_hardware (default) | The server’s own audio output, via mpv | mpv binary on PATH |
sonos | A Sonos speaker on the local network, via SoCo | pip install beetstreamnext[sonos] |
chromecast | A Chromecast device on the local network, via pychromecast | pip 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-devicevalue (e.g.alsa/hw:0,0on Linux,coreaudio/BuiltInSpeakerDeviceon 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: hostif you want the “Discover devices” scan to work. - Otherwise, set
jukebox_hardware_devicedirectly in the Web UI or inconfig.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(stillbeets-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
beetsavailable as a Python package (for its query engine and file-path handling). -
Optionally (but like, highly recommended),
ffmpeginstalled and available onPATH. -
Optionally,
mpvinstalled and available onPATHfor jukebox mode using the server’s own audio hardware (not needed for thesonos/chromecastjukebox 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_pathsettings if they aren’t on yourPATH.
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-configcontains its ownbeetstreamnext:section, it is ignored in standalone mode. Put those settings in a dedicated BeetstreamNext--configYAML file, orBSN_*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(defaultall): which optional feature sets to install, comma-separated. Same list as above:wiki,podcasts,podcast-discovery,radio-discovery,sonos,chromecast, orall.BEETS_VERSION: install this exact version ofbeetsinstead of whateverpyproject.tomlwould otherwise pick.WITH_MPV(defaultfalse): also installmpv. Only needed for theserver_hardwarejukebox backend, which isn’t fully set up for Docker yet (see below), so leave this off unless you’re experimenting.WITH_DEBUG_TOOLS(defaultfalse): also installcurl,wget,ping,dig/nslookup,nc, andip/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=truewhen you actually need them.PYTHON_VERSION(default3.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
/configis wherebeetstreamnext.yaml, the.envfile (holdingBEETSTREAMNEXT_KEY, see Encryption key), and BeetstreamNext’s own database (beetstreamnext.db) all get created./cacheis scratch space. It’s not necessary to mount, but you can (if you want the cache to survive restarts).PUID/PGID(default1000/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/configand/cacheto thatPUID/PGIDbefore dropping to it for the rest of the process’s life.
Note: Don’t mount your library/music paths at
/configor/cache, or the startupchownwill recursively re-own them. If you want to control the folders’ ownership yourself, you can run the container as a specific user directly (usedocker run --user UID:GID(in which case also make sure/configand/cacheare 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.dbis 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 mountlibrary.dbitself 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
:rofor 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:/configmust point at the same host path as the mainruncontainer 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=1to that command to prevent it from writing the.envfile (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
beetsinstalled, so you can use that directly. The image setsBEETSDIR=/config/beets, sobeet(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 beetsconfig.yamllives there, both tools agree on the library/music paths with no extra flags. Just create/editconfig.yamlat/path/to/config/beets/config.yamlon the host (same volume as the one mounted at/configabove). - I recommend adding
alias beet="docker exec -it --user beetstream beetstreamnext beet"to your host’s.bashrc/.zshrcfor convenience. The--user beetstreamis important here, sincedocker execruns as root by default and you don’t wantbeetwriting 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 invokebeetin the container,docker exec’sBEETSDIRorbeet --config) at it explicitly, which overrides the/config/beetsdefault.
- BeetstreamNext itself picks up that same file automatically too (as a fallback
-
Use a separate beets install: Point BeetstreamNext at a
library.dbmanaged 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): setBEETSTREAMNEXT_KEYas 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
.envfile next to your database. Keep that file safe: set restrictive permissions if your platform doesn’t already (it’s created with0600), 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 beetstreamnextandbeetstreamnext 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_KEYfrom 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(withBSN_ADMIN_USER/BSN_ADMIN_PASSWORDset) 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]
| Flag | Effect |
|---|---|
| (none) | Start the server |
--host HOST[,HOST...] | Override host for this run (comma-separated for multiple) |
--port PORT | Override port for this run |
--threads N | Override how many threads the Waitress worker uses for this run |
--debug | Run in Flask debug mode |
--force-trust-host | Force debug mode even when not bound to localhost (NOT recommended) |
-c, --create-user | Create a new user (interactive prompts) |
--noinput | With --create-user: non-interactive, see Unattended bootstrap below |
-u, --update-user USERNAME | Update an existing user’s roles (interactive prompts) |
-d, --delete-user USERNAME | Delete a user (asks for confirmation) |
-p, --password USERNAME | Change a user’s password (interactive prompts) |
--list-users | List all registered users and their roles |
--clear-cache | Clear 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]
| Command | Effect |
|---|---|
run | Start the server (default) |
create-user | Create 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-users | List all registered users and their key roles |
clear-cache | Clear the thumbnail and HTTP caches |
Note:
commanddefaults torunif omitted
| Flags | Env var | Effect |
|---|---|---|
--config PATH | — | BeetstreamNext YAML config file (default location is OS-dependent, see here) |
--library-db PATH | BEETS_LIBRARY_DB | Path to the Beets library.db |
--music-root PATH | MUSIC_ROOT | Music root directory (where song paths are relative to) |
--bsn-db PATH | BSN_DB_PATH | Path to BeetstreamNext’s own database (default: alongside library.db, or /config in Docker) |
--beets-config PATH | BSN_BEETS_CONFIG | Optional Beets config file, for path formats/plugins only (default: $BEETSDIR/config.yaml, if set and present) |
--host HOST[,HOST...] | BSN_HOST | Host(s) to listen on |
--port PORT | BSN_PORT | Port to listen on |
--threads N | BSN_THREADS | Waitress worker threads |
--debug | — | Run in Flask debug mode |
--force-trust-host | — | Force debug mode even when not bound to localhost (NOT recommended) |
--noinput | — | With 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/nto 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_hostnameif 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:
- Explicit sources:
- via CLI flag
- via an environment variable
- in plugin mode: via Beets’
config.yaml, under abeetstreamnext:block (see Installation) - in standalone mode: via **BeetstreamNext’s own config YAML (see Installation)
- 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
homepage_github_link
Show the “View on GitHub” link on the public home page.
type: bool
default: True
homepage_docs_link
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 withcors_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
- addChatMessage
- changePassword
- createBookmark
- createInternetRadioStation
- createPlaylist
- createPodcastChannel
- createShare
- createUser
- deleteBookmark
- deleteInternetRadioStation
- deletePlaylist
- deletePodcastChannel
- deletePodcastEpisode
- deleteShare
- deleteUser
- download
- downloadPodcastEpisode
- findSonicPath
- getAlbum
- getAlbumInfo
- getAlbumInfo2
- getAlbumList
- getAlbumList2
- getArtist
- getArtistInfo
- getArtistInfo2
- getArtists
- getAvatar
- getBookmarks
- getCaptions
- getChatMessages
- getCoverArt
- getGenres
- getIndexes
- getInternetRadioStations
- getLicense
- getLyrics
- getLyricsBySongId
- getMusicDirectory
- getMusicFolders
- getNewestPodcasts
- getNowPlaying
- getOpenSubsonicExtensions
- getPlaylist
- getPlaylists
- getPlayQueue
- getPlayQueueByIndex
- getPodcastEpisode
- getPodcasts
- getRandomSongs
- getScanStatus
- getShares
- getSimilarSongs
- getSimilarSongs2
- getSong
- getSongsByGenre
- getSonicSimilarTracks
- getStarred
- getStarred2
- getTopSongs
- getTranscodeDecision
- getTranscodeStream
- getUser
- getUsers
- getVideoInfo
- getVideos
- hls
- jukeboxControl
- ping
- refreshPodcasts
- reportPlayback
- savePlayQueue
- savePlayQueueByIndex
- scrobble
- search
- search2
- search3
- setRating
- star
- startScan
- stream
- tokenInfo
- unstar
- updateInternetRadioStation
- updatePlaylist
- updateShare
- updateUser
Note: Following the reference Subsonic specification, error responses are returned with an HTTP 200 status. The actual outcome is in the response body’s
statusfield ("ok"or"failed", with an errorcode/messageon failure). Don’t rely on the HTTP status alone to detect a failed call.
Extensions
Alongside the base spec, BeetstreamNext implements every OpenSubsonic extension.
| Extension | Description |
|---|---|
apiKeyAuthentication | Authenticating with an API key alone, no password |
formPost | Accepting requests as application/x-www-form-urlencoded POST bodies |
getPodcastEpisode | Retrieving a single podcast episode’s metadata by ID |
indexBasedQueue | Setting/reading the play queue by index instead of only by ID |
playbackReport | Clients reporting their playback timeline back to the server |
songLyrics | Synchronized, multi-language lyrics, retrievable by song ID |
topSongsByArtistId | Retrieving an artist’s top songs by artist ID |
transcodeOffset | Starting a transcode from a given time offset |
transcoding | Clients making their own transcoding decisions and requesting transcoded streams directly |
sonicSimilarity | Acoustic 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_authif 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 :)