# YouTube App Selbst-gehostete Anwendung: YouTube-Videos per Browser Extension erfassen, auf einem Server speichern und per Kotlin-App auf Android/Android TV streamen und herunterladen. ## Dokumentation - `systems.md` — Technologie-Stack pro Komponente - `commication.md` — Kommunikationsflüsse zwischen den Systemen - `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 Drei Komponenten: ### Browser Extension (`browser_extension/`) - **Manifest V2**, Firefox-kompatibel (`browser.*` API) - 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 - **Einzelversand**: jedes sichtbare Video sendet sofort einen separaten POST (kein Batching mehr) - Selektoren ohne Klassen: nur Tags (`h3`, `img`) und Attribute (`href`, `src`) - `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 (sendet einzelne `profileId`) - Dockerisiert: `docker compose up --build -d` im `backend/` Verzeichnis - Laeuft auf `http://localhost:8000` - 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 - Gradle-Projekt, Modul `frontend` - Screens: AllVideos (Grid), Downloaded (lokal verfuegbare Videos), VideoDetail, VideoPlayer - Retrofit fuer API-Calls, Coil fuer Thumbnails, ExoPlayer fuer Streaming - OkHttp WebSocket-Client — automatisches Neuladen bei neuen Videos - 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 /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/ 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; 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 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) frontend/src/main/res/ layout/player_view.xml — PlayerView mit TextureView fuer TV-Kompatibilitaet drawable/tv_banner.png — Android TV Launcher-Banner ``` ## Entscheidungen - Kein Jellyfin — erfuellt nicht die Anforderung, Videos vor dem Download aufzulisten - Kein PostgreSQL/MySQL — SQLite reicht fuer den Prototyp - Profile fest in DB — kein UI zum Erstellen/Loeschen, werden direkt in der Datenbank verwaltet - Server-Datei wird nach lokalem Download geloescht — spart Speicherplatz auf dem Server - DOM-Extraktion statt ytInitialData-Parsing — funktioniert auch bei SPA-Navigation und Scrollen - 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 (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 ```