# 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 & Migrationen](#datenbank--migrationen) - [n8n Workflows](#n8n-workflows) - [Bedienung](#bedienung) - [API-Referenz](#api-referenz) - [Projektstruktur](#projektstruktur) - [Entwicklung & Updates](#entwicklung--updates) - [Fehlerbehebung](#fehlerbehebung) --- ## Ü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 ist kein Node.js oder Python für den Betrieb nötig. Code anpassen, mit `scripts/deploy.sh` auf den Server übertragen, dort `docker compose up -d --build` ausführen. Für KI-Assistenten / Projekt-Kontext: siehe [`PROMPT.md`](PROMPT.md). --- ## Features ### Medien-Übersicht - **Cover** – primär aus der Datenbank (`cover_image` BYTEA), Fallback auf externe URL via Proxy - **Original-** und **Neu-Dateiname** immer sichtbar (Rename-Kontrolle) - 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 Typ (Movie/Episode), Videoformat, Codec und Container - **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 ### Weitere Aktionen - **Löschen** einzelner Einträge (Mülleimer, mit Bestätigung) - **Live-Aktualisierung** alle 5 Sekunden (pausiert im Hintergrund-Tab) - Responsives Layout: **Tabelle** (Desktop) / **Karten** (Mobil, ≤900px) --- ## Architektur ```text Browser │ ▼ ┌─────────────────┐ /api/* ┌─────────────────┐ │ nginx (web) │ ───────────────►│ FastAPI (api) │ │ Port 8090 │ │ 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 | --- ## 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) - Alle SQL-Migrationen ausgeführt (siehe [Datenbank & Migrationen](#datenbank--migrationen)) --- ## 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 sudo mkdir -p /opt/nginx_filebot git clone https://git.vogt.de.com/vogto/filebot-media-browser.git /opt/nginx_filebot ``` ### 2. Datenbank vorbereiten Alle Migrationen ausführen (siehe [Datenbank & Migrationen](#datenbank--migrationen)). ### 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:8090/api/health # Erwartung: {"status":"ok"} ``` Im Browser: **`http://:8090`** --- ## Konfiguration Datei: **`config/config.yaml`** (nicht versioniert, in `.gitignore`). ```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) | | `table` | Tabellenname innerhalb des Schemas | | `db_schema` | Schema (typisch `n8n`) | --- ## Datenbank & Migrationen ### Tabelle | Einstellung | Wert | |-------------|------| | Datenbank | `n8n_filebot` | | Schema | `n8n` | | Tabelle | `movie_files` | | Vollqualifiziert | `"n8n"."movie_files"` | ### Migrationen ausführen Alle SQL-Dateien im Verzeichnis `sql/` der Reihe nach ausführen: ```bash cd /opt/nginx_filebot for f in sql/00*.sql; do echo "==> $f" psql -h 192.168.30.186 -U n8n_filebot -d n8n_filebot -f "$f" done ``` | Datei | Inhalt | |-------|--------| | `001_add_seen_at.sql` | Spalte `seen_at TIMESTAMPTZ` + Index | | `002_add_cover_url.sql` | Spalte `cover_url TEXT` | | `003_add_object_type.sql` | Spalte `object_type TEXT` | | `004_add_cover_image.sql` | Spalten `cover_image BYTEA` + `cover_mime_type TEXT` | ### Gesehen-Status - `seen_at IS NULL` → **ungesehen** (blau hervorgehoben) - gesetzt → **gesehen** ### Cover-Bilder - `cover_url` – externe Poster-URL (z. B. TVMaze, OMDB) - `cover_image` – Bild-Binärdaten direkt in der DB (BYTEA) - `cover_mime_type` – MIME-Typ des gespeicherten Bildes (z. B. `image/jpeg`) Die UI lädt Cover **primär aus der DB** (`/api/records/{id}/cover`), Fallback auf den URL-Proxy (`/api/cover?url=…`). --- ## n8n Workflows ### Workflow 1: FileBot (Haupt-Workflow) Wird von Filebot per Webhook ausgelöst und schreibt Medien-Metadaten in die DB. Danach läuft alle 2 Minuten ein Schedule-Trigger, der fehlende Cover-URLs nachlädt und die Cover-Bilder herunterlädt. **Ablauf:** ``` Webhook (Filebot) → Insert rows (Metadaten) Schedule (alle 2 Min) → Select (cover_url IS NULL) → Loop → Switch (Movie / Episode) Movie → HTTP omdbapi → Edit Fields → Download Cover → Prepare Data → Speichere Cover in DB Episode → If thetvdb_id → HTTP tvmaze → Edit Fields1 → Download Cover → Prepare Data → Speichere Cover in DB ``` **Wichtig für n8n:** - `seen_at` und `cover_image` beim INSERT/UPDATE **nicht** überschreiben - `cover_url` bleibt immer erhalten (auch wenn der Bild-Download fehlschlägt) --- ### Workflow 2: Cover Backfill Einmaliger / regelmäßiger Workflow zum Nachfüllen von `cover_image` für Einträge, die bereits eine `cover_url` haben, aber noch kein Bild in der DB. **Ablauf:** ``` Manuell / Stündlich → Select (cover_image IS NULL AND cover_url IS NOT NULL) → Loop (Batch 5) → HTTP Download (cover_url, continueOnFail) → Code (Base64 extrahieren) → Execute Query (UPDATE cover_image, cover_mime_type) → Loop (weiter) ``` **Hinweis:** Wenn der Workflow wegen Timeout abbricht (viele Einträge), beim erneuten Ausführen macht er automatisch dort weiter, wo er aufgehört hat – der SELECT lädt nur noch Einträge mit `cover_image IS NULL`. Tabellengröße prüfen: ```sql SELECT COUNT(*) AS zeilen, COUNT(cover_image) AS mit_bild, pg_size_pretty(pg_total_relation_size('"n8n"."movie_files"')) AS gesamt FROM n8n.movie_files; ``` --- ## 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. Im Hintergrund-Tab pausiert das Polling. 4. **Zeile antippen/klicken** → Gesehen-Status umschalten. 5. **Mülleimer** → Eintrag löschen (mit Bestätigung). 6. Spaltenköpfe klicken zum Sortieren; Schnellfilter unter der Filterleiste. ### Mobil - Gleiche Logik in **Kartenform** mit Cover links. - Filter untereinander gestapelt. ### 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: `http://:8090/api` | Methode | Pfad | Beschreibung | |---------|------|--------------| | GET | `/health` | DB-Ping | | GET | `/status` | Diagnose (Config, Tabellenliste bei Fehler) | | GET | `/columns` | Liste aller geladenen Spalten | | GET | `/filters` | Distinct-Werte für Filter-Buttons | | GET | `/records` | Datensätze; Query: `search`, `format`, `codec`, `container`, `object_type`, `created_from`, `created_to`, `unseen_only`, `limit`, `offset` | | GET | `/records/meta` | Leichtgewichtiger Check für Auto-Refresh (total, unseen_total, revision) | | GET | `/records/{id}/cover` | Cover-Bild direkt aus DB (BYTEA); 404 wenn kein Bild gespeichert | | GET | `/cover?url=…` | Cover-Proxy für externe URLs (umgeht Hotlink-Sperren) | | PATCH | `/records/{id}/seen` | Body: `{"seen": true\|false}` | | POST | `/records/mark-all-seen` | Alle gefilterten Einträge als gesehen markieren | | DELETE | `/records/{id}` | Eintrag löschen | --- ## 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 │ ├── types.ts │ ├── styles.css │ ├── components/ │ │ ├── DataTable.vue │ │ ├── MobileCards.vue │ │ ├── FilterBar.vue │ │ ├── CoverImage.vue # DB-Bild primär, URL-Proxy als Fallback │ │ ├── NamePair.vue │ │ ├── DateTimeRangeFilter.vue │ │ └── DeleteButton.vue │ ├── composables/ │ │ └── useAutoRefresh.ts │ └── utils/ │ ├── coverUrl.ts │ ├── datetime.ts │ ├── names.ts │ ├── objectType.ts │ ├── preserveScroll.ts │ └── seen.ts ├── nginx/ │ ├── Dockerfile │ └── nginx.conf ├── scripts/ │ └── deploy.sh # Einmaliges SSH-Login, alle Dateien übertragen └── sql/ ├── 001_add_seen_at.sql ├── 002_add_cover_url.sql ├── 003_add_object_type.sql └── 004_add_cover_image.sql ``` --- ## Entwicklung & Updates ### Dateien auf den Server übertragen ```bash cd "/Users/vogto/Documents/ClaudeCode/Webseite Filebot" bash scripts/deploy.sh ``` Das Script baut die Container anschließend automatisch neu. ### Manuell (einzelne Dateien) ```bash cd /opt/nginx_filebot docker compose up -d --build # alles docker compose up -d --build web # nur Frontend docker compose up -d --build api # nur API ``` ### Logs ```bash docker compose logs -f api docker compose logs -f web ``` --- ## Fehlerbehebung ### API-Fehler / leere Seite ```bash docker compose logs api --tail 80 curl -s http://localhost:8090/api/status | python3 -m json.tool ``` | Symptom | Ursache | Lösung | |---------|---------|--------| | `relation "..." does not exist` | Falscher Tabellenname oder Schema | `table: "movie_files"`, `db_schema: "n8n"` in config.yaml | | Spalte fehlt (API 500) | Migration nicht ausgeführt | SQL in `sql/` ausführen | | `503` | DB nicht erreichbar | `nc -zv 192.168.30.186 5432` vom Server prüfen | | Cover fehlt | `cover_image IS NULL` | Backfill-Workflow in n8n ausführen | | Altes Design | 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() " ```