Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
PLEX_BASE_URL=http://your-plex-server:32400
PLEX_TOKEN=your-plex-token

# LLM configuration
# PLEXMUSE_MODEL is any LiteLLM model string; it sets the default model and,
# together with the API key you provide, selects the provider.
# OpenAI: gpt-4o (needs OPENAI_API_KEY)
# Anthropic: anthropic/claude-3-5-sonnet-latest (needs ANTHROPIC_API_KEY)
# Gemini: gemini/gemini-1.5-pro (needs GEMINI_API_KEY)
PLEXMUSE_MODEL=gpt-4
# Optional: comma-separated models to offer in the UI dropdown (default is shown first).
# PLEXMUSE_MODELS=gpt-4o,gpt-3.5-turbo,anthropic/claude-3-5-sonnet-latest

# Provide the API key(s) for whichever provider(s) your model(s) use.
OPENAI_API_KEY=your-openai-api-key
ANTHROPIC_API_KEY=your-anthropic-api-key
ANTHROPIC_API_KEY=your-anthropic-api-key
49 changes: 44 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Welcome to **Plexmuse**! This project leverages the power of AI to generate pers
## Features ✨

- **AI-Powered Recommendations**: Generate playlists using advanced language models like GPT-4 and Claude.
- **Configurable Model & Provider**: Pick any [LiteLLM](https://docs.litellm.ai/docs/providers)-supported model (OpenAI, Anthropic, Gemini, …) via a single environment variable β€” no code changes.
- **Multiple Music Libraries**: Works across every music library on your server, regardless of what they're named.
- **Seamless Plex Integration**: Fetch and manage your music library directly from Plex.
- **Customizable Playlists**: Tailor your playlists with specific prompts and models.

Expand Down Expand Up @@ -41,6 +43,34 @@ Welcome to **Plexmuse**! This project leverages the power of AI to generate pers

For setting up OpenAI, Anthropic, or other LLM keys, follow the instructions in the LiteLLM documentation: [LiteLLM - Set Keys](https://docs.litellm.ai/docs/set_keys).

### Configuration βš™οΈ

The LLM model, provider, and API key are all configurable via environment variables, so you can switch providers without changing any code.

| Variable | Required | Description |
| --- | --- | --- |
| `PLEX_BASE_URL` | Yes | URL of your Plex Media Server, e.g. `http://192.168.1.10:32400`. |
| `PLEX_TOKEN` | Yes | Your Plex authentication token. |
| `PLEXMUSE_MODEL` | No (default `gpt-4`) | Any [LiteLLM model string](https://docs.litellm.ai/docs/providers). Sets the default model and selects the provider. |
| `PLEXMUSE_MODELS` | No | Comma-separated list of models to offer in the UI dropdown. The default model is always included. |
| `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / … | Depends on model | The API key for whichever provider your model(s) use. |

The API key required is determined by the chosen model. If it is missing or empty, the app fails fast with a clear error naming the environment variable to set, rather than making a doomed API call.

Examples:

```sh
# Use Anthropic Claude
PLEXMUSE_MODEL=anthropic/claude-3-5-sonnet-latest
ANTHROPIC_API_KEY=sk-ant-...

# Offer a choice of models in the UI
PLEXMUSE_MODEL=gpt-4o
PLEXMUSE_MODELS=gpt-4o,gpt-3.5-turbo,anthropic/claude-3-5-sonnet-latest
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
```

### Running the Application

You can run the application using the Makefile or directly with Docker.
Expand All @@ -51,10 +81,6 @@ You can run the application using the Makefile or directly with Docker.
```sh
make run
```
2. **Start**:
```sh
make start
```

#### Using Docker

Expand All @@ -63,11 +89,24 @@ You can run the application using the Makefile or directly with Docker.
docker compose build
```

2. **Start the Docker container**:
2. **Start the Docker container** (reads configuration from your `.env` file):
```sh
docker compose up
```

#### Using `docker run` with inline configuration

You can also configure the model, provider, and API key directly at launch:

```sh
docker run -p 8000:8000 \
-e PLEX_BASE_URL=http://your-plex-server:32400 \
-e PLEX_TOKEN=your-plex-token \
-e PLEXMUSE_MODEL=anthropic/claude-3-5-sonnet-latest \
-e ANTHROPIC_API_KEY=sk-ant-... \
ghcr.io/lubergalexander/plexmuse:latest
```


## Usage πŸ“–

Expand Down
44 changes: 44 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
Application configuration sourced from environment variables.

This makes the LLM model/provider (and therefore which API key is required)
configurable at runtime, e.g. when launching the Docker container:

docker run -e PLEXMUSE_MODEL=anthropic/claude-3-5-sonnet-latest \
-e ANTHROPIC_API_KEY=sk-... ...

Any model string supported by LiteLLM works, so switching provider is just a
matter of changing ``PLEXMUSE_MODEL`` and supplying the matching API key.
"""

import os
from functools import lru_cache
from typing import List

# Used only when ``PLEXMUSE_MODEL`` is not set.
DEFAULT_MODEL = "gpt-4"


class Settings: # pylint: disable=too-few-public-methods
"""Runtime settings resolved from environment variables."""

def __init__(self) -> None:
# The model used when a request does not specify one.
self.default_model: str = (os.getenv("PLEXMUSE_MODEL") or DEFAULT_MODEL).strip()

# Optional comma-separated list of models offered in the UI dropdown.
raw_models = os.getenv("PLEXMUSE_MODELS", "")
models: List[str] = [m.strip() for m in raw_models.split(",") if m.strip()]

# The default model must always be selectable, listed first.
if self.default_model in models:
models.remove(self.default_model)
models.insert(0, self.default_model)

self.available_models: List[str] = models


@lru_cache(maxsize=1)
def get_settings() -> Settings:
"""Return the cached settings singleton (read once after dotenv loads)."""
return Settings()
27 changes: 22 additions & 5 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Plexmuse API with initialization
"""

import json
import logging
import os
from contextlib import asynccontextmanager
Expand All @@ -13,6 +14,7 @@
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles

from app.config import get_settings
from app.models import Artist, PlaylistRequest, PlaylistResponse, Track

from .services.llm_service import LLMService
Expand Down Expand Up @@ -59,23 +61,33 @@ async def lifespan(app_context: FastAPI): # pylint: disable=unused-argument

@app.get("/")
async def root():
"""Serve the index.html file with Plex configuration injected"""
"""Serve the index.html file with Plex and model configuration injected"""
plex_base_url = os.getenv("PLEX_BASE_URL")
plex_token = os.getenv("PLEX_TOKEN")
settings = get_settings()

with open("static/index.html", "r", encoding="utf-8") as f:
html_content = f.read()

# Inject Plex configuration before closing body tag
# Inject Plex and model configuration before closing body tag
script_tag = f"""<script>
window.plexBaseUrl = "{plex_base_url}";
window.plexToken = "{plex_token}";
window.defaultModel = {json.dumps(settings.default_model)};
window.availableModels = {json.dumps(settings.available_models)};
</script>"""
html_content = html_content.replace("</body>", f"{script_tag}</body>")

return HTMLResponse(content=html_content)


@app.get("/config")
async def get_config():
"""Expose the runtime model configuration to clients"""
settings = get_settings()
return {"default_model": settings.default_model, "available_models": settings.available_models}


@app.get("/health")
async def health_check():
"""Health check endpoint"""
Expand All @@ -92,10 +104,13 @@ async def get_artists():
async def create_recommendations(request: PlaylistRequest):
"""Create playlist recommendations"""
try:
# Resolve the model once so the request, prompts, and playlist metadata agree.
model = request.model or get_settings().default_model

# Step 1: Get artist recommendations
artists = plex_service.get_all_artists()
recommended_artists = llm_service.get_artist_recommendations(
prompt=request.prompt, artists=artists, model=request.model
prompt=request.prompt, artists=artists, model=model
)

# Step 2: Get all recommended artists' albums in one call
Expand All @@ -105,18 +120,20 @@ async def create_recommendations(request: PlaylistRequest):
track_recommendations = llm_service.get_track_recommendations(
prompt=request.prompt,
artist_tracks=artist_albums,
model=request.model,
model=model,
min_tracks=request.min_tracks,
max_tracks=request.max_tracks,
)

# Step 4: Generate playlist name
playlist_name = llm_service.generate_playlist_name(prompt=request.prompt, model=request.model)
playlist_name = llm_service.generate_playlist_name(prompt=request.prompt, model=model)

# Step 5: Create the playlist
playlist = plex_service.create_curated_playlist(
name=playlist_name,
track_recommendations=track_recommendations,
prompt=request.prompt,
model=model,
)
return PlaylistResponse(
name=playlist.title,
Expand Down
4 changes: 3 additions & 1 deletion app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ class PlaylistRequest(BaseModel):
"""Request model for playlist generation"""

prompt: str = Field(..., description="Description of the desired playlist")
model: str = Field(default="gpt-4", description="AI model to use")
model: Optional[str] = Field(
default=None, description="LiteLLM model string to use; defaults to the server-configured model"
)
min_tracks: int = Field(default=30, ge=1, le=100, description="Minimum number of tracks")
max_tracks: int = Field(default=50, ge=1, le=200, description="Maximum number of tracks")

Expand Down
Loading
Loading