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
|
- `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,47 +66,61 @@ 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)
|
||||||
|
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
|
docker-compose.yml — Service-Definition, Port 8000, Volume /videos
|
||||||
.dockerignore — videos/, __pycache__/
|
.dockerignore — videos/, __pycache__/
|
||||||
.gitignore — 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
|
||||||
@@ -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
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user