#!/usr/bin/env bash # # Rippy einrichten — ein Befehl, keine Handarbeit. # # ## Aufbau: erst ALLES prüfen, dann erst ändern # # Das Skript läuft in zwei klar getrennten Phasen: # # Phase 1 PRÜFEN — sieht sich alles an und ändert NICHTS. # (Weiche) — harte Mängel → Abbruch mit Liste, nichts angefasst. # Phase 2 EINRICHTEN — Verzeichnisse, Mount, .env, bauen, starten. # # Warum das wichtig ist (Commander-Einwand 25.07.2026): Die erste Fassung # prüfte und änderte abwechselnd — Verzeichnisse wurden angelegt, bevor # überhaupt klar war, ob der Rest durchläuft. Scheiterte etwas in der Mitte, # blieb ein halb eingerichteter Rechner zurück. Und der Prüf-Modus war als # `if`-Zweig durch fünf Schritte gefädelt; so etwas läuft mit der Zeit # zwangsläufig aus dem Ruder, weil jede neue Aktion daran denken müsste. # # Jetzt gibt es GENAU EINEN Prüf-Pfad, den beide Modi benutzen. `--nur-pruefen` # heißt schlicht: nach Phase 1 aufhören. Damit kann die Prüfung nicht mehr etwas # anderes behaupten als die Installation tut. # # Es ist absichtlich WIEDERHOLBAR: mehrfach ausführen ändert nichts kaputt, und # es ändert NIE einen Wert, der schon in der .env steht. # # Aufruf: sudo ./install.sh # ./install.sh --nur-pruefen (nichts anfassen, nur berichten) set -u NUR_PRUEFEN=0 [ "${1:-}" = "--nur-pruefen" ] && NUR_PRUEFEN=1 MEDIA_DIR="${RIPPY_MEDIA_DIR:-/srv/rippy/media}" MAKEMKV_DIR="${RIPPY_MAKEMKV_DIR:-/srv/rippy/makemkv}" rot() { printf '\033[31m%s\033[0m\n' "$*"; } gruen() { printf '\033[32m%s\033[0m\n' "$*"; } gelb() { printf '\033[33m%s\033[0m\n' "$*"; } titel() { printf '\n\033[1m%s\033[0m\n' "$*"; } # Befunde der Prüfphase. PROBLEME verhindern die Installation, HINWEISE nicht. PROBLEME="" HINWEISE="" problem() { PROBLEME="${PROBLEME}$1"$'\n'; rot " X $1"; } hinweis() { HINWEISE="${HINWEISE}$1"$'\n'; gelb " ! $1"; } ok() { gruen " OK $1"; } # Welches Paketwerkzeug hat dieser Host? (Commander-Frage 26.07.2026: „Was wenn # der Unterbau nicht Debian ist?") Vorher nannte das Skript bei fehlendem # Compose stur `sudo apt install …` — auf Arch, Fedora oder openSUSE ist das # schlicht falsch, und der Nutzer stand vor einem Befehl, den es nicht gibt. # # Erkannt wird über das VORHANDENE Werkzeug, nicht über /etc/os-release: Ein # Derivat kann sich anders nennen als sein Unterbau, aber sein Paketwerkzeug # liegt im PATH. Findet sich keines, wird nichts behauptet. paket_befehl() { if command -v apt-get >/dev/null 2>&1; then echo "sudo apt install docker-compose-plugin" elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install docker-compose-plugin" elif command -v yum >/dev/null 2>&1; then echo "sudo yum install docker-compose-plugin" elif command -v pacman >/dev/null 2>&1; then echo "sudo pacman -S docker-compose" elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install docker-compose" elif command -v apk >/dev/null 2>&1; then echo "sudo apk add docker-cli-compose" else echo "" fi } cd "$(dirname "$0")" || exit 1 echo "======================================================================" echo " Rippy — Einrichtung" echo "======================================================================" echo " Phase 1 von 2: prüfen. Es wird noch NICHTS geändert." # =========================================================================== # PHASE 1 — PRÜFEN. Ab hier bis zur Weiche wird nichts verändert. # =========================================================================== # --- 1. Betriebssystem und Docker ------------------------------------------ titel "Prüfung 1/5 Betriebssystem und Docker" if [ "$(uname -s)" != "Linux" ]; then problem "Kein Linux. Rippy braucht einen Linux-Host mit Docker." rot " (Docker Desktop unter Windows/macOS kann keine optischen Laufwerke" rot " durchreichen — deshalb geht es dort grundsätzlich nicht.)" else ok "Linux erkannt ($(uname -r))" fi if ! command -v docker >/dev/null 2>&1; then problem "Docker ist nicht installiert." rot " Installation: curl -fsSL https://get.docker.com | sudo sh" else ok "Docker gefunden ($(docker --version 2>/dev/null | head -1))" fi COMPOSE="" if docker compose version >/dev/null 2>&1; then COMPOSE="docker compose" ok "Compose-Plugin gefunden" elif command -v docker-compose >/dev/null 2>&1; then COMPOSE="docker-compose" hinweis "Nur das alte docker-compose gefunden — funktioniert, ist aber veraltet." else problem "Docker Compose fehlt." PAKET_BEFEHL="$(paket_befehl)" if [ -n "$PAKET_BEFEHL" ]; then rot " Auf diesem System: $PAKET_BEFEHL" fi # Der Weg, der auf JEDER Distribution funktioniert — Dockers eigenes Skript # bringt das Compose-Plugin mit. Steht immer da, auch wenn oben schon ein # passender Paketbefehl genannt wurde: Paketnamen wandern, dieses Skript nicht. rot " Überall gültig: curl -fsSL https://get.docker.com | sudo sh" fi if [ "$(id -u)" = "0" ]; then ok "Als root gestartet" elif [ "$NUR_PRUEFEN" = 1 ]; then # Die Prüfung ändert nichts und darf deshalb NICHT an root scheitern — # sonst kann man vor der Installation nicht nachsehen, ob alles passt. ok "Ohne root — für die Prüfung völlig in Ordnung" else problem "Bitte mit sudo starten: sudo ./install.sh" rot " (Verzeichnisse unter /srv und die Mount-Propagation brauchen root.)" rot " Nur nachsehen, ohne etwas zu ändern: ./install.sh --nur-pruefen" fi # --- 2. Optisches Laufwerk ------------------------------------------------- titel "Prüfung 2/5 Optisches Laufwerk" # MakeMKV spricht Laufwerke über die SCSI-Generic-Schicht an und braucht # deshalb ZWEI Knoten: /dev/srN und den passenden /dev/sgM. Welche sg-Nummer # dazugehört, ist je Host anders — deshalb wird sie hier über die SCSI-Adresse # ermittelt statt geraten. Beide Knoten zeigen in /sys auf dasselbe # Geräteverzeichnis (z. B. 3:0:0:0), das ist der Abgleich. SR="" SG="" LAUFWERK="" for srpfad in /sys/block/sr*; do [ -e "$srpfad" ] || continue srname=$(basename "$srpfad") adresse=$(basename "$(readlink -f "$srpfad/device" 2>/dev/null)" 2>/dev/null) [ -n "$adresse" ] || continue for sgpfad in /sys/class/scsi_generic/sg*; do [ -e "$sgpfad" ] || continue if [ "$(basename "$(readlink -f "$sgpfad/device" 2>/dev/null)" 2>/dev/null)" = "$adresse" ]; then SR="/dev/$srname" SG="/dev/$(basename "$sgpfad")" LAUFWERK="$(cat "$srpfad/device/vendor" 2>/dev/null) $(cat "$srpfad/device/model" 2>/dev/null)" break 2 fi done done if [ -n "$SR" ]; then ok "Laufwerk gefunden: $(echo "$LAUFWERK" | tr -s ' ')" gruen " $SR + $SG (SCSI-Adresse abgeglichen, nicht geraten)" else hinweis "Kein optisches Laufwerk gefunden — Rippy kann dann nur komprimieren." gelb " Die Installation läuft trotzdem durch; diese Maschine dient dann" gelb " als reine KOMPRIMIER-Maschine für andere Rippy-Instanzen." gelb " Zum Rippen fehlt das Laufwerk. Häufigste Ursachen:" gelb " - In einer VM: Laufwerk per USB-Passthrough durchreichen" gelb " (Proxmox: qm set -usb0 host=:,usb3=1)" gelb " NICHT als emuliertes CD-ROM (media=cdrom) — das kann keine" gelb " SCSI-Kommandos und MakeMKV sieht es nie." gelb " - Auf echter Hardware: 'ls /dev/sr*' prüfen, Kabel/Strom." fi # --- 3. Verzeichnisse und Platz ------------------------------------------- titel "Prüfung 3/5 Verzeichnisse und Plattenplatz" for d in "$MEDIA_DIR" "$MAKEMKV_DIR"; do if [ -d "$d" ]; then ok "$d liegt schon da" else # Anlegen können wir nur mit root — und ob der Elternpfad beschreibbar ist, # entscheidet, ob es später klappt. Beides hier schon sagen. eltern="$(dirname "$d")" while [ -n "$eltern" ] && [ ! -d "$eltern" ]; do eltern="$(dirname "$eltern")"; done if [ "$(id -u)" = "0" ] || [ -w "$eltern" ]; then ok "$d fehlt noch — wird in Phase 2 angelegt" else problem "$d fehlt und $eltern ist nicht beschreibbar." fi fi done FREI_GB=$(df -BG --output=avail "$(dirname "$MEDIA_DIR")" 2>/dev/null | tail -1 | tr -dc '0-9') if [ -n "${FREI_GB:-}" ]; then if [ "$FREI_GB" -lt 60 ]; then hinweis "Nur ${FREI_GB} GB frei. Eine Blu-ray braucht roh rund 40 GB, eine 4K-UHD bis 100 GB." gelb " Rippy läuft trotzdem — lege die Ablage später besser auf eine" gelb " NAS-Freigabe (Einstellungen → Speicherziele)." else ok "${FREI_GB} GB frei" fi fi # --- 4. Mount-Propagation -------------------------------------------------- titel "Prüfung 4/5 Mount-Propagation (für NAS-Freigaben aus dem UI)" # Rippy hängt NAS-Freigaben aus dem UI ein. Damit die auch in den anderen # Containern sichtbar werden, muss der Ablage-Pfad ein 'shared' Mount sein. # Auf den meisten systemd-Hosts ist / schon rshared und es ist nichts zu tun. propagation_ok() { findmnt -no PROPAGATION --target "$MEDIA_DIR" 2>/dev/null | grep -q shared } PROPAGATION_NOETIG=0 if propagation_ok; then ok "$MEDIA_DIR ist bereits 'shared' — nichts zu tun" elif [ ! -d "$MEDIA_DIR" ]; then ok "wird nach dem Anlegen des Verzeichnisses geprüft und bei Bedarf gesetzt" PROPAGATION_NOETIG=1 else ok "Propagation fehlt — wird in Phase 2 eingerichtet" PROPAGATION_NOETIG=1 fi # --- 5. Konfiguration ------------------------------------------------------ titel "Prüfung 5/5 Konfiguration (.env)" if [ ! -f .env ]; then if [ -f .env.example ]; then ok ".env fehlt — wird in Phase 2 aus .env.example erzeugt" else problem ".env.example fehlt. Ist das wirklich das Rippy-Repo?" fi else ok ".env liegt schon da — bestehende Werte bleiben unangetastet" fi # Was in Phase 2 in die .env geschrieben würde — hier nur ANSAGEN. wirksamer_wert() { schluessel="$1"; standard="$2" wert=$(grep -E "^${schluessel}=" .env 2>/dev/null | tail -1 | cut -d= -f2-) [ -n "${wert:-}" ] && printf '%s' "$wert" || printf '%s' "$standard" } for paar in "OPTICAL_SR:$SR" "OPTICAL_SG:$SG" "MAKEMKV_DATA_HOST:$MAKEMKV_DIR"; do schluessel="${paar%%:*}"; erkannt="${paar#*:}" if grep -qE "^${schluessel}=.+" .env 2>/dev/null; then echo " $schluessel steht schon in der .env — bleibt unverändert" elif [ -n "$erkannt" ]; then echo " $schluessel wird auf $erkannt gesetzt" fi done # Gegenprobe: Zeigen die WIRKSAMEN Gerätepfade auf etwas, das es hier gibt? # Fängt den Fall "'.env' von einem anderen Rechner kopiert" und Tippfehler ab — # sonst startet Docker mit einem Pfad, den es nicht gibt, und die Meldung ist # kryptisch. for paar in "OPTICAL_SR:/dev/sr0" "OPTICAL_SG:/dev/sg1"; do schluessel="${paar%%:*}"; standard="${paar##*:}" # Nur prüfen, was auch wirklich gelten wird if grep -qE "^${schluessel}=.+" .env 2>/dev/null; then wirksam=$(wirksamer_wert "$schluessel" "$standard") elif [ -n "$SR" ]; then continue # wird gleich korrekt gesetzt else wirksam="$standard" fi if [ ! -e "$wirksam" ]; then hinweis "$schluessel zeigt auf $wirksam — dieses Gerät gibt es hier NICHT." if [ -n "$SR" ]; then gelb " Gefunden wurde stattdessen: $SR + $SG" gelb " Trage das in der .env ein (oder lösche die Zeile $schluessel=)." fi fi done echo " API-Keys (TMDB/OMDb) brauchst du hier NICHT einzutragen —" echo " der Einrichtungs-Assistent im Browser fragt sie ab und prüft sie." # =========================================================================== # WEICHE — hier wird entschieden. Bis hierher wurde NICHTS verändert. # =========================================================================== titel "Ergebnis der Prüfung" ANZ_PROBLEME=$(printf '%s' "$PROBLEME" | grep -c . || true) ANZ_HINWEISE=$(printf '%s' "$HINWEISE" | grep -c . || true) if [ "$ANZ_PROBLEME" -gt 0 ]; then rot " $ANZ_PROBLEME Punkt(e) verhindern die Installation:" printf '%s' "$PROBLEME" | grep . | while IFS= read -r z; do rot " - $z"; done rot "" rot " Es wurde NICHTS verändert. Bitte das Obige nachholen und erneut starten." exit 1 fi if [ "$ANZ_HINWEISE" -gt 0 ]; then gelb " Alles Wichtige ist da. $ANZ_HINWEISE Hinweis(e), die dich nicht aufhalten:" printf '%s' "$HINWEISE" | grep . | while IFS= read -r z; do gelb " - $z"; done else gruen " Alles in Ordnung, keine Einschränkungen." fi if [ "$NUR_PRUEFEN" = 1 ]; then echo "" gruen "Prüfung fertig — es wurde nichts verändert." echo "Zum Installieren: sudo ./install.sh" exit 0 fi # =========================================================================== # PHASE 2 — EINRICHTEN. Erst ab hier wird etwas verändert. # =========================================================================== echo "" echo "======================================================================" echo " Phase 2 von 2: einrichten" echo "======================================================================" titel "Schritt 1/4 Verzeichnisse anlegen" for d in "$MEDIA_DIR" "$MAKEMKV_DIR"; do if [ -d "$d" ]; then ok "$d war schon da" else mkdir -p "$d" && ok "$d angelegt" fi done echo " Ablage: $MEDIA_DIR (hier landen die fertigen Filme)" echo " MakeMKV-Daten: $MAKEMKV_DIR (Schlüsselspeicher, rebuild-fest)" titel "Schritt 2/4 Mount-Propagation einrichten" if propagation_ok; then ok "war schon 'shared' — nichts zu tun" elif [ "$PROPAGATION_NOETIG" = 1 ]; then mount --bind "$MEDIA_DIR" "$MEDIA_DIR" 2>/dev/null mount --make-rshared "$MEDIA_DIR" 2>/dev/null if propagation_ok; then ok "Propagation eingerichtet" # Neustart-fest machen, sonst ist es nach dem nächsten Reboot wieder weg # und das Einhängen aus dem UI scheitert stillschweigend. UNIT="/etc/systemd/system/$(systemd-escape -p --suffix=mount "$MEDIA_DIR" 2>/dev/null)" if [ -n "${UNIT:-}" ] && [ ! -e "$UNIT" ] && command -v systemctl >/dev/null 2>&1; then cat > "$UNIT" </dev/null if systemctl enable "$(basename "$UNIT")" >/dev/null 2>&1; then ok "neustart-fest gemacht ($(basename "$UNIT"))" else gelb " ! Konnte nicht neustart-fest gemacht werden — nach einem Reboot" gelb " dieses Skript einfach erneut ausführen." fi fi else gelb " ! Propagation ließ sich nicht einrichten." gelb " Rippy läuft trotzdem — nur das Einhängen von NAS-Freigaben AUS DEM UI" gelb " funktioniert dann nicht. Freigaben stattdessen auf dem Host einhängen," gelb " z. B. unter $MEDIA_DIR/nas. Betrifft nur klassisches Docker mit" gelb " SYS_ADMIN — nicht rootless/Podman." fi fi titel "Schritt 3/4 Konfiguration schreiben" if [ ! -f .env ]; then cp .env.example .env && ok ".env aus .env.example erzeugt" fi # Setzt einen Wert NUR, wenn er noch nicht (unkommentiert) gesetzt ist. setze_wenn_leer() { schluessel="$1"; wert="$2" [ -f .env ] || return if grep -qE "^${schluessel}=.+" .env; then echo " $schluessel steht schon in der .env — unverändert gelassen" else sed -i "/^#*${schluessel}=/d" .env printf '%s=%s\n' "$schluessel" "$wert" >> .env ok "$schluessel=$wert eingetragen" fi } if [ -n "$SR" ]; then setze_wenn_leer OPTICAL_SR "$SR" setze_wenn_leer OPTICAL_SG "$SG" fi setze_wenn_leer MAKEMKV_DATA_HOST "$MAKEMKV_DIR" titel "Schritt 4/4 Bauen und starten" echo " Der erste Bau dauert je nach Internet 3-10 Minuten. Nur beim ersten Mal." echo "" if ! $COMPOSE up -d --build; then rot "" rot "Der Start ist gescheitert. Die häufigsten Ursachen:" rot " - Kein Platz auf der Platte: df -h /" rot " - Port 80 schon belegt: ss -tlnp | grep ':80 '" rot " - Download von makemkv.com gedrosselt (Cloudflare): Tarballs von" rot " makemkv.com/download nach docker/worker/vendor/ legen, dann erneut." rot "Die volle Meldung steht oben. Logs: $COMPOSE logs --tail=50" exit 1 fi IP=$(ip -4 route get 1.1.1.1 2>/dev/null | grep -oE 'src [0-9.]+' | awk '{print $2}') [ -n "${IP:-}" ] || IP="" echo "" echo "======================================================================" gruen " Fertig. Rippy läuft." echo "======================================================================" echo "" echo " Jetzt im Browser öffnen: http://$IP" echo "" echo " Dort startet der Einrichtungs-Assistent. Er zeigt dir, ob Laufwerk," echo " Worker und Platz in Ordnung sind, und stellt die Kompression passend" echo " zu deiner CPU ein. Mehr ist nicht zu tun." echo "" echo " Status ansehen: $COMPOSE ps" echo " Logs ansehen: $COMPOSE logs -f" echo " Aktualisieren: git pull && sudo ./install.sh" echo ""