From 2156bb322654323f8f6e25acb1a86fc0b1386938 Mon Sep 17 00:00:00 2001 From: Marek Lenczewski Date: Wed, 8 Apr 2026 19:15:34 +0200 Subject: [PATCH] update --- CLAUDE.md | 129 +++++++++++++++++++++++------------- structure.md => database.md | 0 2 files changed, 82 insertions(+), 47 deletions(-) rename structure.md => database.md (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 284d20a..88b71d3 100644 --- a/CLAUDE.md +++ b/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,58 +66,72 @@ 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 - docker-compose.yml — Service-Definition, Port 8000, Volume /videos - .dockerignore — videos/, __pycache__/ - .gitignore — videos/, __pycache__/ + 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 + .gitignore — .gradle/, build/, .idea/, local.properties frontend/src/main/java/com/youtubeapp/ - MainActivity.kt — Einstiegspunkt - data/ — Video, Profile, ApiClient, VideoApi, VideoRepository, LocalStorageService - ui/screens/ — AllVideos, Downloaded, VideoDetail, VideoPlayer - ui/components/ — VideoCard - ui/viewmodel/ — VideoViewModel (inkl. WebSocket-Client) - ui/navigation/ — AppNavigation, Routes - ui/theme/ — Theme (Dark) + MainActivity.kt — Einstiegspunkt + data/ — Video, Profile, ApiClient, VideoApi, VideoRepository, LocalStorageService + ui/screens/ — AllVideos, Downloaded, VideoDetail, VideoPlayer + ui/components/ — VideoCard + ui/viewmodel/ — VideoViewModel (inkl. WebSocket-Client) + ui/navigation/ — AppNavigation, Routes + ui/theme/ — Theme (Dark) frontend/src/main/res/ layout/player_view.xml — PlayerView mit TextureView fuer TV-Kompatibilitaet drawable/tv_banner.png — Android TV Launcher-Banner @@ -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 +``` diff --git a/structure.md b/database.md similarity index 100% rename from structure.md rename to database.md