update
This commit is contained in:
107
CLAUDE.md
107
CLAUDE.md
@@ -9,6 +9,7 @@ Selbst-gehostete Anwendung: YouTube-Videos per Browser Extension erfassen, auf e
|
||||
- `features.md` — Konkrete Features und Benutzerinteraktionen
|
||||
- `architecture.md` — Detaillierter Systemaufbau (Endpoints, Services, Models, Screens)
|
||||
- `szenarios.md` — Benutzer-Szenarien
|
||||
- `modules.md` — Modul-Aufteilung pro Komponente
|
||||
|
||||
## Architektur
|
||||
|
||||
@@ -16,31 +17,45 @@ Drei Komponenten:
|
||||
|
||||
### Browser Extension (`browser_extension/`)
|
||||
- **Manifest V2**, Firefox-kompatibel (`browser.*` API)
|
||||
- `content.js` — extrahiert Videodaten direkt aus dem YouTube-DOM:
|
||||
- Modulare Ordnerstruktur (`tracking/`, `api/`, `config/`); `manifest.json` bleibt im Root
|
||||
- `tracking/content.js` — extrahiert Videodaten direkt aus dem YouTube-DOM:
|
||||
- `ytd-rich-item-renderer` (Homepage, Abos, Kanalseiten)
|
||||
- `ytd-video-renderer` (Suchergebnisse)
|
||||
- IntersectionObserver (threshold 50%) — nur sichtbare Cards erfassen
|
||||
- MutationObserver registriert neue Cards beim IntersectionObserver
|
||||
- `yt-navigate-finish` Event-Listener fuer SPA-Navigation
|
||||
- Deduplizierung ueber `sentUrls` Set, wird bei Navigation geleert
|
||||
- Batch-Versand: sammelt sichtbare Videos mit Profil-ID, sendet als Array
|
||||
- **Einzelversand**: jedes sichtbare Video sendet sofort einen separaten POST (kein Batching mehr)
|
||||
- Selektoren ohne Klassen: nur Tags (`h3`, `img`) und Attribute (`href`, `src`)
|
||||
- `background.js` — empfaengt Batch vom Content Script, sendet POST an Server
|
||||
- `popup.html/popup.js` — Profil-Auswahl (holt Profile vom Server, speichert in browser.storage.local)
|
||||
- `api/background.js` — empfaengt `{profileId, video}` vom Content Script, baut URL `/profiles/{profileId}/videos` und sendet POST
|
||||
- `config/popup.html` + `config/popup.js` — Profil-Auswahl (holt Profile vom Server, speichert in browser.storage.local)
|
||||
- Laden via `about:debugging#/runtime/this-firefox` → "Temporaeres Add-on laden" → `manifest.json`
|
||||
|
||||
### Server (`backend/`)
|
||||
- **Python, FastAPI, SQLAlchemy, SQLite** (`videos/youtubeapp.db`)
|
||||
- **yt-dlp + ffmpeg + Deno** fuer Video-Download und Streaming
|
||||
- **WebSocket** (`/ws`) — benachrichtigt verbundene Clients bei neuen Videos
|
||||
- **WebSocket** (`/ws`) — benachrichtigt verbundene Clients bei neuen Videos (sendet einzelne `profileId`)
|
||||
- Dockerisiert: `docker compose up --build -d` im `backend/` Verzeichnis
|
||||
- Laeuft auf `http://localhost:8000`
|
||||
- Download-Service speichert Videos unter `/videos/{id}.mp4`
|
||||
- Stream-Service: heruntergeladene Videos von Datei, sonst ffmpeg Live-Muxing von Video+Audio mit gleichzeitigem Streaming und Speichern
|
||||
- Dedup: beim Batch-Import wird bestehender Eintrag mit gleicher Video-ID geloescht und neu eingefuegt
|
||||
- Sortierung: nach ID absteigend (erstes Video im Batch bekommt hoechste ID)
|
||||
- Profile: fest in DB definiert, Videos ueber Many-to-Many zugeordnet
|
||||
- Nach lokalem Download wird die Server-Datei geloescht (file_path auf null)
|
||||
- Modulare Ordnerstruktur (`database/`, `model/`, `api/`, `download/`, `stream/`, `notify/`, `base/`) mit `__init__.py`-Markern
|
||||
- Entrypoint: `base.app:app` (im Dockerfile als `uvicorn`-Target)
|
||||
- **Active Record Pattern**: DB-Methoden leben als Klassenmethoden auf den Models, nicht in einem Service-Layer
|
||||
- Download-Service: `downloadVideo` ruft yt-dlp mit `--force-overwrites --no-continue`, prueft Datei-Existenz und Mindestgroesse, dann `Video.updateFilePath`. `downloadAsync` startet das im Hintergrund-Thread
|
||||
- Stream-Service: `streamAndSave` ist ein Generator, der `streamVideoLive` (ffmpeg Live-Muxing) umhuellt und am Ende `Video.updateFilePath` setzt
|
||||
- **Profil-scoped Dedup**: pro Profil eine eigene Video-Zeile. `Video.deleteIfExists(db, youtubeUrl, profileId)` loescht nur die Zeile dieses Profils, `Video.create(...)` fuegt eine neue ein. Profile sind unabhaengig.
|
||||
- Sortierung: nach ID absteigend
|
||||
- Profile: fest in DB definiert, Videos ueber Many-to-Many zugeordnet (de facto 1:1 pro Zeile durch das per-Profil-Dedup)
|
||||
- Nach lokalem Download wird die Server-Datei geloescht (`Video.deleteServerFile`)
|
||||
|
||||
#### Naming-Konventionen im Backend
|
||||
- **camelCase** fuer Variablen, Funktionen, Methoden, Pydantic-Felder, Klassenattribute (auch in `Video`-SQLAlchemy-Model — DB-Spaltennamen werden via `Column("snake_case", ...)` angegeben)
|
||||
- **PascalCase** fuer Klassen
|
||||
- **UPPER_SNAKE_CASE** fuer Konstanten (`VIDEOS_DIR`, `CHUNK_SIZE`, `DATABASE_URL`)
|
||||
- **snake_case** bleibt nur fuer DB-Tabellen-/Spaltennamen im SQL und Python-Standardbibliothek
|
||||
- Verstoesst gegen PEP 8, ist vom User explizit so gewuenscht (Hintergrund Symfony/Spring Boot)
|
||||
|
||||
#### DB-Session per FastAPI
|
||||
- `DbSession = Annotated[Session, Depends(getDb)]` in `database/database.py` definiert; Routen schreiben nur `db: DbSession`
|
||||
|
||||
### App (`app/`)
|
||||
- **Kotlin, Jetpack Compose**, Android/Android TV
|
||||
@@ -51,47 +66,61 @@ Drei Komponenten:
|
||||
- Navigation mit TopBar (Profil-Auswahl, Aufraeumen-Icon) und Bottom Bar, Dark Theme
|
||||
- Profil-Auswahl wird in SharedPreferences persistiert, filtert Videos nach Profil
|
||||
- Lokaler Download: Videos und Metadaten werden auf dem Geraet gespeichert, lokal bevorzugt abgespielt, offline verfuegbar
|
||||
- Heruntergeladen-Tab liest aus lokalem Storage, **nicht** vom Server (kein `/downloaded`-Endpoint mehr)
|
||||
- Aufraeumen: loescht alle nicht lokal gespeicherten Videos des Profils (sendet lokale IDs als Ausnahme)
|
||||
- Server-IP konfigurierbar in `ApiClient.kt` (Emulator: `10.0.2.2`, echtes Geraet: `192.168.178.34`)
|
||||
- Emulator: Android Studio → Device Manager → Pixel 7a, API 36
|
||||
- **camelCase** Felder durchgaengig (`thumbnailUrl`, `youtubeUrl`, `isDownloaded`, `profileIds`)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
Profil-bezogene Routen liegen in `api/profile_controller.py`, video-Aktionen in `api/video_controller.py`.
|
||||
|
||||
- `GET /profiles` — alle Profile abrufen
|
||||
- `POST /videos` — Video-Batch von Extension empfangen (Dedup, Reverse-Insert, Profil-Zuordnung, WebSocket-Benachrichtigung)
|
||||
- `GET /videos` — alle Videos abrufen (optional `?profile_id=X`, sortiert nach ID absteigend)
|
||||
- `GET /videos/downloaded` — heruntergeladene Videos abrufen (optional `?profile_id=X`)
|
||||
- `DELETE /videos?profile_id=X&exclude_ids=` — Videos des Profils loeschen (ausser lokal gespeicherte)
|
||||
- `POST /videos/{id}/download` — Download auf Server triggern
|
||||
- `GET /videos/{id}/stream` — Video streamen (von Datei oder progressiver Download via yt-dlp)
|
||||
- `GET /videos/{id}/file` — Video-Datei zum Download auf Client ausliefern, setzt Download-Status zurueck wenn Datei fehlt
|
||||
- `DELETE /videos/{id}/file` — Server-Datei loeschen (nach lokalem Download)
|
||||
- `WS /ws` — WebSocket, sendet Profile-IDs bei neuen Videos
|
||||
- `POST /profiles/{profileId}/videos` — Einzelnes Video von Extension empfangen (Dedup pro Profil, WebSocket-Benachrichtigung). Antwort: `204 No Content`
|
||||
- `GET /profiles/{profileId}/videos` — Videos eines Profils, sortiert nach ID absteigend
|
||||
- `POST /profiles/{profileId}/videos/cleanup` — Videos des Profils loeschen (Body: `{"excludeIds": [...]}` fuer lokal gespeicherte Ausnahmen)
|
||||
- `POST /videos/{id}/download` — Download auf Server triggern (Background-Thread via `downloadAsync`)
|
||||
- `GET /videos/{id}/stream` — Video streamen (von Datei oder Live-Muxing via `streamAndSave`)
|
||||
- `GET /videos/{id}/file` — Video-Datei zum Download auf Client ausliefern (`Video.getValidPath` korrigiert verwaiste DB-Eintraege)
|
||||
- `DELETE /videos/{id}/file` — Server-Datei loeschen (`Video.deleteServerFile`)
|
||||
- `WS /ws` — WebSocket, sendet eine einzelne `profileId` als Text bei neuen Videos
|
||||
|
||||
JSON-Wire-Format ist durchgaengig **camelCase** (Backend, Extension, App).
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
backend/
|
||||
main.py — FastAPI App, CORS, Startup, Seed-Profile, WebSocket
|
||||
database.py — SQLAlchemy Engine, Session, Base
|
||||
models.py — Video, Profile, video_profiles (Many-to-Many)
|
||||
schemas.py — Pydantic Schemas (VideoCreate, VideoResponse, ProfileResponse)
|
||||
routes/videos.py — Video- und Profil-Routen
|
||||
services/
|
||||
video_service.py — CRUD-Operationen, Dedup, Profil-Filter
|
||||
download_service.py — yt-dlp Download
|
||||
stream_service.py — ffmpeg Live-Muxing + Streaming
|
||||
Dockerfile — Python 3.12 + ffmpeg + Deno
|
||||
base/
|
||||
app.py — FastAPI App, CORS, Startup, Seed-Profile, Router-Includes
|
||||
database/
|
||||
database.py — SQLAlchemy Engine, SessionLocal, Base, getDb, DbSession-Alias
|
||||
model/
|
||||
profile.py — Profile-Klasse, getAll() liefert list[dict]
|
||||
video.py — Video-Klasse mit Properties (isDownloaded, profileIds) und Klassenmethoden (deleteIfExists, create, getAll, getById, updateFilePath, getValidPath, deleteServerFile, deleteNotDownloaded)
|
||||
profile_video.py — videoProfiles M:N-Tabelle
|
||||
api/
|
||||
schemas.py — Pydantic Schemas (VideoCreate, VideoResponse, CleanupRequest)
|
||||
profile_controller.py — /profiles und profil-scoped Video-Routen
|
||||
video_controller.py — Video-Aktionen (download, stream, file, deleteFile)
|
||||
download/
|
||||
download_service.py — yt-dlp Download (downloadVideo, downloadAsync)
|
||||
stream/
|
||||
stream_service.py — ffmpeg Live-Muxing (streamVideoLive, streamAndSave)
|
||||
notify/
|
||||
notify_clients.py — WebSocket-Endpoint, connectedClients, notifyClients(profileId), registerWebsocket
|
||||
Dockerfile — Python 3.12 + ffmpeg + Deno, CMD `uvicorn base.app:app`
|
||||
docker-compose.yml — Service-Definition, Port 8000, Volume /videos
|
||||
.dockerignore — videos/, __pycache__/
|
||||
.gitignore — videos/, __pycache__/
|
||||
|
||||
browser_extension/
|
||||
manifest.json — Manifest V2, Permissions, browser_action, storage
|
||||
content.js — DOM-Extraktion + IntersectionObserver + Batch-Versand mit Profil
|
||||
background.js — Batch-POST an Server
|
||||
popup.html — Profil-Auswahl UI
|
||||
popup.js — Profile laden, Auswahl speichern
|
||||
manifest.json — Manifest V2, Permissions, browser_action, storage; verweist auf tracking/, api/, config/
|
||||
tracking/content.js — DOM-Extraktion + IntersectionObserver + Einzel-Send mit Profil
|
||||
api/background.js — Einzel-POST an /profiles/{profileId}/videos
|
||||
config/popup.html — Profil-Auswahl UI
|
||||
config/popup.js — Profile laden, Auswahl speichern
|
||||
|
||||
app/
|
||||
.gitignore — .gradle/, build/, .idea/, local.properties
|
||||
@@ -118,6 +147,12 @@ app/
|
||||
- IntersectionObserver statt blindem Scan — nur sichtbare Videos erfassen
|
||||
- ffmpeg Live-Muxing statt komplettem Download vor dem Abspielen
|
||||
- Deno als JavaScript-Runtime fuer yt-dlp — YouTube erfordert JS-Ausfuehrung zur URL-Extraktion
|
||||
- Videos ohne Profilzuweisung werden automatisch dem Standardprofil zugeordnet
|
||||
- Videos ohne Profilzuweisung werden automatisch dem Standardprofil zugeordnet (Fallback in `notifyClients` und `Video.create`)
|
||||
- Per-Profil-Dedup statt globalem Dedup — verhindert, dass Profile sich gegenseitig Videos loeschen
|
||||
- Einzelversand statt Batches — einfachere Logik, geringfuegig schlechtere Sortier-Garantie wird in Kauf genommen
|
||||
- WebSocket statt Polling — effiziente Echtzeit-Aktualisierung der Videoliste
|
||||
- REST Nested Resources — `profileId` in der URL statt als Query/Body
|
||||
- Active Record statt Service-Layer — DB-Methoden direkt am Model
|
||||
- camelCase statt snake_case im Python-Code — bewusster Verstoss gegen PEP 8 zugunsten der Lesbarkeit fuer den User (Hintergrund Symfony/Spring Boot)
|
||||
- Sprache der Dokumentation: Deutsch
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user