Files
project-kino/nextcloud-import/Plan-Botomir.md
Hermes Agent 884c198783 Import remaining Nextcloud Kino-Projekt material into git
Moves git-compatible content from Nextcloud
(Botomir/Projekte/Angefangen/Kino-Projekt) into this repo per cleanup
request, under nextcloud-import/ to avoid colliding with the actively
maintained backend/frontend/worker layout:

- status/planning docs (Botomir-Status.md, Plan-Botomir.md, Projektbeschreibung.md)
- docs/ (deployment.md, provider-policy.md)
- backend-nextcloud-variant/ (older app/ layout, kept for reference only)
- plugin.video.xstream/, script.module.xstreamscraper/ (vendored Kodi addon source)

A third-party Firefox extension folder was intentionally left in Nextcloud
(unrelated vendor code, not project source).
2026-08-17 10:47:13 +02:00

18 KiB
Raw Permalink Blame History

Kino-Projekt Implementation Plan

For Hermes: Use subagent-driven-development skill to implement this plan task-by-task.

Goal: Eine interne WebUI namens „Kino-Projekt“, mit der YouTube-Videos sowie Links aus legalen Mediatheken/öffentlich erlaubten Quellen geprüft, heruntergeladen/importiert, sauber für Jellyfin abgelegt und anschließend in Jellyfin aktualisiert werden.

Architecture: Das System besteht aus einer Mesh-only WebUI, einem FastAPI-Backend, einer SQLite-Datenbank, einer Job-Queue für Imports und einem Provider-System. Provider sind strikt getrennt: YouTube, Mediathek-/Direktlink-Import, Internet Archive und später weitere legale Quellen. Downloads laufen nur über geprüfte Provider und definierte Zielpfade; Jellyfin wird danach per API aktualisiert.

Tech Stack: Python 3.12+, FastAPI, SQLite, SQLModel oder SQLAlchemy, yt-dlp, httpx, Docker Compose, Jellyfin API, Nginx Proxy Manager/Headscale-Mesh.


Scope

Der Projektname bleibt Kino-Projekt.

Der initiale, rechtlich saubere Scope ist:

  1. YouTube-Videos, Playlists und Kanäle importieren, soweit öffentlich erreichbar und nicht DRM-/Login-/Paywall-geschützt.
  2. Links aus legalen Mediatheken oder erlaubten Quellen hinzufügen, z. B. öffentlich verfügbare ARD/ZDF/Arte/3sat-/Mediathek-URLs, Internet Archive, eigene Download-URLs oder andere Quellen mit klar erlaubter Nutzung.
  3. Downloads in Jellyfin-kompatibler Struktur ablegen.
  4. Jellyfin-Library-Refresh auslösen.
  5. UI zeigt Status, Quelle, Zielpfad und Fehler transparent an.

Non-Goals

Nicht implementieren:

  • Scraping/Download von Piracy-Seiten wie Kinox/Kinoger/xstream-Hostern.
  • Umgehung von DRM, Paywalls, Logins, Geoblocking, Altersprüfungen oder Zugriffsbeschränkungen.
  • Cookie-Import oder Browser-Session-Nutzung als Standardfunktion.
  • Beliebige Shell-Parameter aus der UI.
  • Freie Schreibpfade außerhalb der konfigurierten Medienordner.

Ziel-UX

Die WebUI soll sich wie ein internes „Kino-Dashboard“ anfühlen:

  • URL oder Suchbegriff eingeben.
  • Quelle erkennen: YouTube, Mediathek, Direktlink, Internet Archive.
  • Metadaten anzeigen: Titel, Kanal/Sender, Dauer, Beschreibung, Thumbnail, Quelle.
  • Qualität/Zielbibliothek auswählen.
  • Import starten.
  • Fortschritt beobachten.
  • Nach Abschluss Link zu Jellyfin bzw. Zielpfad anzeigen.

Beispiel-Ziel-URL:

https://kino.dasposchi.de

Die WebUI wird nur über das Headscale-Mesh erreichbar gemacht.


Datenmodell

Tabelle: sources

class Source(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    kind: str  # youtube, mediathek, direct_url, internet_archive
    url: str
    title: str | None = None
    provider: str | None = None
    external_id: str | None = None
    metadata_json: str = "{}"
    created_at: datetime

Tabelle: import_jobs

class ImportJob(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    source_id: int = Field(foreign_key="source.id")
    status: str  # queued, probing, downloading, postprocessing, refreshing, done, failed
    target_library: str
    target_path: str | None = None
    progress: float = 0.0
    error: str | None = None
    created_at: datetime
    updated_at: datetime

Provider-Konzept

Alle Quellen implementieren ein gemeinsames Interface.

class Provider(Protocol):
    name: str

    def can_handle(self, url: str) -> bool: ...

    async def probe(self, url: str) -> MediaMetadata: ...

    async def download(self, url: str, target_dir: Path, options: DownloadOptions) -> DownloadResult: ...

Provider 1: YouTube

  • Erkennung über youtube.com, youtu.be, Playlist-/Channel-URLs.
  • Metadaten via yt-dlp --dump-json.
  • Download via yt-dlp, aber ohne Cookies und ohne Umgehungsoptionen.
  • Qualitätsprofil: maximal 1080p als Default.
  • Für legale Mediathek-Links und direkte Mediendateien.
  • Keine feste Domain-Allowlist, weil diese zu aufwändig zu pflegen wäre.
  • Stattdessen konservative Sicherheitsprüfung pro URL:
    • nur http/https
    • DNS-Auflösung und Redirect-Kette prüfen
    • keine privaten, loopback-, link-local- oder sonstigen internen Ziel-IP-Ranges
    • Content-Type muss Video/Audio oder ein bekannter öffentlicher Stream-Typ sein
    • Dateiendung/Container plausibilisieren
    • Maximalgröße, Timeout und Rate-Limits erzwingen
    • keine Cookies, keine Login-Flows, keine Umgehungsflags
  • Wenn yt-dlp eine Mediathek-URL direkt unterstützt, darf derselbe sichere yt-dlp Wrapper genutzt werden.
  • Keine DRM-/Paywall-/Login-/Geoblocking-Umgehung.

Provider 3: Internet Archive

  • API-/URL-basierter Import öffentlich verfügbarer Inhalte.
  • Später als eigener Provider, wenn MVP stabil ist.

Konfiguration

Datei: .env

KINOPROJEKT_DB=/data/kino.sqlite3
KINOPROJEKT_MEDIA_ROOT=/jellyfin
KINOPROJEKT_TMP=/data/tmp
JELLYFIN_URL=http://jellyfin:8096
JELLYFIN_API_KEY=change-me
DEFAULT_MAX_HEIGHT=1080
MAX_DOWNLOAD_BYTES=15000000000

Secrets wie JELLYFIN_API_KEY kommen nicht nach Nextcloud, sondern in Vaultwarden oder eine geschützte .env auf dem Zielhost.


Ziel-Dateistruktur

kino-projekt/
  backend/
    app/
      main.py
      config.py
      db.py
      models.py
      schemas.py
      providers/
        base.py
        youtube.py
        mediathek.py
        internet_archive.py
      services/
        downloader.py
        jellyfin.py
        paths.py
        metadata.py
      web/
        index.html
        app.js
        style.css
    tests/
      test_provider_detection.py
      test_path_safety.py
      test_youtube_probe.py
      test_mediathek_probe.py
    pyproject.toml
  docker-compose.yml
  README.md
  docs/
    provider-policy.md
    deployment.md

Tasks

Task 1: Projektgerüst anlegen

Objective: Repository-Struktur, Python-Projekt und minimale FastAPI-App erstellen.

Files:

  • Create: backend/pyproject.toml
  • Create: backend/app/main.py
  • Create: backend/app/config.py
  • Create: backend/tests/test_health.py

Steps:

  1. FastAPI-Abhängigkeiten eintragen.
  2. /health Endpoint erstellen.
  3. Test für /health schreiben.
  4. pytest ausführen.

Verification:

cd backend
pytest -q
uvicorn app.main:app --host 0.0.0.0 --port 8080
curl http://localhost:8080/health

Expected:

{"status":"ok"}

Task 2: Konfiguration und Pfadsicherheit

Objective: Medien-/Temp-Pfade sicher konfigurieren und Path Traversal verhindern.

Files:

  • Create: backend/app/services/paths.py
  • Modify: backend/app/config.py
  • Create: backend/tests/test_path_safety.py

Rules:

  • Alle Downloads nur unter KINOPROJEKT_TMP.
  • Alle finalen Dateien nur unter KINOPROJEKT_MEDIA_ROOT.
  • Keine ../-Escape-Möglichkeiten.
  • Titel/Kanalnamen werden für Dateisysteme normalisiert.

Verification:

pytest backend/tests/test_path_safety.py -q

Task 3: Datenbankmodelle erstellen

Objective: SQLite-Modelle für Quellen und Import-Jobs anlegen.

Files:

  • Create: backend/app/db.py
  • Create: backend/app/models.py
  • Create: backend/tests/test_models.py

Verification:

pytest backend/tests/test_models.py -q

Task 4: Provider-Interface definieren

Objective: Gemeinsame Provider-Abstraktion für YouTube, Mediathek und zukünftige Quellen schaffen.

Files:

  • Create: backend/app/providers/base.py
  • Create: backend/tests/test_provider_detection.py

Implementation Notes:

  • MediaMetadata enthält Titel, Beschreibung, Thumbnail, Dauer, Provider, external_id.
  • DownloadOptions enthält Qualität, Zielbibliothek und Audio-only-Flag.
  • DownloadResult enthält Ausgabedateien und Metadatenpfade.

Task 5: YouTube Provider Probe

Objective: YouTube-URLs erkennen und Metadaten über yt-dlp --dump-json abrufen.

Files:

  • Create: backend/app/providers/youtube.py
  • Create: backend/tests/test_youtube_provider.py

Security Constraints:

  • Kein Cookie-Import.
  • Keine Login-/DRM-/Paywall-Umgehung.
  • Subprocess-Aufruf mit fester Argumentliste, keine Shell.

Verification:

python -m app.providers.youtube 'https://www.youtube.com/watch?v=PUBLIC_TEST_ID'

Expected: JSON-Metadaten oder sauberer Fehler.


Objective: Legale Mediathek- und Direktlinks ohne feste Domain-Allowlist prüfen und Metadaten grob erfassen.

Files:

  • Create: backend/app/providers/mediathek.py
  • Create: backend/tests/test_mediathek_provider.py

Rules:

  • Keine feste Domain-Allowlist, weil diese zu aufwändig zu pflegen wäre.
  • Stattdessen Sicherheitsprüfung pro URL:
    • nur http/https
    • keine privaten/loopback/link-local IP-Ziele nach DNS-Auflösung
    • Redirect-Kette prüfen
    • Content-Type/Dateiendung plausibilisieren
    • Maximalgröße/Timeout/Rate-Limits erzwingen
    • keine Cookies, keine Login-Flows, keine Umgehungsflags
  • Direkte Medien-URLs müssen erlaubte Content-Types liefern.
  • yt-dlp darf als Extractor genutzt werden, wenn Quelle öffentlich und ohne Umgehung erreichbar ist.

Task 7: Probe API erstellen

Objective: UI kann eine URL einreichen und Metadaten anzeigen.

Files:

  • Modify: backend/app/main.py
  • Create: backend/app/schemas.py
  • Create: backend/tests/test_probe_api.py

Endpoint:

POST /api/probe
Content-Type: application/json

{"url":"https://www.youtube.com/watch?v=..."}

Response:

{
  "provider": "youtube",
  "title": "...",
  "thumbnail": "...",
  "duration_seconds": 123,
  "allowed": true
}

Task 8: Minimal-WebUI bauen

Objective: Eine einfache interne WebUI zum Einfügen und Prüfen von Links erstellen.

Files:

  • Create: backend/app/web/index.html
  • Create: backend/app/web/style.css
  • Create: backend/app/web/app.js
  • Modify: backend/app/main.py

UI:

  • Titel: „Kino-Projekt“
  • URL-Eingabe
  • Button: „Quelle prüfen“
  • Metadatenkarte
  • Button: „Import starten“ zunächst disabled, bis Task 10.

Task 9: Download Wrapper bauen

Objective: Sicheren Download-Service mit yt-dlp und festen Optionen implementieren.

Files:

  • Create: backend/app/services/downloader.py
  • Create: backend/tests/test_downloader_args.py

YouTube Default Command:

yt-dlp \
  --no-playlist \
  --write-thumbnail \
  --write-info-json \
  --merge-output-format mkv \
  -f "bv*[height<=1080]+ba/b[height<=1080]/b" \
  -o "<safe-target-template>" \
  "<url>"

Important: In Python subprocess.run([...], shell=False) nutzen.


Task 10: Import-Job API

Objective: Import-Jobs anlegen und ausführen.

Files:

  • Modify: backend/app/main.py
  • Create: backend/app/services/jobs.py
  • Create: backend/tests/test_jobs.py

Endpoints:

POST /api/imports
GET /api/imports/{job_id}
GET /api/imports

MVP darf Jobs synchron oder mit einfachem BackgroundTask ausführen. Später kann Redis/RQ ergänzt werden.


Task 11: Jellyfin Refresh Service

Objective: Nach erfolgreichem Import Jellyfin-Library aktualisieren.

Files:

  • Create: backend/app/services/jellyfin.py
  • Create: backend/tests/test_jellyfin.py

Endpoint intern:

POST {JELLYFIN_URL}/Library/Refresh
X-Emby-Token: {JELLYFIN_API_KEY}

Verification:

  • Mit Test-API-Key gegen Jellyfin prüfen.
  • Danach in Jellyfin sichtbar machen.

Task 12: Docker Compose

Objective: Dienst als Container betreiben.

Files:

  • Create: Dockerfile
  • Create: docker-compose.yml
  • Create: .env.example

Services:

services:
  kino-projekt:
    build: .
    ports:
      - "127.0.0.1:8099:8080"
    volumes:
      - ./data:/data
      - /jellyfin:/jellyfin
    env_file:
      - .env

Task 13: App-Login einbauen

Objective: Zusätzlich zu Mesh-only einen einfachen App-Login mit sicherem Passwort-Hash und Session-Cookie einbauen.

Files:

  • Create: backend/app/services/auth.py
  • Modify: backend/app/models.py
  • Modify: backend/app/main.py
  • Modify: backend/app/web/app.js
  • Create: backend/tests/test_auth.py

Rules:

  • Passwort nie im Klartext speichern.
  • Hashing mit argon2-cffi oder passlib[bcrypt].
  • Session-Cookie HttpOnly, Secure, SameSite=Lax.
  • Initialer Admin-User wird über .env/Setup-Befehl erstellt, nicht im Repo.

Verification:

pytest backend/tests/test_auth.py -q
curl -kI https://kino.dasposchi.de/ # ohne Session: Redirect/Login oder 401

Task 14: Mesh-only Reverse Proxy

Objective: WebUI nur im Headscale-Netz verfügbar machen.

NPM Config:

  • Domain: kino.dasposchi.de
  • Forward: Zielhost Port 8099
  • Access List: headscale-mesh-only
  • SSL: Lets Encrypt

Verification:

curl -4 -kI https://kino.dasposchi.de/        # public: 403
curl --interface tailscale0 -kI https://kino.dasposchi.de/ # mesh: 200

Task 15: Dokumentation

Objective: Nutzung und Grenzen dokumentieren.

Files:

  • Create: README.md
  • Create: docs/provider-policy.md
  • Create: docs/deployment.md

README enthält:

  • Projektname bleibt Kino-Projekt.
  • Unterstützte Quellen: YouTube, legale Mediatheken, Direktlinks, Internet Archive später.
  • Keine feste Mediathek-Domain-Allowlist; stattdessen URL-/Redirect-/IP-/Content-Type-/Größen-/Timeout-Sicherheitschecks.
  • Nicht unterstützte Quellen: Piracy-/Hoster-Scraper, DRM, Paywalls, Login-geschützte Inhalte.
  • Jellyfin-Konfiguration mit Medienwurzel /jellyfin.
  • App-Login plus Mesh-only Deployment.

Getroffene Entscheidungen

  1. Host: Neue kleine Debian-VM oder Debian-LXC/CT auf Proxmox. Empfehlung: LXC/CT, wenn Jellyfin-Medienpfade sauber per Mount/Berechtigung eingebunden werden können; sonst VM.
  2. Zielbibliothek/Zielordner: Inhalte werden unter /jellyfin in passende Jellyfin-Ordner geladen. Die WebUI bietet dafür ein Zielprofil an, z. B. Film, Serie, YouTube/Mediathek. „Passender Ordner“ wird technisch so konkretisiert:
    • Filme: /jellyfin/Filme/<Titel> (<Jahr>)/...
    • Serien: /jellyfin/Serien/<Serientitel>/Staffel<Nummer>/... (bestehende Struktur nutzt z. B. Staffel1, Staffel2, nicht Season 01)
    • YouTube/Mediathek-Clips ohne Serienstruktur: /jellyfin/YouTube-Mediathek/<Kanal oder Sender>/<Titel> [<id>].mkv
    • /jellyfin/YouTube-Mediathek existiert aktuell noch nicht und soll beim Deployment mit passenden Rechten angelegt werden.
  3. Medienwurzel: /jellyfin.
  4. Login: Zusätzlich zu Mesh-only wird ein Login eingebaut. MVP: lokaler App-Login mit Passwort-Hash und Session-Cookie; optional später OIDC/Authelia.
  5. Mediathek-Allowlist: Keine feste Allowlist. Stattdessen URL-Sicherheitsprüfungen, Content-Type-Prüfung, Redirect-Prüfung, Size-/Timeout-Limits und Verbot privater Ziel-IP-Ranges.

Empfohlener MVP:

  • Host: neue kleine Debian-VM oder Debian-LXC/CT auf Proxmox.
  • Domain: kino.dasposchi.de.
  • Medienwurzel: /jellyfin.
  • Bibliothek/Zielprofil: automatisch auswählbare Zielprofile für Film, Serie und YouTube/Mediathek.
  • Provider zuerst: YouTube + generischer Mediathek-/Direktlink-Provider ohne Domain-Allowlist, aber mit strikten Sicherheitschecks.
  • Zugriff: Headscale-Mesh-only plus App-Login.

Konkretisierung: „passender Jellyfin-Ordner“

Die WebUI soll nicht nach einer abstrakten Jellyfin-Bibliothek fragen, sondern nach einem Zielprofil. Das Zielprofil bestimmt den Ordner unter /jellyfin und die Dateibenennung.

MVP-Zielprofile:

  1. Film
    • Ziel: /jellyfin/Filme/<Titel> (<Jahr>)/<Titel> (<Jahr>).mkv
    • Nutzer kann Jahr optional manuell korrigieren.
  2. Serie/Episode
    • Ziel: /jellyfin/Serien/<Serientitel>/Staffel<SS>/<Serientitel> - S<SS>E<EE> - <Episodentitel>.mkv
    • Bestehende Ordner nutzen Staffel1, Staffel2, Staffel3 ohne Leerzeichen/führende Null.
    • Staffel/Episode können manuell gesetzt werden, falls Metadaten fehlen.
  3. YouTube/Mediathek-Clip
    • Ziel: /jellyfin/YouTube-Mediathek/<Kanal oder Sender>/<Titel> [<id>].mkv
    • Default für einzelne YouTube-/Mediathek-Videos.

Die vorhandene Jellyfin-Ordnerstruktur muss vor der Umsetzung einmal geprüft werden; falls die realen Ordnernamen anders sind, werden die Zielprofile entsprechend angepasst.


Verifizierte Jellyfin-Struktur

Geprüft auf Proxmox CT 100 (jellyfin, IP 192.168.178.222). Jellyfin ist aktiv und lauscht auf 8096.

Jellyfin-Bibliotheken laut Konfiguration:

  • Filme: /jellyfin/Filme
  • Serien: /jellyfin/Serien

Rechte/Owner:

  • /jellyfin: jellyfinuser:media, Modus 775
  • /jellyfin/Filme: jellyfinuser:media, setgid-Gruppe media
  • /jellyfin/Serien: jellyfinuser:media, setgid-Gruppe media
  • Jellyfin-Prozessuser jellyfin ist Mitglied der Gruppe media

Beispiele aus der vorhandenen Struktur:

/jellyfin/Filme/Der Super Mario Bros. Film (2023)/Der Super Mario Bros. Film (2023).mp4
/jellyfin/Serien/The White Lotus/Staffel1/The-White-Lotus-S01E02-German-720p-WEB-h264-WvF.mp4

Für das Kino-Projekt bedeutet das:

  • Die Annahme /jellyfin als Medienwurzel ist korrekt.
  • Filme und Serien sollen direkt in die vorhandenen Ordner /jellyfin/Filme und /jellyfin/Serien geschrieben werden.
  • Serien-Staffelordner sollen zur vorhandenen Struktur passend Staffel1, Staffel2, ... heißen.
  • Für YouTube-/Mediathek-Einzelclips soll neu /jellyfin/YouTube-Mediathek angelegt werden, idealerweise ebenfalls jellyfinuser:media und setgid.

Akzeptanzkriterien MVP

  • https://kino.dasposchi.de ist öffentlich blockiert und im Mesh erreichbar.
  • Zusätzlich ist ein App-Login aktiv.
  • YouTube-URL kann geprüft werden.
  • Mediathek-/Direktlink kann ohne feste Domain-Allowlist geprüft werden, sofern URL-, Redirect-, IP-, Content-Type-, Größen- und Timeout-Sicherheitschecks bestehen.
  • Import-Job lädt Datei unter /jellyfin in das gewählte Zielprofil bzw. den passenden Jellyfin-Ordner.
  • Jellyfin Refresh wird ausgelöst.
  • Status und Fehler sind in der UI sichtbar.
  • Passwort-Hashes/Secrets liegen nicht in Nextcloud oder im Repository.
  • Keine nicht erlaubten Quellen/Umgehungsmechanismen sind implementiert.