# Filebot Media Browser Web-Oberfläche zur Anzeige und Verwaltung von Medien-Metadaten aus **PostgreSQL** (Filebot / n8n). Fokus: **Original- und neuer Dateiname** zum Prüfen von Renames, plus Cover, Filtern und Gesehen-Status. ![Stack](https://img.shields.io/badge/Vue-3-42b883) ![FastAPI](https://img.shields.io/badge/FastAPI-009688) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791) ![Docker](https://img.shields.io/badge/Docker-2496ED) --- ## Inhaltsverzeichnis - [Überblick](#überblick) - [Features](#features) - [Architektur](#architektur) - [Voraussetzungen](#voraussetzungen) - [Installation auf dem Server](#installation-auf-dem-server) - [Konfiguration](#konfiguration) - [Datenbank](#datenbank) - [Bedienung](#bedienung) - [API-Referenz](#api-referenz) - [Projektstruktur](#projektstruktur) - [Entwicklung & Updates](#entwicklung--updates) - [Fehlerbehebung](#fehlerbehebung) - [Hinweise für n8n](#hinweise-für-n8n) --- ## Überblick Die Anwendung läuft als **zwei Docker-Container** auf einem Linux-Server: 1. **web** – Nginx liefert die Vue-App aus und leitet `/api` an das Backend weiter 2. **api** – FastAPI liest Daten aus PostgreSQL **Produktiv-Pfad auf dem Server:** `/opt/nginx_filebot` > Auf dem Entwicklungs-PC (z. B. Mac) ist **kein** Node.js oder Python für den Betrieb nötig. Code anpassen, auf den Server kopieren, dort `docker compose up -d --build` ausführen. Für KI-Assistenten / Projekt-Kontext: siehe [`PROMPT.md`](PROMPT.md). --- ## Features ### Medien-Übersicht - **Cover** aus `cover_url` (z. B. TVMaze-Poster) in der ersten Spalte / auf Karten - **Original-** und **Neu-Dateiname** immer sichtbar - Technische Spalten: Rating, Format, Auflösung, Codecs, Bitraten, Container, Erstellungsdatum - **Neueste Einträge zuerst** (Sortierung nach `created_at`) ### Filter - Volltextsuche in beiden Dateinamen - **Button-Filter** für Videoformat, Codec und Container (nebeneinander auf Desktop, gestapelt auf Mobil) - **Zeitraum** „Von / Bis“ mit Kalender und Uhrzeit (HH:MM) - Schnellfilter in der Tabelle (clientseitig in geladenen Daten) ### Gesehen-Status - Ungesehene Zeilen sind **hervorgehoben** (blauer Akzent) - **Tippen auf eine Zeile** markiert sie als gesehen oder ungesehen - Filter **„Nur ungesehen“** und Aktion **„Alle als gesehen“** (für aktuelle Filterauswahl) - Zähler **„X neu“** im Header und in den Filtern ### Weitere Aktionen - **Löschen** einzelner Einträge (Mülleimer, mit Bestätigung) - Responsives Layout: **Tabelle** (Desktop) / **Karten** (Mobil, ≤900px Breite) --- ## Architektur ```text Browser │ ▼ ┌─────────────────┐ /api/* ┌─────────────────┐ │ nginx (web) │ ───────────────►│ FastAPI (api) │ │ Port 8080 │ │ Port 8000 │ │ Vue static │ └────────┬────────┘ └─────────────────┘ │ ▼ ┌─────────────────┐ │ PostgreSQL │ │ n8n.movie_files│ └─────────────────┘ ``` | Container | Image-Build | Aufgabe | |-----------|-------------|---------| | `web` | `nginx/Dockerfile` (Node → Nginx) | Statische Dateien, Reverse Proxy | | `api` | `api/Dockerfile` | REST-API, DB-Zugriff | Compose-Datei nutzt **relative Pfade** – immer aus `/opt/nginx_filebot` starten. --- ## Voraussetzungen - Linux-Server mit **Docker** und **Docker Compose v2** - Erreichbare **PostgreSQL**-Instanz (im LAN, z. B. `192.168.30.186:5432`) - Tabelle **`n8n.movie_files`** in Datenbank **`n8n_filebot`** (anpassbar in Config) - Optional: Spalten `seen_at` und `cover_url` (siehe [Datenbank](#datenbank)) --- ## Installation auf dem Server Ausführliche Anleitung inkl. **SMB-Gruppe** und Umzug: [`docs/DEPLOY-NEUER-SERVER.md`](docs/DEPLOY-NEUER-SERVER.md). ### 1. Projekt ablegen ```bash # Ein User (vogto): Rechte auf ganz /opt, nicht nur ein Unterordner sudo groupadd -f opt-docker sudo usermod -aG opt-docker,docker vogto sudo chown vogto:opt-docker /opt && sudo chmod 2775 /opt sudo mkdir -p /opt/nginx_filebot # Variante A: Git git clone /opt/nginx_filebot # Variante B: rsync vom Entwicklungsrechner # rsync -avz --exclude node_modules --exclude config/config.yaml \ # ./ user@server:/opt/nginx_filebot/ ``` ### 2. Datenbank vorbereiten Siehe Abschnitt [Datenbank](#datenbank) – Migrationen ausführen. ### 3. Konfiguration anlegen ```bash cd /opt/nginx_filebot cp config/config.example.yaml config/config.yaml nano config/config.yaml ``` ### 4. Starten ```bash cd /opt/nginx_filebot docker compose up -d --build ``` ### 5. Prüfen ```bash docker compose ps curl -s http://localhost:8080/api/health # Erwartung: {"status":"ok"} ``` Im Browser: **`http://:8080`** --- ## Konfiguration Datei: **`/opt/nginx_filebot/config/config.yaml`** (nicht versionieren, in `.gitignore`). Beispiel (produktive Werte aus dem Projekt): ```yaml database: host: "192.168.30.186" port: 5432 name: "n8n_filebot" # PostgreSQL-Datenbankname user: "n8n_filebot" password: "geheim" table: "movie_files" # Tabellenname (ohne Schema) db_schema: "n8n" # PostgreSQL-Schema ssl: false ``` | Parameter | Bedeutung | |-----------|-----------| | `name` | **Datenbank** (Catalog), nicht Tabellenname | | `table` | Tabellenname innerhalb des Schemas | | `db_schema` | Schema (bei euch typisch `n8n`) | | `schema` | Alias für `db_schema` in YAML (optional) | Die API verbindet sich mit: `"n8n"."movie_files"` (in Anführungszeichen, case-sensitiv). --- ## Datenbank ### Tabelle und Schema | Einstellung | Typischer Wert | |-------------|----------------| | Datenbank | `n8n_filebot` | | Schema | `n8n` | | Tabelle | `movie_files` | Tabellen prüfen: ```bash docker compose exec api python /app/scripts/list-tables.py ``` ### Migration: `seen_at` ```bash psql -h 192.168.30.186 -U n8n_filebot -d n8n_filebot \ -f /opt/nginx_filebot/sql/001_add_seen_at.sql ``` Oder manuell: ```sql ALTER TABLE n8n.movie_files ADD COLUMN IF NOT EXISTS seen_at TIMESTAMPTZ NULL; CREATE INDEX IF NOT EXISTS idx_movie_files_seen_at ON n8n.movie_files (seen_at); ``` - `seen_at IS NULL` → **ungesehen** (hervorgehoben in der UI) - gesetzt → **gesehen** ### Migration: `cover_url` ```bash psql -h 192.168.30.186 -U n8n_filebot -d n8n_filebot \ -f /opt/nginx_filebot/sql/002_add_cover_url.sql ``` ```sql ALTER TABLE n8n.movie_files ADD COLUMN IF NOT EXISTS cover_url TEXT NULL; ``` Beispiel-URL: `https://static.tvmaze.com/uploads/images/medium_portrait/200/502332.jpg` (Doppelte Anführungszeichen in der DB werden in der UI bereinigt.) ### Weitere Spalten Die API liest alle in `api/main.py` → `COLUMNS` definierten Felder (entsprechen dem n8n/Filebot-Export). Fehlende Spalten führen zu API-Fehlern – dann Spalte in PostgreSQL ergänzen oder `COLUMNS` anpassen. --- ## Bedienung ### Desktop 1. Seite öffnen → Daten werden geladen (Status „DB verbunden“). 2. Filter oben nutzen; **Aktualisieren** lädt neu, **Zurücksetzen** löscht Filter. 3. **Live-Aktualisierung:** Alle 5 Sekunden werden die Daten still neu geladen (ohne Seiten-Reload). Im Header: „Live · HH:MM:SS“. Im Hintergrund-Tab pausiert das Polling. 4. **Cover:** Bilder werden über `/api/cover` geladen (Proxy), falls externe Poster (z. B. TVMaze) blockiert werden. 5. **Zeile antippen/klicken** → Gesehen-Status umschalten. 6. **Mülleimer** → Eintrag löschen (mit Bestätigung). 7. Spaltenköpfe klicken zum Sortieren; Schnellfilter unter der Filterleiste. ### Mobil - Gleiche Logik in **Kartenform** mit Cover links. - Filter untereinander. ### Gesehen-Status | Aktion | Effekt | |--------|--------| | Zeile antippen (ungesehen) | `seen_at` = jetzt | | Zeile antippen (gesehen) | `seen_at` = NULL | | „Nur ungesehen“ | Zeigt nur Einträge ohne `seen_at` | | „Alle als gesehen“ | Setzt `seen_at` für alle **aktuell gefilterten** Einträge | --- ## API-Referenz Basis-URL hinter Nginx: `http://:8080/api` ### `GET /health` ```json { "status": "ok" } ``` ### `GET /status` Diagnose: Config-Pfad, qualifizierte Tabelle, Zeilenanzahl, bei Fehler Tabellenlisten. ### `GET /records` Query-Parameter: | Parameter | Beschreibung | |-----------|--------------| | `search` | ILIKE in `file_name_original` und `file_name_new` | | `format` | `standard_video_format` | | `codec` | `video_codec_library` | | `container` | `container_format` | | `created_from` | ISO-Datum/Zeit, `created_at >=` | | `created_to` | ISO-Datum/Zeit, `created_at <=` | | `unseen_only` | `true` → nur `seen_at IS NULL` | | `limit`, `offset` | Pagination (Standard limit=5000) | Antwort enthält u. a. `records`, `total`, `unseen_total`. ### `GET /records/meta` Gleiche Query-Parameter wie `GET /records` (ohne Pagination). Leichtgewichtige Antwort für Auto-Refresh: ```json { "total": 120, "unseen_total": 3, "max_id": 456, "latest_created_at": "2026-06-04T12:00:00+00:00", "revision": "a1b2c3…" } ``` ### `GET /cover` Query: `url` (HTTP(S)-Poster-URL). Liefert das Bild über die API (umgeht Hotlink-/Referrer-Sperren). ### `GET /filters` Distinct-Werte für Format-, Codec- und Container-Buttons. ### `PATCH /records/{id}/seen` Body: ```json { "seen": true } ``` oder `{ "seen": false }` (setzt `seen_at` auf NULL). ### `POST /records/mark-all-seen` Markiert alle Einträge, die zu den **gleichen Query-Filtern** wie `GET /records` passen (ohne `unseen_only`). ### `DELETE /records/{id}` Löscht einen Datensatz anhand der `id`. --- ## Projektstruktur ```text . ├── docker-compose.yml ├── PROMPT.md # KI-/Projekt-Kontext ├── README.md ├── config/ │ └── config.example.yaml ├── api/ │ ├── main.py # REST-Endpunkte, COLUMNS │ ├── config.py # YAML-Config, db_schema │ ├── Dockerfile │ ├── requirements.txt │ └── scripts/ │ └── list-tables.py # Diagnose im Container ├── frontend/ │ ├── package.json │ ├── vite.config.ts │ └── src/ │ ├── App.vue │ ├── api.ts │ ├── styles.css # Theme (helles Dark-UI) │ ├── components/ │ │ ├── DataTable.vue │ │ ├── MobileCards.vue │ │ ├── FilterBar.vue │ │ ├── CoverImage.vue │ │ ├── NamePair.vue │ │ ├── DateTimeRangeFilter.vue │ │ └── DeleteButton.vue │ └── utils/ │ ├── datetime.ts │ └── seen.ts ├── nginx/ │ ├── Dockerfile │ └── nginx.conf └── sql/ ├── 001_add_seen_at.sql └── 002_add_cover_url.sql ``` --- ## Entwicklung & Updates ### Code aktualisieren ```bash cd /opt/nginx_filebot git pull # falls Git docker compose up -d --build ``` Nur Frontend: ```bash docker compose up -d --build web ``` Nur API: ```bash docker compose up -d --build api ``` ### Logs ```bash docker compose logs -f api docker compose logs -f web ``` ### Optional: lokale Entwicklung (nicht für Produktion) Nur wenn bewusst gewünscht – normaler Workflow ist Server-only. ```bash # API cd api && pip install -r requirements.txt CONFIG_PATH=../config/config.yaml uvicorn main:app --reload --port 8000 # Frontend (proxied in vite.config.ts nach :8000) cd frontend && npm install && npm run dev ``` --- ## Fehlerbehebung ### API-Fehler / leere Seite ```bash cd /opt/nginx_filebot docker compose logs api --tail 80 curl -s http://localhost:8080/api/status | python3 -m json.tool ``` | Symptom | Ursache | Lösung | |---------|---------|--------| | `relation "n8n.n8n_filebot" does not exist` | Falscher Tabellenname | `table: "movie_files"` in config.yaml | | `relation "public...." does not exist` | Schema falsch | `db_schema: "n8n"` | | Spalte `seen_at` / `cover_url` fehlt | Migration fehlt | SQL in `sql/` ausführen | | `API-Fehler: 503` | DB nicht erreichbar | `nc -zv 5432` vom Server und aus Container | | 0 Einträge | Filter zu streng / leere DB | Filter zurücksetzen, `SELECT COUNT(*)` prüfen | | Altes Design | Cache / kein Rebuild | `docker compose up -d --build web` + Hard-Reload | ### Netzwerk vom Container zur DB ```bash docker compose exec api python -c " import socket s = socket.create_connection(('192.168.30.186', 5432), timeout=5) print('OK'); s.close() " ``` ### Port ändern In `docker-compose.yml` z. B. `"80:80"` oder `"8888:80"` statt `"8080:80"`. --- ## Hinweise für n8n - **INSERT:** Neue Zeilen haben `seen_at = NULL` → erscheinen als „neu“. - **UPDATE:** `seen_at` und `cover_url` nicht mit überschreiben, wenn der Status erhalten bleiben soll. - Datenbank-Workflow und Tabellenname (`movie_files`, Schema `n8n`) mit der `config.yaml` abstimmen. --- ## Lizenz / Nutzung Privates Heimnetz-Projekt. Anpassungen nach Bedarf; `config.yaml` mit Passwörtern nicht in öffentliche Repositories committen.