AI-powered playlists and album recommendations for Plex—using only music you actually own.
MediaSage is a self-hosted web app that creates playlists and recommends albums by combining LLM intelligence with your Plex library. Every suggestion is guaranteed playable because it only considers music you have.
Sample Generated Album Recommendation:

docker run -d \
--name mediasage \
-p 5765:5765 \
-v mediasage-data:/app/data \
--restart unless-stopped \
ghcr.io/ecwilsonaz/mediasage:latestOpen http://localhost:5765 — a setup wizard walks you through connecting Plex, choosing an AI provider, and syncing your library.
You can also pass credentials as environment variables to skip the wizard. See Configuration for details.
Requirements: Docker, a Plex server with music, a Plex token, and an API key from Google, Anthropic, or OpenAI (or a local model via Ollama).
Plex users with personal music libraries have few good options for AI playlists.
Plexamp's built-in Sonic Sage used ChatGPT to generate playlists, but it was designed around Tidal streaming. The AI recommended tracks from an unlimited catalog, and Tidal made them playable. The "limit to library" setting just hid results you didn't own—so if you asked for 25 tracks and only 4 existed in your library, you got a 4-track playlist.
When Tidal integration ended in October 2024, Sonic Sage lost its foundation. Generic tools like ChatGPT have the same problem: they recommend from an infinite catalog with no awareness of what you actually own.
MediaSage inverts the approach:
| Filter-Last (Sonic Sage, ChatGPT) | Filter-First (MediaSage) |
|---|---|
| AI recommends from infinite catalog | AI only sees your library |
| Hide missing tracks after | No missing tracks possible |
| Near-empty playlists | Full playlists, every time |
The result: every track in every playlist exists in your Plex library and plays immediately.
Create playlists two ways:
Describe what you want — Natural language prompts like:
- "Melancholy 90s alternative for a rainy day"
- "Upbeat instrumental jazz for a dinner party"
- "Late night electronic, nothing too aggressive"
Start from a song — Pick a track you love, then explore musical dimensions: mood, era, instrumentation, genre, production style. Select which qualities you want more of.
Describe a mood or moment, answer two quick questions about your preferences, and get a single perfect album to listen to—with an editorial pitch explaining why it fits.
- Library mode — recommends albums you own, ready for instant playback
- Discovery mode — suggests albums you don't own yet, based on your taste profile
- Familiarity control — choose between comfort picks, hidden gems, or rediscoveries
- Show Me Another — regenerate without starting over
- Primary recommendation with a full write-up, plus two secondary picks
Before the AI sees anything, you control the pool:
- Genres — Select from your library's actual genre tags
- Decades — Filter by era
- Minimum rating — Only tracks rated 3+, 4+, etc.
- Exclude live versions — Skip concert recordings automatically
Real-time track counts show exactly how your filters narrow results.
MediaSage syncs your Plex library to a local SQLite database. After a one-time sync (~2 min for 18,000 tracks), all library operations—filtering, counting, sending to AI—happen locally in milliseconds instead of waiting on Plex.
- Setup wizard walks you through first-run configuration and sync
- Footer status shows track count and last sync time
- Auto-refresh keeps cache current (syncs if >24h stale)
- Manual refresh available anytime
Bring your own API key—or run locally:
| Provider | Max Tracks | Typical Cost | Best For |
|---|---|---|---|
| Google Gemini | ~18,000 | $0.03 – $0.25 | Large libraries, lowest cost |
| Anthropic Claude | ~3,500 | $0.15 – $0.25 | Nuanced recommendations |
| OpenAI GPT | ~2,300 | $0.05 – $0.10 | Solid all-around |
| Ollama ⚗️ | Varies | Free | Privacy, local inference |
| Custom ⚗️ | Configurable | Free | Self-hosted, OpenAI-compatible APIs |
⚗️ Local LLM support is experimental. Report issues.
Free option: Google Gemini offers a free API tier that's more than enough for personal use — no credit card required. See the Gemini free credit guide for setup instructions and details.
Estimated cost displays before you generate. MediaSage auto-detects your provider based on which key you configure.
- Play Now — send tracks directly to any Plex device for instant playback
- Create a new playlist, replace an existing one, or append tracks to one
- Device picker shows all active Plex clients with status indicators
- Duplicate detection when appending to existing playlists
- Preview tracks with album art before saving
- Remove tracks you don't want
- Rename the playlist
- See actual token usage and cost
mkdir mediasage && cd mediasage
curl -O https://raw.githubusercontent.com/ecwilsonaz/mediasage/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/ecwilsonaz/mediasage/main/.env.example
mv .env.example .envEdit .env:
PLEX_URL=http://your-plex-server:32400
PLEX_TOKEN=your-plex-token
# Choose ONE provider:
GEMINI_API_KEY=your-gemini-key
# ANTHROPIC_API_KEY=sk-ant-your-key
# OPENAI_API_KEY=sk-your-keyStart:
docker compose up -dSynology (Container Manager)
GUI:
- Container Manager → Registry → Search
ghcr.io/ecwilsonaz/mediasage - Download
latesttag - Container → Create
- Port: 5765 → 5765
- Add environment variables:
PLEX_URL,PLEX_TOKEN,GEMINI_API_KEY
Docker Compose:
mkdir -p /volume1/docker/mediasage && cd /volume1/docker/mediasage
curl -O https://raw.githubusercontent.com/ecwilsonaz/mediasage/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/ecwilsonaz/mediasage/main/.env.example
mv .env.example .env && nano .envThen in Container Manager → Project → Create, point to /volume1/docker/mediasage.
No Docker? Some Synology models (especially ARM-based units) don't support Docker/Container Manager. See Bare Metal below for running MediaSage directly with Python.
Unraid
- Docker → Add Container
- Repository:
ghcr.io/ecwilsonaz/mediasage:latest - Port: 5765 → 5765
- Add variables:
PLEX_URL,PLEX_TOKEN,GEMINI_API_KEY
TrueNAS SCALE
- Apps → Discover Apps → Custom App
- Image:
ghcr.io/ecwilsonaz/mediasage, Tag:latest - Port: 5765
- Add environment variables
Portainer
Stacks → Add Stack:
services:
mediasage:
image: ghcr.io/ecwilsonaz/mediasage:latest
ports:
- "5765:5765"
environment:
- PLEX_URL=http://your-server:32400
- PLEX_TOKEN=your-token
- GEMINI_API_KEY=your-key
volumes:
- ./data:/app/data
restart: unless-stoppedDocker isn't required. MediaSage is Python + FastAPI with no native dependencies, so it runs on any machine with Python 3.11+ — including ARM-based Synology NAS models, Raspberry Pis, or any Linux/macOS/Windows box.
git clone https://github.com/ecwilsonaz/mediasage.git
cd mediasage
python -m venv venv
source venv/bin/activate
pip install -r requirements.txtSet your environment variables:
export PLEX_URL=http://your-plex-server:32400
export PLEX_TOKEN=your-plex-token
export GEMINI_API_KEY=your-gemini-keyStart the server:
uvicorn backend.main:app --host 0.0.0.0 --port 5765Access at http://your-machine-ip:5765.
Running as a background service (systemd)
To keep MediaSage running after you close your terminal, create a systemd service:
# /etc/systemd/system/mediasage.service
[Unit]
Description=MediaSage
After=network.target
[Service]
Type=simple
User=your-user
WorkingDirectory=/path/to/mediasage
EnvironmentFile=/path/to/mediasage/.env
ExecStart=/path/to/mediasage/venv/bin/uvicorn backend.main:app --host 0.0.0.0 --port 5765
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo systemctl enable mediasage
sudo systemctl start mediasage| Variable | Required | Description |
|---|---|---|
PLEX_URL |
Yes | Plex server URL (e.g., http://192.168.1.100:32400) |
PLEX_TOKEN |
Yes | Plex authentication token |
GEMINI_API_KEY |
One required | Google Gemini API key |
ANTHROPIC_API_KEY |
One required | Anthropic API key |
OPENAI_API_KEY |
One required | OpenAI API key |
LLM_PROVIDER |
No | Force provider: gemini, anthropic, openai, ollama, custom |
PLEX_MUSIC_LIBRARY |
No | Library name if not "Music" |
OLLAMA_URL |
No | Ollama server URL (default: http://localhost:11434) |
OLLAMA_CONTEXT_WINDOW |
No | Override detected context window for Ollama (default: 32768) |
CUSTOM_LLM_URL |
No | Custom OpenAI-compatible API base URL |
CUSTOM_LLM_API_KEY |
No | API key for custom provider (if required) |
CUSTOM_CONTEXT_WINDOW |
No | Context window size for custom provider (default: 32768) |
You can also configure MediaSage through the Settings page in the web UI. Settings entered there are saved to config.user.yaml and persist across restarts. Environment variables always take priority over UI-saved settings.
Mount a config file for additional options:
plex:
music_library: "Music"
llm:
provider: "gemini"
model_analysis: "gemini-2.5-flash"
model_generation: "gemini-2.5-flash"
smart_generation: false # true = use smarter model for both (higher quality, ~3-5x cost)
defaults:
track_count: 25MediaSage uses a two-model strategy by default:
| Role | Purpose | Models Used |
|---|---|---|
| Analysis | Interpret prompts, suggest filters, analyze seed tracks | claude-sonnet-4-5 / gpt-4.1 / gemini-2.5-flash |
| Generation | Select tracks from filtered list | claude-haiku-4-5 / gpt-4.1-mini / gemini-2.5-flash |
This balances quality with cost. Enable smart_generation: true to use the analysis model for everything.
Run MediaSage with local models for privacy and zero API costs.
Ollama
-
Install Ollama and pull a model:
ollama pull llama3:8b
-
Configure MediaSage via environment or Settings UI:
LLM_PROVIDER=ollama OLLAMA_URL=http://localhost:11434
-
Select your model in Settings → the context window is auto-detected.
Recommended models: llama3:8b, qwen3:8b, mistral — models with 8K+ context work best.
Custom OpenAI-Compatible API
For LM Studio, text-generation-webui, vLLM, or any OpenAI-compatible server:
-
Start your server with an OpenAI-compatible endpoint
-
Configure in Settings:
- API Base URL:
http://localhost:5000/v1 - API Key: If required by your server
- Model Name: The model identifier
- Context Window: Your model's context size
- API Base URL:
Note: Local models are slower and may produce less accurate results than cloud providers. A 10-minute timeout is used for generation. Models with larger context windows will support more tracks.
MediaSage uses a filter-first architecture designed for large libraries (50,000+ tracks):
┌─────────────────────────────────────────────────────────────────┐
│ 1. ANALYZE │
│ LLM interprets your prompt → suggests genre/decade filters │
├─────────────────────────────────────────────────────────────────┤
│ 2. FILTER │
│ Plex library narrowed to matching tracks │
│ "90s Alternative" → 2,000 tracks │
├─────────────────────────────────────────────────────────────────┤
│ 3. SAMPLE │
│ If too large for context, randomly sample │
│ Fits within model's token limits │
├─────────────────────────────────────────────────────────────────┤
│ 4. GENERATE │
│ Filtered track list + prompt sent to LLM │
│ LLM selects best matches from available tracks │
├─────────────────────────────────────────────────────────────────┤
│ 5. MATCH │
│ Fuzzy matching links LLM selections to library │
│ Handles minor spelling/formatting differences │
├─────────────────────────────────────────────────────────────────┤
│ 6. SAVE │
│ Playlist created in Plex │
│ Ready in Plexamp or any Plex client │
└─────────────────────────────────────────────────────────────────┘
This ensures every track exists in your library while keeping API costs manageable.
git clone https://github.com/ecwilsonaz/mediasage.git
cd mediasage
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
export PLEX_URL=http://your-plex-server:32400
export PLEX_TOKEN=your-plex-token
export GEMINI_API_KEY=your-key
uvicorn backend.main:app --reload --port 5765pytest tests/ -v- Backend: Python 3.11+, FastAPI, python-plexapi, rapidfuzz, httpx
- Frontend: Vanilla HTML/CSS/JS (no build step)
- LLM SDKs: anthropic, openai, google-genai (+ Ollama via REST API)
- Deployment: Docker
Interactive documentation available at /docs when running.
| Endpoint | Method | Description |
|---|---|---|
/api/health |
GET | Health check |
/api/config |
GET/POST | Get or update configuration |
/api/setup/status |
GET | Onboarding checklist state |
/api/setup/validate-plex |
POST | Validate Plex credentials |
/api/setup/validate-ai |
POST | Validate AI provider credentials |
/api/setup/complete |
POST | Mark setup wizard as complete |
/api/library/stats |
GET | Library statistics |
/api/library/status |
GET | Cache state, track count, sync progress |
/api/library/sync |
POST | Trigger background library sync |
/api/library/search |
GET | Search library tracks |
/api/analyze/prompt |
POST | Analyze natural language prompt |
/api/analyze/track |
POST | Analyze a seed track |
/api/filter/preview |
POST | Preview filtered track list |
/api/generate |
POST | Generate playlist |
/api/generate/stream |
POST | Stream playlist generation (SSE) |
/api/playlist |
POST | Save playlist to Plex |
/api/playlist/update |
POST | Replace or append to a playlist |
/api/recommend/albums/preview |
GET | Preview album candidates for filters |
/api/recommend/analyze-prompt |
POST | Analyze prompt for genre/decade filters |
/api/recommend/questions |
POST | Generate clarifying questions |
/api/recommend/generate |
POST | Generate album recommendations |
/api/recommend/switch-mode |
POST | Switch library/discovery mode |
/api/results |
GET | List saved result history |
/api/results/{id} |
GET/DELETE | Get or delete a saved result |
/api/plex/clients |
GET | List active Plex clients |
/api/plex/playlists |
GET | List existing Plex playlists |
/api/play-queue |
POST | Send tracks to a Plex client |
/api/art/{rating_key} |
GET | Proxy album art from Plex |
/api/ollama/status |
GET | Ollama connection status |
/api/ollama/models |
GET | List available Ollama models |
/api/ollama/model-info |
GET | Get model details (context window) |
MIT



