Files
Homelab--Bratonein-Kontroll…/Kapitel 13/Tutorial.md

20 KiB
Raw Blame History

🛠️ Kapitel 13 Clipper (Tutorial)


Einleitung

Clips sind der beste Weg, lange Streams in kurze, teilbare Highlights zu verwandeln. Unser Ziel in diesem Kapitel: Wir bauen ein System, das neue Videos automatisch erkennt, sinnvolle Stellen analysiert, passende Highlights schneidet und die Ergebnisse in Nextcloud ablegt inklusive Titeln und Hashtags für jede Plattform. Der Clipper selbst übernimmt dabei die technische Verarbeitung, während n8n für Steuerung, Analyse und Benachrichtigungen sorgt. Das System bleibt dadurch flexibel, ressourcenschonend und jederzeit erweiterbar.


Voraussetzungen

  • Proxmox LXC mit Debian 12 (Bookworm)
  • Nextcloud (Pflicht, Zielort für Clips & Metadaten)
  • n8n-Instanz (Automatisierung, Steuerung Clipper, Analyse, Metadaten-Erzeugung)
  • Twitch-Entwickler-Account inkl. API-Key (für VOD- und Clip-Zugriff)
  • Optional: RTMP-Server, falls VODs lokal aufgezeichnet werden
  • Ressourcen für den LXC: 1 vCPU, 12 GB RAM, 10 GB Speicher reichen aus
  • Grundwissen: SSH-Verbindung, Nano-Editor, Basiskenntnisse in n8n

Vorbereitung

Wir beginnen mit einem frischen Debian12LXC in Proxmox, benennen ihn clipper und vergeben die im Abschnitt oben genannten Ressourcen. Danach bringen wir das System auf Stand und installieren die Grundwerkzeuge:

apt update && apt upgrade -y
apt install -y curl unzip ffmpeg inotify-tools

Eine korrekte Systemzeit ist entscheidend, da Schnittmarken später auf exakten Sekunden basieren. Prüfe die Zeit mit:

timedatectl status

Wenn hier UTC steht und du lieber „Europe/Berlin“ nutzen willst:

timedatectl list-timezones | grep Europe
timedatectl set-timezone Europe/Berlin
timedatectl status

Die Zeit wird sofort angepasst, Logs und Schnittzeiten passen damit zur lokalen Umgebung.

Zum Schluss legen wir die Arbeitsordner an:

mkdir -p /srv/clipper/{watch,out,temp,logs}
  • watch Eingangsordner für neue Videos (egal ob von Twitch oder RTMP)
  • out fertige Clips und Metadaten
  • temp Zwischenspeicher für Analyse
  • logs Protokolle aller Abläufe

Damit ist das Fundament gelegt.


Abschnitt 2 ClipperLXC einrichten (Benutzer, Verzeichnisse, Pakete, Skripte)

In diesem Abschnitt richten wir den ClipperContainer so ein, dass SSHSchlüssel, Downloads und n8nAufrufe ohne Berechtigungsfehler funktionieren. Wir arbeiten jetzt im Terminal deines ClipperLXC als root.

Entscheidung & Begründung Benutzer mit Home & Bash
Der Benutzer clipper bekommt ein Homeverzeichnis (/home/clipper) und eine LoginShell. So können SSHSchlüssel sauber in ~clipper/.ssh landen und n8n später per SSH Befehle ausführen. Varianten ohne Home (SystemUser) führen bei ssh-copy-id zu Fehlern.

2.1 Benutzer und Verzeichnisse anlegen (Terminal, als root)

adduser --home /home/clipper --shell /bin/bash clipper

Vergib ein Passwort und bestätige die Abfragen. Danach legst du die Arbeitsordner an und überträgst den Besitz an clipper:

mkdir -p /srv/clipper/{inbox,watch,out,temp,logs,bin}
chown -R clipper:clipper /srv/clipper
chmod 750 /srv/clipper

(optional, aber hilfreich für ssh-copy-id später):

install -d -m 700 -o clipper -g clipper /home/clipper/.ssh

Falls du den Benutzer bereits ohne Home angelegt hast:
Richte ihn so nach:
mkdir -p /home/clipper/.ssh && chown -R clipper:clipper /home/clipper && chmod 700 /home/clipper/.ssh

2.2 Pakete installieren (Terminal, als root)

apt update && apt install -y yt-dlp jq python3 python3-venv curl unzip inotify-tools sudo
  • ffmpeg: Analyse & Schnitt
  • yt-dlp: TwitchVOD/ClipDownloads (HLS)
  • jq: JSONHandling
  • python3/venv: spätere AnalyseTools
  • inotify-tools: DateisystemEvents (optional)
  • sudo: für gezielte Rechteerhöhungen falls nötig

2.3 Zentrale Konfiguration (Terminal, als root)

Bevor wir die Umgebungsdatei anlegen, brauchen wir ein eigenes Konfigurationsverzeichnis. Das existiert standardmäßig nicht, daher legen wir es einmalig an:

mkdir -p /etc/clipper
chown root:clipper /etc/clipper
chmod 750 /etc/clipper

Entscheidung & Begründung eigenes /etc/clipper
Konfiguration gehört nach /etc. Mit einem eigenen Ordner /etc/clipper bleibt alles übersichtlich getrennt.
Besitzer ist root, die Gruppe clipper. So kann der Clipper-User die Datei lesen, aber nicht verändern genau die Balance zwischen Sicherheit und Funktion.

Entscheidung & Begründung eigenes /etc/clipper Konfiguration gehört nach /etc. Mit einem eigenen Ordner /etc/clipper bleibt alles übersichtlich getrennt. Besitzer ist root, die Gruppe clipper. So kann der Clipper-User die Datei lesen, aber nicht verändern genau die Balance zwischen Sicherheit und Funktion.

Lege eine Umgebungsdatei an, die beide Skripte laden:

nano /etc/clipper/clipper.env

Inhalt:

CLIPPER_INBOX=/srv/clipper/inbox
CLIPPER_IN=/srv/clipper/watch
CLIPPER_OUT=/srv/clipper/out
CLIPPER_TMP=/srv/clipper/temp
CLIPPER_LOG=/srv/clipper/logs/clipper.log

Dateirechte setzen, damit clipper sie lesen darf:

chown root:clipper /etc/clipper/clipper.env
chmod 640 /etc/clipper/clipper.env

2.4 PythonUmgebung vorbereiten (Wechsel zu Benutzer clipper)

Wechsle jetzt zum Benutzer clipper:

su - clipper

Erzeuge und fülle eine virtuelle Umgebung für die spätere Analyse:

python3 -m venv /srv/clipper/.venv
source /srv/clipper/.venv/bin/activate
pip install --upgrade pip
pip install numpy opencv-python-headless
deactivate

Wechsle für die nächsten Schritte im Benutzer clipper weiter.

2.5 Einstiegsskripte erstellen (im Benutzer clipper)

AnalyseStub prüft Eingaben, schreibt Logs, erzeugt leere Kandidatenliste:

nano /srv/clipper/bin/clipper-analyze

Inhalt:

#!/usr/bin/env bash
set -euo pipefail
ENV_FILE="/etc/clipper/clipper.env"; [ -r "$ENV_FILE" ] || { echo "ENV nicht lesbar: $ENV_FILE" >&2; exit 1; }; source "$ENV_FILE"
IN="$1"       # absolute Datei
JOBID="${2:-manual}"
mkdir -p "$CLIPPER_TMP/$JOBID"
echo "$(date '+%F %T') [ANALYZE] job=$JOBID file=$IN" | tee -a "$CLIPPER_LOG"
OUT_JSON="$CLIPPER_TMP/$JOBID/candidates.json"
echo '[]' > "$OUT_JSON"
echo "$OUT_JSON"

SchneidStub protokolliert Schnittaufrufe, echte Logik folgt in Abschnitt 5:

nano /srv/clipper/bin/clipper-cut

Inhalt:

#!/usr/bin/env bash
set -euo pipefail
ENV_FILE="/etc/clipper/clipper.env"; [ -r "$ENV_FILE" ] || { echo "ENV nicht lesbar: $ENV_FILE" >&2; exit 1; }; source "$ENV_FILE"
IN="$1"            # absolute Datei
RANGES_JSON="$2"   # Zeitbereiche (kommt später aus Abschnitt 4)
JOBID="${3:-manual}"
mkdir -p "$CLIPPER_OUT/$JOBID"
echo "$(date '+%F %T') [CUT] job=$JOBID file=$IN ranges=$RANGES_JSON" | tee -a "$CLIPPER_LOG"
exit 0

Rechte setzen und Eigentümer korrigieren:

chmod +x /srv/clipper/bin/clipper-*
chown -R clipper:clipper /srv/clipper/bin

2.6 Logrotation (zurück zu root)

Beende die Session (exit) und kehre zu root zurück. Richte Logrotation ein:

nano /etc/logrotate.d/clipper

Inhalt:

/srv/clipper/logs/*.log {
  rotate 14
  daily
  missingok
  notifempty
  compress
  delaycompress
  copytruncate
}

Schnelltest (optional):
Zurück im Benutzer clipper:
/srv/clipper/bin/clipper-analyze /srv/clipper/watch/demo.mp4 job-001
/srv/clipper/bin/clipper-cut /srv/clipper/watch/demo.mp4 /srv/clipper/temp/job-001/ranges.json job-001
tail -n 50 /srv/clipper/logs/clipper.log

Mit dieser Einrichtung sind SSHSchlüssel, Berechtigungen und Pfade konsistent. ssh-copy-id aus Abschnitt 3 funktioniert dadurch ohne Fehlermeldungen und n8n kann die Skripte stabil starten.


Abschnitt 3 n8n ↔ Twitch: VOD & Clips importieren, in Nextcloud ablegen, Clipper starten

In diesem Abschnitt verbinden wir n8n mit Twitch, Nextcloud und dem Clipper. Das Ziel: n8n erkennt automatisch neue VODs auf Twitch, lädt sie zusammen mit Clips herunter, legt sie in Nextcloud ab und startet dann die Analyse auf dem Clipper. Wir gehen Schritt für Schritt vor immer mit klaren Hinweisen, ob wir uns gerade in der n8n-Weboberfläche, im Terminal oder in Nextcloud befinden.

Note

Rollenverteilung
n8n steuert (APIs, Logik, Benachrichtigungen). Clipper arbeitet (Download, Analyse, Schnitt). Nextcloud speichert (Archiv & Übergabe). So bleibt n8n schlank und ausfallsicher, während Clipper CPU/IO für Medienjobs bekommt.


Schritt 1: Zugriff zwischen den Containern vorbereiten (Terminal)

Optional: SSH Known-Hosts zurücksetzen (falls Clipper schon einmal erstellt wurde)

Wenn du den Clipper-LXC bereits früher erstellt und neu aufgesetzt hast, kann es zu einem SSH-Fehler kommen:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Das bedeutet, dass sich der Server-Hostkey geändert hat. SSH blockiert dann aus Sicherheitsgründen.

Lösung Schritt für Schritt (im n8n-LXC als root)

  1. Fingerprint auf dem Clipper prüfen
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

(SHA256-Wert merken und vergleichen)

  1. Alten Eintrag löschen
ssh-keygen -R "<CLIPPER-IP>"

Note

SSH schützt vor Man-in-the-Middle-Angriffen, indem es Hostkeys prüft. Wenn ein Container neu erstellt wird, ist ein neuer Hostkey normal. Wir löschen den alten Eintrag bewusst, prüfen den neuen Fingerprint und akzeptieren ihn erst danach. So bleibt die Verbindung sicher und stabil.

SSH-Schlüssel für Clipper hinterlegen (zwingend):

ssh-keygen -t rsa -b 4096 -m PEM -f ~/.ssh/id_n8n_clipper -N ""
ssh-copy-id -i ~/.ssh/id_n8n_clipper.pub clipper@<CLIPPER-IP>   # << unbedingt ausführen
ssh -i ~/.ssh/id_n8n_clipper clipper@<CLIPPER-IP> "echo OK"

Ersetze <CLIPPER-IP> durch die IP/den Hostnamen deines Clipper-LXC (z. B. 10.0.0.42). Wenn OK erscheint, kann n8n ohne Passwort auf den Clipper zugreifen.

Note

RSA (PEM) statt ed25519: n8n akzeptiert nur PEM-Schlüssel zuverlässig. Ed25519 erzeugt oft OpenSSH-Keys, die von n8n nicht geparst werden können. Mit -m PEM stellen wir sicher, dass das Format passt.


Zugangsdaten für Nextcloud-WebDAV erstellen (Weboberfläche)

Ort in Nextcloud: Avatar (oben rechts) → Persönliche EinstellungenSicherheitganz nach unten zum Bereich App-Passwörter.

Anleitung:

  1. Melde dich in der Nextcloud-Weboberfläche an.
  2. Gehe zu Persönliche EinstellungenSicherheit.
  3. Ganz nach unten scrollen bis zum Abschnitt App-Passwörter.
  4. Trage bei App-Name z. B. n8n Clipper ein und klicke auf Neues App-Passwort erstellen.
  5. Kopiere das einmalig angezeigte App-Passwort sofort und notiere zusätzlich deinen Benutzernamen.

WebDAV-URL für n8n/Uploads:

https://<DEINE_NEXTCLOUD_DOMAIN>/remote.php/dav/files/<DEIN_NC_BENUTZER>/

Twitch-API Zugang (Developer Console)

  1. Gehe auf Twitch Developer ConsoleApplicationsRegister Your Application.

  2. Felder ausfüllen:

    • Name: frei wählbar
    • OAuth Redirect URL: z. B. https://localhost/
    • Category: passend wählen
  3. App speichern → Manage öffnen.

  4. Client ID notieren, New Secret klicken und Client Secret sichern.

Twitch OAuth2 Credential in n8n (Client Credentials Flow)

  • Name: Twitch API
  • Grant Type: Client Credentials
  • Authorization URL: https://id.twitch.tv/oauth2/authorize
  • Access Token URL: https://id.twitch.tv/oauth2/token
  • Client ID / Secret: aus Twitch Developer Console
  • Scope: leer lassen
  • Authentication: Body

Schritt 2: Twitch-User-ID herausfinden (n8n-Weboberfläche)

HTTP Request Node:

  • Methode: GET

  • URL:

    https://api.twitch.tv/helix/users?login=<DEIN_LOGIN>
    
  • Auth: OAuth2 Credential

  • Header: Client-Id: <DEINE_CLIENT_ID>

Im Ergebnis findest du im Feld data[0].id deine User-ID (z. B. 123456789).


Schritt 3: Workflow bauen (n8n-Weboberfläche)

In diesem Schritt erstellen wir den eigentlichen Workflow in n8n. Er sorgt dafür, dass regelmäßig neue VODs bei Twitch abgefragt, geprüft und anschließend verarbeitet werden.

Note

Ziel dieses Schrittes
Wir bauen den Ablauf in n8n: Cron-Trigger → Twitch API → Prüfung der State-Datei → Entscheidung über neue VODs → Download und Upload in Nextcloud.


  1. Cron-Trigger Zeitplan festlegen (Node-Name: Cron Alle 10 Min)

    • Dieser Node löst den Workflow regelmäßig aus.
    • Stelle ein: alle 10 Minuten ausführen.
  2. HTTP Request Twitch Videos abfragen (Node-Name: HTTP Get Videos)

    • Dieser Node ruft über die Twitch-API alle VODs deines Accounts ab.
    • URL:
    https://api.twitch.tv/helix/videos?user_id=<DEINE_TWITCH_USER_ID>&type=archive
    
    • Authentifizierung: OAuth2 (Credential „Twitch API“)
    • Header: Client-Id: <DEINE_CLIENT_ID>
  3. SSH Credentials in n8n anlegen

    • Damit n8n mit dem Clipper kommunizieren kann, brauchst du SSH-Zugangsdaten.
    • Trage in n8n ein:
      • Name: SSH Clipper
      • Host: <CLIPPER-IP> (z. B. 10.0.0.42)
      • Port: 22
      • Username: clipper
      • Private Key: Inhalt von ~/.ssh/id_n8n_clipper
      • Working Directory: /srv/clipper
  4. SSH Node State-Datei prüfen (Node-Name: SSH Check State)

    • Dieser Schritt prüft, ob die Datei /srv/clipper/state/vod_seen.list bereits existiert und ob VOD-IDs eingetragen sind.
    • Command (als Expression):
    {{`set -euo pipefail; STATE_FILE="/srv/clipper/state/vod_seen.list"; fe=false; ne=false; arr='[]'; if [ -f "$STATE_FILE" ]; then fe=true; if [ -s "$STATE_FILE" ]; then ne=true; mapfile -t L < "$STATE_FILE"; json='['; sep=''; for id in "\${L[@]}"; do id_trim="$(printf '%s' "$id" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"; [ -n "$id_trim" ] || continue; json+="$sep\"$id_trim\""; sep=','; done; json+=']'; arr="$json"; fi; fi; printf '{"file_exists":%s,"non_empty":%s,"vods":%s}\n' "$fe" "$ne" "$arr"`}}
    
    • Typische Ergebnisse:
    {"file_exists":false,"non_empty":false,"vods":[]}
    {"file_exists":true,"non_empty":false,"vods":[]}
    {"file_exists":true,"non_empty":true,"vods":["123456789","987654321"]}
    
  5. Set Node Ergebnisse übernehmen (Node-Name: Set State Ergebnis)

    • Ziel: die Ausgabe aus dem vorherigen Schritt in Felder umwandeln, die n8n versteht.
    • Expressions:
      • file_exists → {{ JSON.parse($json.stdout).file_exists }}
      • non_empty → {{ JSON.parse($json.stdout).non_empty }}
      • vods → {{ JSON.parse($json.stdout).vods }}

    Important

    Hier nur die Expressions eintragen, keine Typ-Konvertierungen vornehmen.

  6. If-Node Entscheidung (Node-Name: If State Prüfung)

    • Dieser Node entscheidet, ob neue VODs heruntergeladen werden müssen.
    • Bedingungen:
    1. Bedingung: {{ $json.file_exists }} is true (Boolean)
    2. Bedingung: {{ $json.non_empty }} is true (Boolean)
    Operator: AND
    
    • Ergebnis: zwei mögliche Pfade → False = neu anlegen, True = prüfen auf neue VODs.

  1. False-Pfad VODs herunterladen und hochladen

    Note

    Dieser Pfad läuft, wenn die State-Datei nicht existiert oder leer ist. Es werden alle VODs heruntergeladen und in Nextcloud hochgeladen.

    A. Vorbereitung (einmalig) rclone an Nextcloud anbinden
    Ort: Clipper-LXC Shell

    apt update && apt install -y rclone
    su - clipper
    rclone config create nc webdav \
      url=https://DEINE_DOMAIN/remote.php/dav/files/DEIN_BENUTZERNAME/ \
      vendor=nextcloud \
      user=DEIN_BENUTZERNAME \
      pass=$(rclone obscure 'DEIN_APP_PASSWORT')
    rclone ls nc: --config /home/clipper/.config/rclone/rclone.conf
    

    Note

    Uploads laufen im Clipper mit rclone. Das ist robuster als Upload-Nodes in n8n, vermeidet Fehler und räumt lokale Dateien auf.

    B. Download/Upload Skript erstellen
    Ort: Clipper-LXC Shell

    nano <clipper-ordner>/bin/clipper-vod-get
    

    Inhalt:

    #!/usr/bin/env bash
    set -euo pipefail
    ID="${1:?need VOD id}"
    URL="${2:-https://www.twitch.tv/videos/${ID}}"
    TMP="<clipper-ordner>/temp"
    LOGDIR="<clipper-ordner>/logs"
    CONF="/home/clipper/.config/rclone/rclone.conf"
    DST="nc:<gewünschter Ordner>/VODs/${ID}/"
    OUT="$TMP/${ID}.%(ext)s"
    FILE="$TMP/${ID}.mp4"
    TEMP="$TMP/${ID}.temp.mp4"
    PART="$TMP/${ID}.mp4.part"
    LOCK="$TMP/${ID}.lock"
    LOG="$LOGDIR/${ID}.log"
    mkdir -p "$TMP" "$LOGDIR"
    log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
    log "=== Start VOD $ID ==="
    log "URL: $URL"
    if rclone lsf "$DST" --config "$CONF" >/dev/null 2>&1; then log "SKIP: $ID bereits in Nextcloud"; exit 0; fi
    if [[ -e "$LOCK" ]]; then log "LOCK: $ID wird bereits verarbeitet"; exit 0; fi
    trap 'rm -f "$LOCK"' EXIT; : > "$LOCK"
    if [[ -s "$TEMP" && ! -s "$FILE" ]]; then mv -f "$TEMP" "$FILE"; fi
    if [[ -s "$FILE" ]]; then rclone move "$FILE" "$DST" --config "$CONF" --create-empty-src-dirs -v; rm -f "$PART" "$TEMP" || true; exit 0; fi
    yt-dlp -q --no-progress --retries 20 --fragment-retries 50 --retry-sleep 5 --socket-timeout 30 --hls-prefer-ffmpeg --remux-video mp4 -o "$OUT" "$URL"
    [[ -s "$FILE" ]] || { [[ -s "$TEMP" ]] && mv -f "$TEMP" "$FILE"; }
    if [[ ! -s "$FILE" ]]; then log "ERROR: Download fehlgeschlagen ($FILE fehlt/leer)"; exit 2; fi
    rclone move "$FILE" "$DST" --config "$CONF" --create-empty-src-dirs -v
    rm -f "$PART" "$TEMP" || true
    log "=== Done VOD $ID ==="
    

    Rechte setzen:

    chmod 755 <clipper-ordner>/bin/clipper-vod-get
    chown clipper:clipper <clipper-ordner>/bin/clipper-vod-get
    

    Note

    Pro VOD entsteht ein Logfile in <clipper-ordner>/logs/<ID>.log. Du kannst es live mit tail -f <clipper-ordner>/logs/<ID>.log verfolgen.


Mit diesem Aufbau ist der False-Pfad fertig: Wenn die State-Datei fehlt oder leer ist, werden alle VODs verarbeitet und in Nextcloud hochgeladen. Der True-Pfad folgt im nächsten Abschnitt.


Schritt 8: True-Pfad Check und ggf. Download

Note

Der False-Pfad wurde bereits im vorherigen Abschnitt vollständig erklärt. Viele Nodes überschneiden sich mit dem True-Pfad. Damit du nicht alles doppelt anlegen musst, verweisen wir hier auf die bereits erstellten und konfigurierten Nodes.


Verkabelung (Kurzüberblick):

  1. HTTP Request →
  2. Split Out →
  3. Set vods in Array →
  4. Split Out Array →
  5. Merge →
  6. Split In Batches →
  7. SSH Write State →
  8. SSH Download VOD

Node-Einstellungen (1:1 in n8n eintragen)

Important

Die folgenden Nodes haben wir im vorherigen Schritt (False-Pfad) bereits erstellt und konfiguriert. Wenn du dem Tutorial bis hierhin gefolgt bist, musst du an diesen Nodes nichts mehr verändern:

  • HTTP Request Get Videos
  • Split Out data
  • Split In Batches
  • SSH Write State
  • SSH Download VOD

Neu im True-Pfad:

3) Set vods in Array (Node-Name: Set vods in Array)

  • Node-Typ: Set
  • Field: vods
  • Expression:
{{ (typeof $json.vods === 'string' ? $json.vods : String($json.vods))
.split(',')
.map(s => s.trim())
.filter(Boolean) }}

4) Split Out vods (Node-Name: Split Out vods)

  • Node-Typ: Split Out
  • Field to Split Out: vods

5) Merge Combine (Node-Name: Merge Combine)

  • Node-Typ: Merge
  • Mode: Combine
  • Combine Mode: Matching Fields
  • Fields To Match Have Different Names: ON
  • Field 1: vods
  • Field 2: data.id
  • Output Type: Keep Non-Matches
  • Output Data From: Input 2
  • Eingang 1: Split Out Array VODs
  • Eingang 2: Split Out Twitch VODs

Ergebnis

  • Es werden nur neue VODs heruntergeladen und hochgeladen.
  • Die State-Datei wird erweitert, ohne bestehende IDs zu überschreiben.
  • Logs bleiben konsistent, Doppel-Downloads werden vermieden.
  • Jeder Node ist eindeutig benannt, was die Übersicht verbessert.