Files
streamrip-ui/docs/superpowers/plans/2026-09-24-streamrip-ui.md
T

8.3 KiB

streamrip-ui Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Webapp (Next.js + FastAPI) für streamrip (nur Deezer, lokal Single-User) mit Suche über alle 4 Medientypen, Download-Dialog mit Defaults, echter Fortschritt via SSE, Verlauf mit Re-Download, volle config.toml-Verwaltung, Dateibrowser.

Architecture: FastAPI-Backend importiert streamrip als Library (kein Subprocess, umgeht kaputten pipx-Shim); Next.js-Frontend redet nur mit Backend-REST/SSE. Jobs laufen als asyncio-Tasks mit In-Memory-Store.

Tech Stack: Python 3.14 + FastAPI + streamrip 2.2.0 (Library); Next.js 14 + React + next/image; SSE für Fortschritt; Docker-Compose für Betrieb.

Spec: Grill-Ergebnisse dieser Session (Stack Next.js+Python API; Quelle nur Deezer; Vollausbau; lokal Single-User; alle 4 Suchtypen; Download-Dialog mit Defaults aus Einstellungen; echter Fortschritt; volle config.toml; Cover-Grid + Preview-Player; Defaults FLAC+Fallback; Verlauf mit Re-Download).

Global Constraints

  • Nur Quelle deezer (Qobuz/Tidal/SoundCloud ausgeblendet, Code aber erweiterbar).
  • Single-User lokal, kein Auth.
  • Download-Ordner Default ~/StreamripDownloads, Qualität Default FLAC (2) + Fallback.
  • Backend nutzt /usr/bin/rip-Umgebung bzw. direkten Library-Import (nicht ~/.local/bin/rip).

Review Focus

  • Suche mit leerem Query darf Backend nicht crashen (400 erwartet).
  • Download mit ungültiger ID/Type gibt 422 mit klarer Message.
  • config.toml-Write erstellt Backup und validiert TOML vor dem Schreiben.
  • SSE-Stream bricht sauber ab wenn Job-ID unbekannt (404, kein Hängen).
  • Dateibrowser kann nicht aus Download-Root ausbrechen (Path-Traversal → 403).

Task 1: Backend-Gerüst + Health

Files:

  • Create: backend/requirements.txt
  • Create: backend/app/main.py
  • Create: backend/app/core/settings.py
  • Test: backend/tests/test_health.py

Interfaces:

  • Consumes: nichts

  • Produces: GET /api/health -> {status: ok}, GET /api/sources -> [deezer]

  • Step 1: Write the failing test

from fastapi.testclient import TestClient
from app.main import app

def test_health():
    c = TestClient(app)
    r = c.get("/api/health")
    assert r.status_code == 200
    assert r.json()["status"] == "ok"
  • Step 2: Run test to verify it fails

Run: cd backend && python3 -m pytest tests/test_health.py -v Expected: FAIL with "No module named 'app'" / collection error

  • Step 3: Write minimal implementation

requirements: fastapi, uvicorn, pydantic, tomlkit, aiofiles, httpx, pytest main.py: FastAPI-App mit /api/health + /api/sources settings.py: DOWNLOAD_DIR, CONFIG_PATH, DEFAULT_QUALITY=2

  • Step 4: Run test to verify it passes

Run: cd backend && python3 -m pytest tests/ -v Expected: PASS

  • Step 5: Commit
git add -A && git commit -m "feat: backend scaffold with health check" || true

Task 2: Suche (alle 4 Typen)

Files:

  • Create: backend/app/api/search.py
  • Create: backend/app/services/streamrip_service.py
  • Modify: backend/app/main.py
  • Test: backend/tests/test_search.py

Interfaces:

  • Consumes: settings.py

  • Produces: GET /api/search?type=track|album|artist|playlist&q=..&limit=.. -> {results: [{source, media_type, id, title, artist, album, cover, preview_url, duration, desc}]}

  • Step 1: Write the failing test

def test_search_validation(client):
    assert client.get("/api/search", params={"type": "track", "q": ""}).status_code == 400
    assert client.get("/api/search", params={"type": "foo", "q": "x"}).status_code == 422
  • Step 2: Run test to verify it fails

Run: cd backend && python3 -m pytest tests/test_search.py -v Expected: FAIL (route missing → 404)

  • Step 3: Write minimal implementation

streamrip_service.search(source="deezer", media_type, query, limit): nutzt DeezerClient.search() + SearchResults.from_pages(), mappt Summary-Felder + Roh-Item (cover, preview, duration) durch. search.py: validiert type/q/limit, 400 bei leerem q.

  • Step 4: Run test to verify it passes

Run: cd backend && python3 -m pytest tests/ -v Expected: PASS (Service per Mock getestet, kein Netzwerk nötig)

  • Step 5: Commit
git add -A && git commit -m "feat: deezer search endpoint" || true

Task 3: Download-Queue + SSE-Fortschritt

Files:

  • Create: backend/app/api/downloads.py
  • Create: backend/app/core/jobstore.py
  • Modify: backend/app/main.py
  • Test: backend/tests/test_downloads.py

Interfaces:

  • Consumes: streamrip_service.download_by_id()

  • Produces: POST /api/downloads -> {job_id}, GET /api/downloads, GET /api/downloads/{id}, GET /api/downloads/{id}/events (SSE)

  • Step 1: Write the failing test

def test_create_download_validation(client):
    r = client.post("/api/downloads", json={"media_type": "nope", "id": "x"})
    assert r.status_code == 422
  • Step 2: Run test to verify it fails

Run: cd backend && python3 -m pytest tests/test_downloads.py -v Expected: FAIL (404 route missing)

  • Step 3: Write minimal implementation

jobstore: In-Memory-Dict {job_id: {status, progress, logs, params}}, asyncio-Queue (sequenziell, max 1 parallel weil streamrip concurrency selbst regelt). Worker: Main.add_by_id() → resolve() → download(), fängt Exceptions → status failed + failed_downloads.db-Eintrag sichtbar. SSE: polling-basiert (0.5s) mit data: {json}.

  • Step 4: Run test to verify it passes

Run: cd backend && python3 -m pytest tests/ -v Expected: PASS

  • Step 5: Commit
git add -A && git commit -m "feat: download queue with SSE progress" || true

Task 4: Verlauf + Config + Dateien

Files:

  • Create: backend/app/api/history.py
  • Create: backend/app/api/config.py
  • Create: backend/app/api/files.py
  • Test: backend/tests/test_history_config_files.py

Interfaces:

  • Consumes: settings (DB paths, config path, download dir)

  • Produces: GET /api/history, POST /api/history/redownload, GET/PUT /api/config, GET /api/files?path=..

  • Step 1: Write the failing test

def test_path_traversal_blocked(client):
    r = client.get("/api/files", params={"path": "../../etc"})
    assert r.status_code == 403
  • Step 2: Run test to verify it fails

Run: cd backend && python3 -m pytest tests/test_history_config_files.py -v Expected: FAIL

  • Step 3: Write minimal implementation

history: liest downloads.db/failed_downloads.db via streamrip.db-Klassen (read-only). config: GET gibt TOML-Text + geparste Deezer-Sektion; PUT validiert TOML, schreibt Backup config.toml.bak. files: listet Download-Root, blockt ..-Escape.

  • Step 4: Run test to verify it passes

Run: cd backend && python3 -m pytest tests/ -v Expected: PASS

  • Step 5: Commit
git add -A && git commit -m "feat: history, config, files endpoints" || true

Task 5: Frontend (Next.js)

Files:

  • Create: frontend/package.json, frontend/next.config.js, frontend/app/layout.tsx, frontend/app/page.tsx (Suche mit 4 Tabs, Cover-Grid, Preview-Audio), frontend/components/DownloadDialog.tsx, frontend/app/queue/page.tsx (SSE), frontend/app/history/page.tsx, frontend/app/settings/page.tsx, frontend/app/files/page.tsx

Interfaces:

  • Consumes: alle Backend-Endpoints

  • Produces: UI

  • Step 1: Write failing check

Run: cd frontend && npm run build Expected: FAIL (noch kein Scaffold)

  • Step 2: Scaffold
cd frontend && npm init -y && npm i next@14 react react-dom && npm i -D typescript @types/react @types/node
  • Step 3: Implement minimal pages (Suche → Dialog → Queue per SSE → Rest)
  • Step 4: Verify

Run: cd frontend && npm run build Expected: PASS (exit 0)

  • Step 5: Commit
git add -A && git commit -m "feat: next.js frontend" || true

Task 6: Compose + README + E2E-Smoke

Files:

  • Create: docker-compose.yml, README.md

  • Step 1: Backend-Tests grün: cd backend && python3 -m pytest tests/ -v

  • Step 2: Backend-Boot: cd backend && python3 -m uvicorn app.main:app --port 8000 & + curl localhost:8000/api/health

  • Step 3: Frontend-Build grün: cd frontend && npm run build

  • Step 4: Commit