SoundStash documentation
SoundStash is a self-hosted server plus a free iOS companion app. The server downloads audio from any URL supported by yt-dlp, plus Plex and watched folders; the iOS app subscribes to podcasts, downloads episodes for offline playback, and syncs playback position with your server.
This page covers podcast features, the iOS app, server install and configuration, and troubleshooting. No signup. No external accounts.
Quick start
Run SoundStash on your own server in one command:
Then open http://localhost:5100. Full install + configuration below.
Podcasts
SoundStash subscribes to podcast RSS feeds, downloads episodes for offline playback, parses chapter markers, and groups episodes into rule-based queues. The server-side feature set and the iOS app cover the same surface; the differences are called out per subsection below.
Subscribe
There are three ways to subscribe to a podcast.
- RSS URL: paste any RSS or Atom feed URL into the Podcasts page sidebar (server web UI) or the iOS app's Add podcast field. The feed is parsed via
feedparserwith iTunes namespace support and tolerance for malformed elements. - iTunes search: type a podcast name; the server proxies the iTunes Search API and returns matching feeds. Available in both the server UI and the iOS app.
- Podcast Index search: a secondary search source (
python-podcastindex) for shows missing from iTunes. Server web UI only — the iOS app uses iTunes.
Subscribing fetches the feed once, persists the podcast metadata and the most recent episodes, and queues a background refresh job. Duplicate subscriptions are silently de-duplicated by feed URL.
Discover
The Discover page shows a curated list of popular podcasts pulled from the iTunes Top Podcasts feed by category. Tap any tile to subscribe in one step. Available in both the server web UI and the iOS app.
Episode downloads
Episodes can be downloaded one at a time or in bulk.
- Single episode: tap the download button on an episode row. The server enqueues the audio file and emits WebSocket progress events. The iOS app downloads to the device's documents directory.
- Bulk download: the server exposes four filter presets —
all_undownloaded,last_5,last_10,all_unlistened— exposed via "Download Episodes" buttons on the podcast detail and subscriptions pages. - Concurrency: the server runs two simultaneous downloads with a 15-minute watchdog timeout per download; queue state survives restarts. The iOS app runs two concurrent downloads with WiFi-only mode and pause/resume.
Downloaded episodes are stored on the server's data volume and (separately) in the iOS app's documents directory. Deleting on one surface does not delete on the other.
OPML import & export
OPML is the standard podcast-subscription exchange format (XML, .opml extension). SoundStash imports and exports OPML files compatible with Apple Podcasts, Overcast, and Pocket Casts.
- Import: upload an OPML file from Settings → Podcasts → Import OPML (server web UI) or the iOS app's equivalent. The server parses with
listparserand subscribes to every feed URL it finds, skipping duplicates. Progress streams over WebSocket. - Export: Settings → Podcasts → Export OPML downloads
subscriptions.opmlcontaining every active subscription. Server-only — the iOS app triggers export via the server endpoint when both surfaces are connected.
Background refresh
The server runs a background scheduler that re-fetches every subscribed feed at a configurable interval. Refresh uses HTTP conditional-GET (ETag + If-Modified-Since headers), so unchanged feeds return 304 Not Modified and consume no bandwidth or CPU.
Per-podcast settings let you enable auto-download for new episodes, set a retention window (auto-delete listened episodes after N days), and override the global refresh interval.
The iOS app uses iOS Background App Refresh against the same conditional-GET endpoint when the app is backgrounded; new episodes appear without re-launching the app.
Chapters
SoundStash parses chapter markers from three sources, in priority order:
- Podcasting 2.0 JSON (
<podcast:chapters url="…" type="application/json+chapters">) — the modern external chapter format. Both surfaces. - PSC (Podlove Simple Chapters, embedded in the RSS feed via
<psc:chapters>) — the older XML in-feed format. Both surfaces. - ID3 CHAP (chapter frames embedded in MP3 ID3v2 tags, parsed via
mutagen) — extracted server-side after download. Server-only in v17.0 — iOS native ID3 CHAP parser deferred to v17.1.
Chapter markers display as a tappable list during playback with skip-to-chapter support. Chapters with images render the chapter art on the player surface.
Queues
A queue is an ordered list of episodes you can play through continuously, with auto-advance between tracks. Queues sync between the server and the iOS app — adding an episode to a queue on one surface adds it on the other (Phase 159 bidirectional sync with absorbing tombstones and per-field last-write-wins resolution).
You can manually create queues, drag-reorder episodes, and rename them inline. The Now Playing surface shows the current queue context and the next episode countdown.
Smart playlists
Smart playlists are rule-based queues that auto-update as episodes are downloaded, listened to, or deleted. The rule engine supports seven filter fields (podcast, downloaded state, played state, duration, publish date, episode number, season) and five sort modes including grouped-by-podcast interleaving.
Four smart playlists are created automatically on first launch:
- Recent unplayed — newest undownloaded or downloaded-but-unplayed episodes across all subscriptions.
- Currently in progress — episodes you started but haven't finished.
- Quick listens — episodes under 30 minutes.
- Long form — episodes over 60 minutes.
Smart-queue rule editing on iOS ships in v17.0 as read-only display of the four defaults. The custom rule editor is server-web-UI-only in v17.0 (iOS rule editor deferred to v17.1).
iOS app
The SoundStash iOS app is a free companion to your self-hosted server. It connects to the server over your local network, downloads tracks and podcast episodes for offline playback, and syncs playback position both ways. Available on the App Store for iPhone.
Install
Install from the App Store: SoundStash — Audio Library. iPhone only. The app is free; an optional in-app purchase removes the offline-download limit (see In-app purchase below).
First connect
On first launch the app looks for a SoundStash server on your local network using mDNS (Bonjour). If the server's mDNS advertisement is reachable, the server appears in a picker and you tap to connect.
If mDNS auto-discovery doesn't find your server (some routers block multicast, or the server is on a different VLAN), tap Manual and enter the server IP and port directly — for example 192.168.1.212 and 5100. The app saves the address and reconnects on every launch.
You can change the server later under Settings → Server. The app supports multiple saved servers if you run more than one SoundStash instance.
Offline downloads
Tap the download button on any track, playlist, series, or podcast episode to save the audio file (and its cover art) to your device. Downloaded items play without a network connection.
- Concurrency: two simultaneous downloads.
- Pause / resume: per-item or globally; the queue survives app restarts.
- Auto-retry: up to three attempts on transient failures.
- WiFi-only mode: Settings → WiFi-only pauses the queue when you're on cellular.
Files are stored in the app's documents directory and removed when you delete the app. The free tier caps offline downloads at twenty items across all sources combined; the in-app purchase removes the cap.
Sync
The app keeps your playback position, listening status, queues, and podcast subscriptions in sync with the server. Sync is bidirectional and runs in the foreground only (there is no background sync that drains the battery).
Sync runs at four moments:
- App launch, after the player and server config have initialised.
- Every fifteen minutes while the app is in the foreground.
- When the app returns to the foreground from the background.
- Pull-to-refresh on the Browse tab.
Each cycle pushes your local changes first (positions, listening status, queue edits queued offline) and then pulls server changes since the last successful sync. Conflicts use last-write-wins on positions and listening status; the server always wins on metadata; deletions remove metadata but keep already-downloaded files on disk so you can still play them offline.
A thin green bar at the top of the screen indicates a sync is in progress. Auto-syncs are silent on error; only manual pull-to-refresh shows an error alert.
Sleep timer
On the Now Playing surface, tap the moon icon to start a sleep timer. Choose a preset — 15, 30, 45, or 60 minutes, or End of track — or enter a custom minute count.
While the timer is running, the moon icon shows a countdown badge. Tap it to extend by +15m or +30m, or to cancel. At expiry the audio fades out over ten seconds and the player pauses.
The timer uses wall-clock time, so pausing playback does not pause the timer; this matches the behaviour of Apple Podcasts and Overcast and avoids "I paused for a phone call and the timer stopped" surprises.
Visualizer
The Now Playing surface includes a real-time audio visualizer driven by a native iOS FFT module (MTAudioProcessingTap + vDSP, 30 fps). Five modes:
- Spectrum Bars — the classic frequency-bar display.
- Oscilloscope — the time-domain waveform.
- Circular — bars arranged radially around the cover art.
- Milkdrop-Lite — pseudo-3D plasma fields synced to the beat.
- Frequency Landscape — a scrolling 3D terrain of frequency intensities.
Each mode supports five colour palettes, including an automatic palette extracted from the current cover art. Tap the visualizer area to cycle modes; long-press to open the palette picker.
In-app purchase
SoundStash Unlimited is a one-time in-app purchase priced at £0.99 (non-subscription, non-renewing). It removes the twenty-item offline-download limit; everything else in the app is free.
The free tier limit is unified across all sources — tracks, series episodes, and podcast episodes count against the same cap. Once you've used twenty downloads on the free tier, the next download prompts a purchase.
Purchases are tied to your Apple ID and are restorable on any device signed in to the same Apple ID (see Restore purchase). Refunds are handled by Apple via the standard App Store refund flow.
Restore purchase
If you reinstall the app, sign in on a new device, or the unlimited unlock is missing for any other reason, open Settings → Restore Purchase and tap the button. The app asks Apple for your purchase history and restores SoundStash Unlimited if it finds a record.
Restore Purchase requires no password input from you — it uses your existing Apple ID session. There is no SoundStash account, no email signup, and no server-side purchase record; the unlock state is reconstructed from the App Store on demand.
Server
SoundStash is free self-hosted software. There's no signup, no account, no server-side telemetry. You run it yourself in Docker on your own hardware; your audio files and database stay on disks you control. The downloads, library, podcast features, and sync API are all part of one Flask container.
Install
Docker is the supported path. Manual non-Docker install is possible but not officially supported and not documented here.
Set the host path you want SoundStash to use for audio storage and start the container:
Then open http://localhost:5100. The first start initialises the SQLite database and creates the library/, thumbnails/, and logs/ subdirectories under your music path.
Below is the canonical docker-compose.yml:
network_mode: host is required for the iOS app's mDNS auto-discovery to work; it lets the server's Bonjour broadcast reach your local network. The .:/compose mount lets the server self-edit its compose file from the Settings UI; remove it if you don't need that feature.
Accessing SoundStash from outside your local network requires a reverse proxy (nginx, Caddy, Traefik) with TLS. SoundStash itself does not handle TLS or external authentication; treat it as a LAN-only service unless you've fronted it.
Configuration
SoundStash reads its configuration from environment variables. The most common ones:
MUSIC_PATH— host directory mounted into the container at/data. This is where audio files, thumbnails, and the SQLite database live.DATA_PATH— in-container data root (default/datain Docker mode). You normally don't override this.DATABASE_PATH— SQLite database path (default${DATA_PATH}/soundstash.db).SECRET_KEY— Flask session secret. Change from the default in production.DOWNLOAD_CONCURRENT— max concurrent downloads (default2).PORT— listen port (default5100).LAN_ACCESS_ENABLED— advertise the server over mDNS for iOS auto-discovery (defaulttrue).ADVERTISED_HOST— hostname or IP advertised in the mDNS record (optional; defaults to the bind interface).YT_DLP_PROXY— optional HTTP proxy URL for yt-dlp.MUSICBRAINZ_USER_AGENT— custom User-Agent for online metadata enrichment (required by MusicBrainz).
SoundStash auto-detects whether it's running in Docker, on Windows, or as a local Python process and picks sensible defaults for each. Override the auto-detect with SOUNDSTASH_DEPLOYMENT_MODE=docker (or windows / local) if needed.
Library
The library page shows every audio file SoundStash knows about, regardless of source. Each file lives on disk under ${MUSIC_PATH}/library/; cover art is stored alongside under ${MUSIC_PATH}/thumbnails/; SoundStash does not collect or upload your files anywhere — see Privacy.
- Views: grid or list, toggleable from the toolbar.
- Search: by title, artist, album, file path, or folder name.
- Filters: by format, artist, tag, or favourite state.
- Sort: by date added, title, artist, duration, play count.
- Multi-select: toggle selection mode to pick multiple tracks; bulk delete and bulk add-to-playlist are available from the action bar.
- Duplicate detection: the library page surfaces likely duplicates by title/duration match.
- Tags: custom colour-coded tags, multi-tag per track, library filter by tag.
- Series: tracks group into series with episode ordering, completeness tracking, and continue-listening.
- Playlists: manual playlists with drag-reorder and M3U export; smart playlists with rule-based auto-population.
Supported audio formats for input and output: MP3, FLAC, WAV, AAC, OGG, M4A. The format picker on download surfaces lets you transcode to any of these via FFmpeg.
URL downloads
Paste any URL supported by yt-dlp on the Download page (single item or playlist) — 1000+ video and audio sites, plus archive.org, podcast directories, and more. SoundStash picks an audio stream, transcodes to your chosen format and bitrate via FFmpeg, and streams real-time progress over WebSocket. You are responsible for ensuring the source's terms of service permit your use; see the Terms.
- Source quality picker: Best, High (192+ kbps), Medium (128-191 kbps), Low (under 128 kbps). The server analyses available streams before downloading and recommends a sensible target.
- Output format: MP3, FLAC, WAV, AAC, OGG, M4A.
- Output bitrate: Best, 320k, 256k, 192k, 128k.
- Metadata: title, artist, album, and the source thumbnail are extracted automatically and saved as ID3 tags + cover art.
- Retries: failed downloads can be retried in place from the Downloads queue.
- Quality warnings: the UI flags upsample/downsample mismatches before you commit.
Downloading copyrighted audio you don't own may violate copyright. See the Terms of Service for the full disclaimer.
Plex downloads
Connect a Plex Media Server under Settings → Plex. SoundStash supports OAuth-based server discovery (PIN flow) and a manual token fallback. Once connected, the Plex page lets you browse Libraries → Artists → Albums → Tracks and download tracks individually or by album.
- Encrypted credentials: Plex tokens are stored Fernet-encrypted in the SQLite database.
- Multi-server: connect to several Plex servers; switch between them in the UI.
- Movie / TV audio extraction: the Plex browser also supports extracting the audio track from movies and TV episodes (multi-audio-track selection where the source carries more than one).
- Codec warnings: Plex sources sometimes deliver audio in container formats the in-app player can't decode. SoundStash flags these on download and offers a one-click conversion target.
BorrowBox imports
BorrowBox is the UK library audiobook service. SoundStash imports BorrowBox loans two ways:
- Account login: connect your BorrowBox account under Settings → BorrowBox. Credentials are Fernet-encrypted. The Loans page lists active loans with cover art; one-click download streams the audiobook with chapter-level progress.
- ZIP import: drag a BorrowBox ZIP export onto the import surface. SoundStash extracts chapters, reads ID3 metadata, creates a series record, and resolves duplicates over WebSocket.
BorrowBox loans expire on the lender's schedule. SoundStash tracks loan expiry and shows active / expiring / expired badges in library and series views; expired loans are not auto-deleted — you decide when to remove the files.
Watch folders
SoundStash watches one or more host folders for new audio files. Drop an MP3 into a watched folder and it appears in the library on the next scan, with metadata read from ID3 tags and cover art extracted from embedded artwork or a sibling cover.jpg.
- Schedule: startup, daily, weekly, or custom interval.
- Drag-drop upload: the library page accepts file drops in the browser, which save into the active watch folder.
- Cloud placeholder detection: SoundStash skips OneDrive / Dropbox / iCloud cloud-only placeholder files and waits for them to materialise on disk.
- Multi-folder: bootstrap a default folder under
${MUSIC_PATH}/library/; add more folders under Settings → Folders.
Settings
The Settings page surfaces operational toggles that don't fit a per-track UI:
- Default download format and bitrate (MP3 / 192k is the install default).
- Concurrent downloads (mirrors the
DOWNLOAD_CONCURRENTenv var; runtime override). - Background scanning interval for watch folders.
- Online enrichment toggle (MusicBrainz / OpenLibrary / Google Books) for series auto-detection.
- Storage statistics (library size, database size, free disk space on the data volume).
- Plex / BorrowBox account management (connect, disconnect, refresh tokens).
- Mount management (Docker-only): add, remove, or rename volume mounts; the server self-edits the compose file and recreates the container.
Backup & restore
SoundStash's state lives in two places: the audio files under ${MUSIC_PATH}/library/ (and thumbnails under ${MUSIC_PATH}/thumbnails/) and the SQLite database ${MUSIC_PATH}/soundstash.db. Back up both to keep a complete copy.
The Backup tab generates a manifest archive containing the database plus an SHA-256 checksum for every audio file. Restore reads the manifest and offers two modes:
- Merge: add tracks from the backup that aren't already in the current library; existing rows are not modified.
- Replace: wipe the current library state and adopt the backup's view of the world. Audio files on disk are not deleted.
The integrity step verifies every file in the backup matches its recorded SHA-256 before any database changes are written.
Troubleshooting
If something breaks, check the logs first — the Docker container streams structured logs to stdout, and the iOS app surfaces sync errors as alerts on manual pull-to-refresh. The subsections below cover the most common failure modes and their fixes.
Container won't start
Check the obvious causes first:
- Port
5100isn't already in use on the host (lsof -i :5100orss -lntp | grep 5100). - The host directory you set as
MUSIC_PATHexists and is writable by the user the Docker daemon runs as. - Docker itself is healthy (
docker --version,docker info).
Read the container logs:
If the container exits immediately, the relevant error is usually in the last twenty lines. A common offender after an upgrade is a missing database migration — the log shows sqlite3.OperationalError: no such column. Stop the container, restore from backup if you took one, and check the Common errors subsection below.
Audio won't play
If the player shows a track but no audio plays:
- Confirm the file exists on disk under
${MUSIC_PATH}/library/. - Open the browser DevTools console and look for
404,403, or CORS errors on the stream URL. - Try a different format. Some browsers refuse to play OGG or M4A out of the box; MP3 is the most compatible target.
- Verify the server returns the right MIME type. Streaming responses should carry
audio/mpeg,audio/flac, or the matching MIME for the source format.
If the in-app player flags the track with a codec badge, see Codec warnings.
Missing thumbnails
Thumbnails are downloaded alongside the audio file. Some sources don't provide artwork; the library shows the default cover art SVG in that case. If a thumbnail is missing for a track that should have one:
- Check that
${MUSIC_PATH}/thumbnails/exists and is writable. - Re-trigger metadata extraction from Settings → Bulk re-scan, or open the track's detail panel and re-upload cover art manually.
- For URL downloads, the thumbnail comes from the source's poster image — some sources use a black or near-black image that's saved correctly but looks empty.
Database locked
SQLite supports a single writer at a time. Database locked errors mean another process is holding the write lock. Causes and fixes:
- Two SoundStash containers running against the same DB file: only one container should mount a given
${MUSIC_PATH}. Stop duplicates withdocker ps+docker stop. - External SQLite client open: close any DB browser,
sqlite3CLI session, or backup tool that has the database open. SoundStash's backup flow handles concurrent reads safely; ad-hoc clients don't. - Permissions on the DB file: the container's user needs write access to
${MUSIC_PATH}/soundstash.dband the directory it sits in. Fix withchownor by recreating the file from a backup.
If the lock persists with no other writer, restart the container — SQLite occasionally needs the journal file flushed.
Codec warnings
SoundStash supports MP3, FLAC, WAV, AAC, OGG, and M4A. Tracks in unsupported codecs (e.g. some Plex movie audio tracks delivered as opus, or imported audiobooks in ape) are flagged with a codec badge in the library, and the in-app player refuses to play them rather than fail silently.
The fix is to convert. The library page exposes a one-click conversion action that runs FFmpeg server-side with your chosen target format and bitrate. The original file is replaced atomically; conversion progress streams over WebSocket.
For Plex sources, the codec warning appears at download time before the file lands in the library — you choose the conversion target up-front rather than after the fact.
iOS connect failures
If the iOS app can't see your server in the auto-discover picker, walk through these steps:
- Server side: the server must run with
network_mode: hostin compose for mDNS to broadcast on your LAN (see Server install). Bridge or NAT networking blocks the Bonjour packets. - Same network: the iPhone and the server must be on the same broadcast domain. Some routers isolate Wi-Fi from Ethernet by default; check your router's "AP isolation" or "guest network" settings.
- Multicast filtering: some consumer routers drop multicast traffic between SSIDs. If auto-discovery never finds the server, fall back to Manual entry in the iOS app's connection screen and enter the server IP and port (see First connect).
- LAN access disabled: if you set
LAN_ACCESS_ENABLED=false, the server doesn't advertise over mDNS. Re-enable it (default) or use Manual entry.
If the server appears in the picker but connection fails, the iOS app's connect handshake hits /api/sync/info. Confirm that endpoint returns 200 from the same network as the iPhone (curl http://<server-ip>:5100/api/sync/info). A 200 response with a server name and version means the network path is fine; the failure is elsewhere.
Common errors
A few error messages come up often enough to call out by name:
- Video unavailable / geo-restricted: the source URL is the gate, not SoundStash. There's nothing to fix on the server. If the video plays in your browser, set
YT_DLP_PROXYto a proxy in the same region. - Network connectivity: the container can't reach the source URL. Check the container's DNS (
docker exec soundstash nslookup example.com) and any outbound firewall rules. - 500 Internal Server Error after upgrade: almost always a missing database migration. Stop the container, check the logs for
sqlite3.OperationalError: no such column, and verify the migrations dictionary inapp/__init__.pycovers any new columns from the upgrade. - Health check failing: the Docker healthcheck pings
/healthevery 30s. A failing healthcheck is a downstream symptom — check the container logs for the underlying cause before restarting. - URL downloads failing across the board: online video and audio sites periodically change their frontends in ways that break
yt-dlp. Rebuild the container (docker compose build --pull) to pick up the latestyt-dlpfrom the requirements file. Pinningyt-dlpto a stale version is the most common cause of "downloads broke after a Tuesday update".