9.9 KiB
9.9 KiB
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 Komponentecommication.md— Kommunikationsflüsse zwischen den Systemenfeatures.md— Konkrete Features und Benutzerinteraktionenarchitecture.md— Detaillierter Systemaufbau (Endpoints, Services, Models, Screens)szenarios.md— Benutzer-Szenarienmodules.md— Modul-Aufteilung pro Komponente
Architektur
Drei Komponenten:
Browser Extension (browser_extension/)
- Manifest V2, Firefox-kompatibel (
browser.*API) - Modulare Ordnerstruktur (
tracking/,api/,config/);manifest.jsonbleibt 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-finishEvent-Listener fuer SPA-Navigation- Deduplizierung ueber
sentUrlsSet, 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}/videosund sendet POSTconfig/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 einzelneprofileId) - Dockerisiert:
docker compose up --build -dimbackend/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 alsuvicorn-Target) - Active Record Pattern: DB-Methoden leben als Klassenmethoden auf den Models, nicht in einem Service-Layer
- Download-Service:
downloadVideoruft yt-dlp mit--force-overwrites --no-continue, prueft Datei-Existenz und Mindestgroesse, dannVideo.updateFilePath.downloadAsyncstartet das im Hintergrund-Thread - Stream-Service:
streamAndSaveist ein Generator, derstreamVideoLive(ffmpeg Live-Muxing) umhuellt und am EndeVideo.updateFilePathsetzt - 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 viaColumn("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)]indatabase/database.pydefiniert; Routen schreiben nurdb: 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 abrufenPOST /profiles/{profileId}/videos— Einzelnes Video von Extension empfangen (Dedup pro Profil, WebSocket-Benachrichtigung). Antwort:204 No ContentGET /profiles/{profileId}/videos— Videos eines Profils, sortiert nach ID absteigendPOST /profiles/{profileId}/videos/cleanup— Videos des Profils loeschen (Body:{"excludeIds": [...]}fuer lokal gespeicherte Ausnahmen)POST /videos/{id}/download— Download auf Server triggern (Background-Thread viadownloadAsync)GET /videos/{id}/stream— Video streamen (von Datei oder Live-Muxing viastreamAndSave)GET /videos/{id}/file— Video-Datei zum Download auf Client ausliefern (Video.getValidPathkorrigiert verwaiste DB-Eintraege)DELETE /videos/{id}/file— Server-Datei loeschen (Video.deleteServerFile)WS /ws— WebSocket, sendet eine einzelneprofileIdals 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
notifyClientsundVideo.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 —
profileIdin 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