Files
manage/backend
Developer 84dcf9e010 fix: resolve Jellyfin usernames to internal Id and harden qBittorrent login
Jellyfin: get_user_id() returned the configured user_id verbatim, so a
username like "admin" hit /Users/admin/Views and got HTTP 400 ("The value
'admin' is not valid."). The index worker already had username->Id
resolution, but the live API paths (dashboard counts, media query) did not.
Route all user-scoped paths through the new JellyfinClient.resolve_user_id()
(exact Id match -> Name match -> first user), cached per service/credentials
in get_user_id() so repeated requests don't re-list users. The worker is
simplified to call the same method.

qBittorrent: _login() raised "qBittorrent login failed: " (empty) on a 200
with an empty body, which happens when base_url doesn't reach the qBittorrent
login handler (wrong URL/path or a reverse proxy misroute) — not a credentials
issue. Now accepts the SID cookie as a success signal (reverse proxies that
mangle the body), returns a clear "invalid username or password" for "Fails.",
and surfaces a diagnostic error (HTTP status + body + base_url/proxy hint) for
any other/empty body.

Tests: new tests/test_jellyfin_client.py (5) + 3 qBittorrent login tests.
Full backend suite (384) passes; ruff clean.
2026-07-11 11:21:31 +00:00
..
2026-05-07 15:17:31 +02:00

Manage Backend API

FastAPI backend serving the REST API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access for Manage.

Project structure

backend/
├── pyproject.toml
├── README.md
├── src/
│   └── media_library_viewer_api/
│       ├── __init__.py
│       ├── main.py              # FastAPI app entrypoint
│       ├── config.py            # pydantic-settings config
│       ├── auth.py              # OIDC/JWT + API key auth
│       ├── dependencies.py      # Dependency injection
│       ├── observability.py     # Prometheus metrics + request IDs
│       ├── logging_utils.py     # Structured JSON/text logging
│       ├── path_utils.py        # Jellyfin→SSH path resolution
│       ├── jobs.py              # Job templates
│       ├── utils.py             # Formatting helpers
│       ├── routers/
│       │   ├── backups.py
│       │   ├── dashboard.py
│       │   ├── files.py
│       │   ├── jobs.py
│       │   ├── media.py
│       │   ├── monitoring.py
│       │   ├── services.py
│       │   ├── settings.py
│       │   ├── tasks.py
│       │   ├── users.py (+ users_impl.py)
│       │   └── widgets.py
│       ├── clients/
│       │   ├── jellyfin.py
│       │   ├── jellyseerr.py
│       │   ├── local.py
│       │   └── ssh.py
│       ├── integrations/        # Service-registry definitions
│       ├── domain/
│       │   └── media.py
│       ├── models/              # Pydantic request/response models
│       ├── services/
│       │   ├── media_index.py (+ _impl.py)
│       │   ├── settings_store.py
│       │   ├── secrets.py       # Fernet encryption at rest
│       │   ├── targets.py       # Node Exporter target discovery
│       │   ├── task_runner.py
│       │   ├── mail_queue.py (+ mailer.py/_impl.py)
│       │   ├── backup_alert_engine.py (+ backup_poller.py)
│       │   ├── known_hosts.py
│       │   └── db_maintenance.py
│       ├── widgets/             # Widget sources (dashboard data adapters)
│       └── workers/             # Background workers (media index)
└── tests/

Setup

cd backend
python -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'

Configuration

Set environment variables directly in your shell or a wrapper script before running the app or Compose. The Docker Compose files use interpolation and do not require an env_file entry.

For local .env development, you can still create one if you prefer, but it is optional.

# Optional backend logging level
LOG_LEVEL=INFO

# Optional SMTP settings for the Users -> message popup
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=your-smtp-username
SMTP_PASSWORD=your-smtp-password
SMTP_FROM_ADDRESS=no-reply@example.com
SMTP_FROM_NAME=Manage
SMTP_USE_TLS=true
SMTP_USE_SSL=false
SMTP_TIMEOUT=30

# Jellyfin, Jellyseerr, and SSH targets are now configured per machine in the app's Settings tab.
# The backend seeds a local machine automatically, so no global Jellyfin or SSH env vars are required.

# Versioning
# The backend tries to auto-detect its version from installed package metadata.
# If needed, you can override the displayed version/build markers with APP_VERSION and APP_BUILD_INFO.
APP_VERSION=0.1.0
APP_BUILD_INFO=dev

# Authentik / OIDC
AUTH_ENABLED=true
OIDC_ISSUER_URL=https://authentik.example/application/o/media-library-viewer/
OIDC_AUDIENCE=media-library-viewer
OIDC_JWKS_URL=
OIDC_CLOCK_SKEW_SECONDS=30

Running

cd backend
uvicorn media_library_viewer_api.main:app --reload --port 8000

Or with PYTHONPATH if not installed:

PYTHONPATH=src uvicorn media_library_viewer_api.main:app --reload --port 8000

API docs available at: http://localhost:8000/docs

Docker

The repository root includes a production docker-compose.yml and a development docker-compose.dev.yml. The backend media index is stored in the backend_cache Docker volume so it survives container restarts and image rebuilds. The production compose file expects required environment variables to be supplied via interpolation (shell exports or inline VAR=value docker compose ...).

Configuration workflow examples

  1. Export your runtime variables before launching Compose:
export BACKEND_APP_HOST=manage.example.com
export FRONTEND_APP_HOST=manage.example.com
export CERT_RESOLVER=letsencrypt
export VITE_OIDC_ISSUER=https://authentik.example/application/o/manage/
export VITE_OIDC_CLIENT_ID=manage
export VITE_OIDC_REDIRECT_URI=https://manage.example.com/
export VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://manage.example.com/

docker compose up --build
  1. After the API is running, open the app, go to Settings, and add machine entries:

    • Local: monitors the API host itself without SSH.
    • SSH: monitors another machine using a host, username, and a private key pasted directly into the machine settings, with an optional passphrase.
    • The machine editor groups Connection, Monitoring / Files, Jellyfin, Jellyseerr, and Notes under separate headings so each service area is easier to scan.
    • Saving a monitoring machine validates the banner/auth flow and records the first trusted host key into the backend-managed known_hosts file.
    • If the host cannot be reached or authenticated, the save flow surfaces the SSH error directly in the dialog.
    • Use Validate SSH + trust host in the machine editor before saving if you want to test the banner/auth flow explicitly.
    • The first successful SSH connection uses trust-on-first-use: the backend records that machine's host key into its managed known_hosts file automatically, then continues verifying it strictly on later connects.
  2. Open Observability to see Alertmanager alerts, Prometheus scrape targets, and Grafana deep-links for configured machines. Alertmanager, Grafana, and Prometheus are configured as service instances on the Services page; system metrics (disk, CPU, memory) are owned by the external observability stack (Prometheus + node_exporter + Grafana), not by the Manage backend.

For local development, docker compose -f docker-compose.dev.yml up --build does not require an SSH key unless you configure remote SSH machines in the Settings tab.

API Endpoints

  • GET /api/dashboard/counts — Movie/series/episode totals
  • GET /api/dashboard/libraries — Per-library breakdown
  • GET /api/dashboard/now-playing — Active playback sessions
  • GET /api/monitoring/machines — Persistent monitoring machine definitions
  • GET /api/monitoring/prometheus-targets — Prometheus scrape targets for remote Node Exporters (consumed by external Prometheus via http_sd_configs)
  • GET /api/monitoring/alerts — Active Alertmanager alerts summary (resolves the configured alertmanager service)
  • GET /api/monitoring/alertmanager-status — Alertmanager cluster/status
  • GET /api/monitoring/grafana-status — Grafana service health
  • GET /api/monitoring/prometheus-status — Prometheus service health
  • POST /api/monitoring/alertmanager-webhook — Receive Alertmanager webhooks (log-only)
  • GET /api/settings/machines — Manage machine definitions
  • GET /api/media/status — Index status
  • POST /api/media/build — Rebuild index
  • GET /api/media/query — Query with filters/sort/pagination
  • GET /api/files/list?path= — Directory listing
  • GET /api/files/ffprobe?path= — ffprobe JSON
  • GET /api/files/stat?path= — stat output
  • GET /api/files/resolve-path?path= — Path resolution
  • GET /api/jobs/templates — Available jobs
  • POST /api/jobs/run — Execute a job
  • GET /api/users — Jellyfin users with optional Jellyseerr enrichment
  • GET /api/version — Backend version/build metadata for the UI