Plexsonic is a local bridge that exposes a Plex music library through Subsonic/OpenSubsonic-compatible endpoints.
It provides:
- Local account signup/login
- Plex account linking via Plex PIN
- Plex server and music library selection
- Subsonic-compatible
/rest/*.viewAPI for clients - Web test page for manual API checks
- Playback/scrobble/rating/playlist actions mapped to Plex
Plexsonic keeps a local SQLite cache of Plex music metadata to make browse/search responses fast and reduce repeated Plex API calls.
The cache is stored separately from account/session data (CACHE_SQLITE_PATH, default ./data/cache.db), refreshes automatically when Plex changes are detected, and can be safely rebuilt if removed.
Cache entries are scoped by Plex account + Plex server/library, so local Plexsonic users linked to the same Plex account can share cached metadata.
To support this project, please subscribe to my Patreon.
- Node.js 22.14+ (Node 24 recommended; Node 23 requires 23.6+)
pnpm12.4.0, pinned inpackage.json(use Corepack to select it)- A reachable Plex Media Server with a music library
- A Plex account with access to that server
pnpm install
cp .env.example .envInstall globally from this repo:
pnpm add -g plexsonic
# or
npm install -g plexsonicThen run:
plexsonicEdit .env:
PORT=3127
BIND_HOST=127.0.0.1
BASE_URL=
SQLITE_PATH=./data/app.db
CACHE_SQLITE_PATH=./data/cache.db
TRANSCODE_CACHE_PATH=./data/transcodes
TRANSCODE_CLEANUP_INTERVAL_SEC=3600
TRANSCODE_ARTIFACT_MAX_AGE_SEC=604800
SESSION_SECRET=replace-with-a-long-random-secret
TOKEN_ENC_KEY=
PLEX_PRODUCT=Plexsonic Bridge
PLEX_CLIENT_IDENTIFIER=
PLEX_WEBHOOK_TOKEN=
LICENSE_EMAIL=
PLEX_INSECURE_TLS=0
LOG_LEVEL=warn
LOG_REQUESTS=0PORT: HTTP port (default3127).BIND_HOST: listen interface (127.0.0.1local only,0.0.0.0for LAN).BASE_URL: optional public URL override used for callback generation. If empty, origin is derived from request headers.SQLITE_PATH: credentials/session/application database path.CACHE_SQLITE_PATH: WAL-backed Plex metadata cache database path.TRANSCODE_CACHE_PATH: local cache directory for ffmpeg-transcoded stream outputs.TRANSCODE_CLEANUP_INTERVAL_SEC: periodic cleanup interval for transcode artifacts (0disables cleanup).TRANSCODE_ARTIFACT_MAX_AGE_SEC: max artifact age before cleanup removes it (0disables cleanup).PLEX_WEBHOOK_TOKEN: optional shared secret for/webhooks/plex. If set, webhook calls must provide this token.SESSION_SECRET: cookie/session signing secret. Keep stable across restarts.TOKEN_ENC_KEY: optional but recommended 32-byte key (hex or base64) used to encrypt stored Plex tokens.LOG_LEVEL: logger level (trace,debug,info,warn,error,fatal).LOG_REQUESTS: set to1to enable incoming request logs. Very verbose, and can expose login credentials. (0by default).
Generate secrets (examples):
# SESSION_SECRET
openssl rand -hex 32
# TOKEN_ENC_KEY (hex)
openssl rand -hex 32/rest/stream.view can transcode in-bridge (ffmpeg) to mp3, aac, opus, and flac.
formatchooses target codec/container when supported (mp3,aac,opus,flac).maxBitRateis respected for lossy outputs.- If only
maxBitRateis provided, Plexsonic defaults toopustranscode. - Uncached transcodes start immediately with a chunked response and
Accept-Ranges: none.estimateContentLengthis accepted but does not add a guessed byte length, which can truncate playback. X-Content-Durationreports the response's audio duration in seconds (the remaining duration after a time offset). Players must read this header or the song metadata explicitly; it is separate from HTTPContent-Lengthand embedded MP3 duration metadata.- Transcode outputs are cached on disk (
TRANSCODE_CACHE_PATH) to enable byte-range seeking on subsequent requests. - Completed cached responses include an exact
Content-Lengthand support206byte ranges. - The OpenSubsonic
transcodeOffsetv1 extension supports seeking before a transcode is cached: reopen/rest/streamwithtimeOffsetin seconds. Decimal offsets are rounded to the nearest millisecond; negative, invalid, and known end-of-track or later offsets are rejected. The new audio stream starts at local time zero, so clients should add the applied offset to their playback clock and retain the original track duration. Do not seek the decoder by that offset a second time. - Offset responses are chunked, do not support byte ranges, and never replace the full-song cache. FFmpeg decodes and discards the source prefix, so later seeks can take longer to start. Specify a supported
formatto retain the desired codec; if neither format nor bitrate policy selects an output codec, offset requests use FLAC.format=rawwith a positive offset is rejected; use byte ranges for original files. - Cached transcode artifacts are auto-pruned by age (
TRANSCODE_CLEANUP_INTERVAL_SEC/TRANSCODE_ARTIFACT_MAX_AGE_SEC).
/rest/getCoverArt.view?id=...&size=300 uses ffmpeg to crop the center of cover art to a square, then resize it to at most 300×300 pixels. This preserves proportions and transparency without enlarging smaller images. Resized images are returned as PNG with their own ETag and content length. The maximum requested size is capped at 4096 pixels; omitted, non-positive, or invalid sizes return the original image. Both GET and POST requests support size.
pnpm startOr, if installed globally:
plexsonicDev mode:
pnpm devBuild and run with Compose:
docker compose up -d --buildNotes:
- Compose maps
3127:3127. ./datais mounted to/app/datafor SQLite persistence.docker-compose.ymluses${VAR:-default}interpolation.- You should change at least:
SESSION_SECRETTOKEN_ENC_KEY(recommended)BASE_URLonly if auto-detected origin is wrong in your proxy/network setup
Override via CLI (without editing compose):
SESSION_SECRET='replace-me' \
TOKEN_ENC_KEY='your-32-byte-key' \
BASE_URL='http://192.168.1.50:3127' \
docker compose up -d --buildOr with an env file:
docker compose --env-file .env up -d --buildStop:
docker compose downHealth check:
curl http://127.0.0.1:3127/health- Open
http://127.0.0.1:3127/signup - Create a local Plexsonic account
- Link Plex (
/link/plex) and complete PIN auth - Select Plex server
- Select music library
- Open
/testto run quick API checks
Use:
- Server URL:
http://<host>:3127 - Username/password: your local Plexsonic account
Both endpoint styles are accepted:
/rest/getArtists.view/rest/getArtists
Plexsonic maps Subsonic rating + star state into a single Plex numeric rating:
- Odd points = rated only (not liked):
1, 3, 5, 7, 9 - Even points = liked:
2, 4, 6, 8, 10 0= no rating and not liked
Behavior:
setRating(r)updates stars and keeps current like state when possible.startoggles like on and keeps star level. If unrated, it becomes10points (liked + 5+star).unstartoggles like off and keeps star level.
Set:
BIND_HOST=0.0.0.0
# Optional when auto-detection is not correct:
# BASE_URL=http://<your-lan-ip>:3127Then:
- Open firewall inbound TCP
3127 - Keep it LAN-only (do not expose directly to the internet)
If you run behind a reverse proxy, forward X-Forwarded-Proto and X-Forwarded-Host so Plex PIN callbacks use the correct public origin.
Without HTTPS, credentials travel unencrypted on your network.
Webhook purpose: Plex notifies Plexsonic on library/media events so Plexsonic can refresh caches faster and reduce stale results.
Plex does not auto-discover Plexsonic. You must add the webhook URL in Plex Media Server settings.
- Open Plex Media Server settings, then
Network->Webhooks. - Add your Plexsonic endpoint URL.
Without token:
http://<your-host>:3127/webhooks/plexWith token:http://<your-host>:3127/webhooks/plex?token=<PLEX_WEBHOOK_TOKEN> - Save settings in Plex.
Notes:
BASE_URLis not required for webhook processing.- Webhook URL must be reachable by your Plex Media Server (LAN IP/hostname or public URL, depending on your setup).
- This project currently targets practical client compatibility over strict parity with any single server implementation.
- Some Subsonic features may be partially implemented or client-dependent.
Apache-2.0