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).
18 KiB
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:
- YouTube-Videos, Playlists und Kanäle importieren, soweit öffentlich erreichbar und nicht DRM-/Login-/Paywall-geschützt.
- 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.
- Downloads in Jellyfin-kompatibler Struktur ablegen.
- Jellyfin-Library-Refresh auslösen.
- 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.
Provider 2: Mediathek/Direktlink
- 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
- nur
- Wenn
yt-dlpeine 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:
- FastAPI-Abhängigkeiten eintragen.
/healthEndpoint erstellen.- Test für
/healthschreiben. pytestausfü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:
MediaMetadataenthält Titel, Beschreibung, Thumbnail, Dauer, Provider, external_id.DownloadOptionsenthält Qualität, Zielbibliothek und Audio-only-Flag.DownloadResultenthä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.
Task 6: Mediathek-/Direktlink Provider Probe
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
- nur
- 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-cffioderpasslib[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: Let’s 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
- 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.
- Zielbibliothek/Zielordner: Inhalte werden unter
/jellyfinin 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, nichtSeason 01) - YouTube/Mediathek-Clips ohne Serienstruktur:
/jellyfin/YouTube-Mediathek/<Kanal oder Sender>/<Titel> [<id>].mkv /jellyfin/YouTube-Mediathekexistiert aktuell noch nicht und soll beim Deployment mit passenden Rechten angelegt werden.
- Filme:
- Medienwurzel:
/jellyfin. - 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.
- 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:
- Film
- Ziel:
/jellyfin/Filme/<Titel> (<Jahr>)/<Titel> (<Jahr>).mkv - Nutzer kann Jahr optional manuell korrigieren.
- Ziel:
- Serie/Episode
- Ziel:
/jellyfin/Serien/<Serientitel>/Staffel<SS>/<Serientitel> - S<SS>E<EE> - <Episodentitel>.mkv - Bestehende Ordner nutzen
Staffel1,Staffel2,Staffel3ohne Leerzeichen/führende Null. - Staffel/Episode können manuell gesetzt werden, falls Metadaten fehlen.
- Ziel:
- YouTube/Mediathek-Clip
- Ziel:
/jellyfin/YouTube-Mediathek/<Kanal oder Sender>/<Titel> [<id>].mkv - Default für einzelne YouTube-/Mediathek-Videos.
- Ziel:
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, Modus775/jellyfin/Filme:jellyfinuser:media, setgid-Gruppemedia/jellyfin/Serien:jellyfinuser:media, setgid-Gruppemedia- Jellyfin-Prozessuser
jellyfinist Mitglied der Gruppemedia
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
/jellyfinals Medienwurzel ist korrekt. - Filme und Serien sollen direkt in die vorhandenen Ordner
/jellyfin/Filmeund/jellyfin/Seriengeschrieben werden. - Serien-Staffelordner sollen zur vorhandenen Struktur passend
Staffel1,Staffel2, ... heißen. - Für YouTube-/Mediathek-Einzelclips soll neu
/jellyfin/YouTube-Mediathekangelegt werden, idealerweise ebenfallsjellyfinuser:mediaund setgid.
Akzeptanzkriterien MVP
https://kino.dasposchi.deist ö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
/jellyfinin 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.