- Port 8090 korrigiert
- Alle 4 Migrationen dokumentiert (inkl. cover_image/cover_mime_type)
- Neuen API-Endpunkt GET /records/{id}/cover ergänzt
- n8n-Workflows (Haupt + Backfill) mit Ablauf beschrieben
- Projektstruktur und deploy.sh aktualisiert
- Fehlerbehebungs-Tabelle erweitert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
429 lines
13 KiB
Markdown
429 lines
13 KiB
Markdown
# 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.
|
||
|
||
   
|
||
|
||
---
|
||
|
||
## 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://<Server-IP>: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://<host>: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()
|
||
"
|
||
```
|