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