Files
youtube-app/CLAUDE.md
Marek Lenczewski 2156bb3226 update
2026-04-08 19:15:34 +02:00

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 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