Initial commit: Filebot Media Browser

Vue 3 + FastAPI Webapp zur Anzeige von Filebot/n8n-Medienmetadaten aus PostgreSQL.
Enthält Frontend, API, Nginx-Config, SQL-Migrationen und Docker-Compose-Setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Housemann
2026-07-11 05:38:37 +02:00
co-authored by Claude Sonnet 4.6
commit 3f253ffe81
45 changed files with 4791 additions and 0 deletions
+480
View File
@@ -0,0 +1,480 @@
# 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 <repository-url> /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://<Server-IP>: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://<host>: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 <host> 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.