Skip to content

ecwilsonaz/mediasage

Repository files navigation

MediaSage for Plex

License: MIT Docker Hub GHCR Python 3.14+

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 Playlist: MediaSage Screenshot

Sample Generated Album Recommendation: MediaSage Screenshot

Home Screen: MediaSage Screenshot

Playlist Flow: MediaSage Screenshot

Album Flow: MediaSage Screenshot


Quick Start

docker run -d \
  --name mediasage \
  -p 5765:5765 \
  -v mediasage-data:/app/data \
  --restart unless-stopped \
  ghcr.io/ecwilsonaz/mediasage:latest

Open 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).


Contents


Why MediaSage?

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.


Features

Playlist Generation

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.

Album Recommendations

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

Smart Filtering

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.

Local Library Cache

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

Multi-Provider Support

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 and Save

  • 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

Installation

Docker Compose (Recommended)

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 .env

Edit .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-key

Start:

docker compose up -d

NAS Platforms

Synology (Container Manager)

GUI:

  1. Container ManagerRegistry → Search ghcr.io/ecwilsonaz/mediasage
  2. Download latest tag
  3. ContainerCreate
  4. Port: 5765 → 5765
  5. 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 .env

Then in Container ManagerProjectCreate, 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
  1. DockerAdd Container
  2. Repository: ghcr.io/ecwilsonaz/mediasage:latest
  3. Port: 5765 → 5765
  4. Add variables: PLEX_URL, PLEX_TOKEN, GEMINI_API_KEY
TrueNAS SCALE
  1. AppsDiscover AppsCustom App
  2. Image: ghcr.io/ecwilsonaz/mediasage, Tag: latest
  3. Port: 5765
  4. Add environment variables
Portainer

StacksAdd 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-stopped

Bare Metal (No Docker)

Docker 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.txt

Set your environment variables:

export PLEX_URL=http://your-plex-server:32400
export PLEX_TOKEN=your-plex-token
export GEMINI_API_KEY=your-gemini-key

Start the server:

uvicorn backend.main:app --host 0.0.0.0 --port 5765

Access 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.target
sudo systemctl enable mediasage
sudo systemctl start mediasage

Configuration

Environment Variables

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)

Web UI Configuration

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.

Advanced: config.yaml

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: 25

Model Selection

MediaSage 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.

Local LLM Setup (Experimental)

Run MediaSage with local models for privacy and zero API costs.

Ollama
  1. Install Ollama and pull a model:

    ollama pull llama3:8b
  2. Configure MediaSage via environment or Settings UI:

    LLM_PROVIDER=ollama
    OLLAMA_URL=http://localhost:11434
  3. 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:

  1. Start your server with an OpenAI-compatible endpoint

  2. 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

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.


How It Works

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.


Development

Local Setup

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 5765

Testing

pytest tests/ -v

Tech Stack

  • 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

API Reference

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)

License

MIT

About

Unofficial AI-powered Plex playlist generator with library awareness. Every track it suggests, you actually own.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors