This commit is contained in:
Marek Lenczewski
2026-04-08 19:15:34 +02:00
parent a0c8ecaf27
commit 2156bb3226
2 changed files with 82 additions and 47 deletions

129
CLAUDE.md
View File

@@ -9,6 +9,7 @@ Selbst-gehostete Anwendung: YouTube-Videos per Browser Extension erfassen, auf e
- `features.md` — Konkrete Features und Benutzerinteraktionen - `features.md` — Konkrete Features und Benutzerinteraktionen
- `architecture.md` — Detaillierter Systemaufbau (Endpoints, Services, Models, Screens) - `architecture.md` — Detaillierter Systemaufbau (Endpoints, Services, Models, Screens)
- `szenarios.md` — Benutzer-Szenarien - `szenarios.md` — Benutzer-Szenarien
- `modules.md` — Modul-Aufteilung pro Komponente
## Architektur ## Architektur
@@ -16,31 +17,45 @@ Drei Komponenten:
### Browser Extension (`browser_extension/`) ### Browser Extension (`browser_extension/`)
- **Manifest V2**, Firefox-kompatibel (`browser.*` API) - **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-rich-item-renderer` (Homepage, Abos, Kanalseiten)
- `ytd-video-renderer` (Suchergebnisse) - `ytd-video-renderer` (Suchergebnisse)
- IntersectionObserver (threshold 50%) — nur sichtbare Cards erfassen - IntersectionObserver (threshold 50%) — nur sichtbare Cards erfassen
- MutationObserver registriert neue Cards beim IntersectionObserver - MutationObserver registriert neue Cards beim IntersectionObserver
- `yt-navigate-finish` Event-Listener fuer SPA-Navigation - `yt-navigate-finish` Event-Listener fuer SPA-Navigation
- Deduplizierung ueber `sentUrls` Set, wird bei Navigation geleert - 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`) - Selektoren ohne Klassen: nur Tags (`h3`, `img`) und Attribute (`href`, `src`)
- `background.js` — empfaengt Batch vom Content Script, sendet POST an Server - `api/background.js` — empfaengt `{profileId, video}` vom Content Script, baut URL `/profiles/{profileId}/videos` und sendet POST
- `popup.html/popup.js` — Profil-Auswahl (holt Profile vom Server, speichert in browser.storage.local) - `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` - Laden via `about:debugging#/runtime/this-firefox` → "Temporaeres Add-on laden" → `manifest.json`
### Server (`backend/`) ### Server (`backend/`)
- **Python, FastAPI, SQLAlchemy, SQLite** (`videos/youtubeapp.db`) - **Python, FastAPI, SQLAlchemy, SQLite** (`videos/youtubeapp.db`)
- **yt-dlp + ffmpeg + Deno** fuer Video-Download und Streaming - **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 - Dockerisiert: `docker compose up --build -d` im `backend/` Verzeichnis
- Laeuft auf `http://localhost:8000` - Laeuft auf `http://localhost:8000`
- Download-Service speichert Videos unter `/videos/{id}.mp4` - Modulare Ordnerstruktur (`database/`, `model/`, `api/`, `download/`, `stream/`, `notify/`, `base/`) mit `__init__.py`-Markern
- Stream-Service: heruntergeladene Videos von Datei, sonst ffmpeg Live-Muxing von Video+Audio mit gleichzeitigem Streaming und Speichern - Entrypoint: `base.app:app` (im Dockerfile als `uvicorn`-Target)
- Dedup: beim Batch-Import wird bestehender Eintrag mit gleicher Video-ID geloescht und neu eingefuegt - **Active Record Pattern**: DB-Methoden leben als Klassenmethoden auf den Models, nicht in einem Service-Layer
- Sortierung: nach ID absteigend (erstes Video im Batch bekommt hoechste ID) - 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
- Profile: fest in DB definiert, Videos ueber Many-to-Many zugeordnet - Stream-Service: `streamAndSave` ist ein Generator, der `streamVideoLive` (ffmpeg Live-Muxing) umhuellt und am Ende `Video.updateFilePath` setzt
- Nach lokalem Download wird die Server-Datei geloescht (file_path auf null) - **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/`) ### App (`app/`)
- **Kotlin, Jetpack Compose**, Android/Android TV - **Kotlin, Jetpack Compose**, Android/Android TV
@@ -51,58 +66,72 @@ Drei Komponenten:
- Navigation mit TopBar (Profil-Auswahl, Aufraeumen-Icon) und Bottom Bar, Dark Theme - Navigation mit TopBar (Profil-Auswahl, Aufraeumen-Icon) und Bottom Bar, Dark Theme
- Profil-Auswahl wird in SharedPreferences persistiert, filtert Videos nach Profil - 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 - 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) - 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`) - 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 - Emulator: Android Studio → Device Manager → Pixel 7a, API 36
- **camelCase** Felder durchgaengig (`thumbnailUrl`, `youtubeUrl`, `isDownloaded`, `profileIds`)
## API Endpoints ## API Endpoints
Profil-bezogene Routen liegen in `api/profile_controller.py`, video-Aktionen in `api/video_controller.py`.
- `GET /profiles` — alle Profile abrufen - `GET /profiles` — alle Profile abrufen
- `POST /videos` — Video-Batch von Extension empfangen (Dedup, Reverse-Insert, Profil-Zuordnung, WebSocket-Benachrichtigung) - `POST /profiles/{profileId}/videos` — Einzelnes Video von Extension empfangen (Dedup pro Profil, WebSocket-Benachrichtigung). Antwort: `204 No Content`
- `GET /videos` alle Videos abrufen (optional `?profile_id=X`, sortiert nach ID absteigend) - `GET /profiles/{profileId}/videos` — Videos eines Profils, sortiert nach ID absteigend
- `GET /videos/downloaded` — heruntergeladene Videos abrufen (optional `?profile_id=X`) - `POST /profiles/{profileId}/videos/cleanup` — Videos des Profils loeschen (Body: `{"excludeIds": [...]}` fuer lokal gespeicherte Ausnahmen)
- `DELETE /videos?profile_id=X&exclude_ids=` — Videos des Profils loeschen (ausser lokal gespeicherte) - `POST /videos/{id}/download` — Download auf Server triggern (Background-Thread via `downloadAsync`)
- `POST /videos/{id}/download` — Download auf Server triggern - `GET /videos/{id}/stream` — Video streamen (von Datei oder Live-Muxing via `streamAndSave`)
- `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 (`Video.getValidPath` korrigiert verwaiste DB-Eintraege)
- `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 (`Video.deleteServerFile`)
- `DELETE /videos/{id}/file` — Server-Datei loeschen (nach lokalem Download) - `WS /ws` — WebSocket, sendet eine einzelne `profileId` als Text bei neuen Videos
- `WS /ws` — WebSocket, sendet Profile-IDs bei neuen Videos
JSON-Wire-Format ist durchgaengig **camelCase** (Backend, Extension, App).
## Projektstruktur ## Projektstruktur
``` ```
backend/ backend/
main.py — FastAPI App, CORS, Startup, Seed-Profile, WebSocket base/
database.py — SQLAlchemy Engine, Session, Base app.py — FastAPI App, CORS, Startup, Seed-Profile, Router-Includes
models.py — Video, Profile, video_profiles (Many-to-Many) database/
schemas.py — Pydantic Schemas (VideoCreate, VideoResponse, ProfileResponse) database.py — SQLAlchemy Engine, SessionLocal, Base, getDb, DbSession-Alias
routes/videos.py — Video- und Profil-Routen model/
services/ profile.py — Profile-Klasse, getAll() liefert list[dict]
video_service.py — CRUD-Operationen, Dedup, Profil-Filter video.py — Video-Klasse mit Properties (isDownloaded, profileIds) und Klassenmethoden (deleteIfExists, create, getAll, getById, updateFilePath, getValidPath, deleteServerFile, deleteNotDownloaded)
download_service.py — yt-dlp Download profile_video.py — videoProfiles M:N-Tabelle
stream_service.py — ffmpeg Live-Muxing + Streaming api/
Dockerfile — Python 3.12 + ffmpeg + Deno schemas.py — Pydantic Schemas (VideoCreate, VideoResponse, CleanupRequest)
docker-compose.yml — Service-Definition, Port 8000, Volume /videos profile_controller.py — /profiles und profil-scoped Video-Routen
.dockerignore videos/, __pycache__/ video_controller.pyVideo-Aktionen (download, stream, file, deleteFile)
.gitignore — videos/, __pycache__/ 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/ browser_extension/
manifest.json — Manifest V2, Permissions, browser_action, storage manifest.json — Manifest V2, Permissions, browser_action, storage; verweist auf tracking/, api/, config/
content.js — DOM-Extraktion + IntersectionObserver + Batch-Versand mit Profil tracking/content.js — DOM-Extraktion + IntersectionObserver + Einzel-Send mit Profil
background.js — Batch-POST an Server api/background.js — Einzel-POST an /profiles/{profileId}/videos
popup.html — Profil-Auswahl UI config/popup.html — Profil-Auswahl UI
popup.js — Profile laden, Auswahl speichern config/popup.js — Profile laden, Auswahl speichern
app/ app/
.gitignore — .gradle/, build/, .idea/, local.properties .gitignore — .gradle/, build/, .idea/, local.properties
frontend/src/main/java/com/youtubeapp/ frontend/src/main/java/com/youtubeapp/
MainActivity.kt — Einstiegspunkt MainActivity.kt — Einstiegspunkt
data/ — Video, Profile, ApiClient, VideoApi, VideoRepository, LocalStorageService data/ — Video, Profile, ApiClient, VideoApi, VideoRepository, LocalStorageService
ui/screens/ — AllVideos, Downloaded, VideoDetail, VideoPlayer ui/screens/ — AllVideos, Downloaded, VideoDetail, VideoPlayer
ui/components/ — VideoCard ui/components/ — VideoCard
ui/viewmodel/ — VideoViewModel (inkl. WebSocket-Client) ui/viewmodel/ — VideoViewModel (inkl. WebSocket-Client)
ui/navigation/ — AppNavigation, Routes ui/navigation/ — AppNavigation, Routes
ui/theme/ — Theme (Dark) ui/theme/ — Theme (Dark)
frontend/src/main/res/ frontend/src/main/res/
layout/player_view.xml — PlayerView mit TextureView fuer TV-Kompatibilitaet layout/player_view.xml — PlayerView mit TextureView fuer TV-Kompatibilitaet
drawable/tv_banner.png — Android TV Launcher-Banner drawable/tv_banner.png — Android TV Launcher-Banner
@@ -118,6 +147,12 @@ app/
- IntersectionObserver statt blindem Scan — nur sichtbare Videos erfassen - IntersectionObserver statt blindem Scan — nur sichtbare Videos erfassen
- ffmpeg Live-Muxing statt komplettem Download vor dem Abspielen - ffmpeg Live-Muxing statt komplettem Download vor dem Abspielen
- Deno als JavaScript-Runtime fuer yt-dlp — YouTube erfordert JS-Ausfuehrung zur URL-Extraktion - 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 - 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 - Sprache der Dokumentation: Deutsch
```