#!/bin/sh # TiviGrid Recorder installer for Linux (x86_64, Raspberry Pi / arm64 / armv7) and macOS. # # curl -fsSL https://tivigrid.com/Server/install.sh | sh # curl -fsSL .../Server/install.sh | sh -s -- --user --port 8765 # options after "sh -s --" # sh Server/install.sh # from a checkout # # Linux (default): /opt/xtreamtv-recorder, data in /var/lib/xtreamtv-recorder, runs as the system user # "xtreamtv" under systemd (needs sudo). Advertised over Bonjour via avahi if present. # Linux --user: ~/.local/share/xtreamtv-recorder, a systemd user service (no sudo). # macOS: ~/Library/Application Support/XtreamTV Recorder, a launchd LaunchAgent (no sudo) # run under `caffeinate -i` so the Mac doesn't idle-sleep while the recorder runs. # # Re-running upgrades in place and keeps the port, data folder and access token. Python 3.9+ only; # nothing is installed with pip. set -eu RAW_BASE="${XTREAMTV_RAW_BASE:-https://tivigrid.com}" SCRIPT=xtreamtv_recorder.py SERVICE=xtreamtv-recorder SERVICE_USER=xtreamtv LABEL=com.xtreamtv.recorder DEFAULT_PORT=8765 ACTION=install USER_MODE=0 DRY_RUN=0 PURGE=0 ASSUME_YES=0 PORT="" DATA_DIR="" # -- output --------------------------------------------------------------------------------------- if [ -t 1 ]; then BOLD=$(printf '\033[1m'); DIM=$(printf '\033[2m'); PINK=$(printf '\033[38;5;204m') LIME=$(printf '\033[38;5;155m'); GOLD=$(printf '\033[38;5;215m'); RESET=$(printf '\033[0m') else BOLD=""; DIM=""; PINK=""; LIME=""; GOLD=""; RESET="" fi say() { printf '%s\n' "$*"; } step() { printf '%s==>%s %s\n' "$PINK" "$RESET" "$*"; } ok() { printf '%s✓%s %s\n' "$LIME" "$RESET" "$*"; } warn() { printf '%s!%s %s\n' "$GOLD" "$RESET" "$*" >&2; } die() { printf '%serror:%s %s\n' "$PINK" "$RESET" "$*" >&2; exit 1; } usage() { cat <&2; die "unknown option: $1" ;; esac shift done if [ -n "$PORT" ]; then case "$PORT" in ''|*[!0-9]*) die "--port must be a number" ;; esac [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] || die "--port must be between 1 and 65535" fi if [ "$PURGE" = 1 ] && [ "$ACTION" != uninstall ]; then die "--purge only goes with --uninstall" fi # -- helpers -------------------------------------------------------------------------------------- # Runs a command, or only prints it with --dry-run. run() { if [ "$DRY_RUN" = 1 ]; then printf '%s+ %s%s\n' "$DIM" "$*" "$RESET" else "$@" fi } # Runs a command as root in system mode (through sudo when needed), as the current user otherwise. as_root() { if [ -n "$SUDO" ]; then run "$SUDO" "$@"; else run "$@"; fi } # Read-only command that may need root to see inside the service's data folder (not sudo in a dry run). root_read() { if [ -n "$SUDO" ] && [ "$DRY_RUN" = 0 ]; then "$SUDO" "$@"; else "$@"; fi } # write_file DEST MODE: writes stdin to DEST (as root in system mode). write_file() { if [ "$DRY_RUN" = 1 ]; then printf '%s+ write %s (mode %s):%s\n' "$DIM" "$1" "$2" "$RESET" sed 's/^/ /' return fi tmp=$(mktemp) cat >"$tmp" if [ -n "$SUDO" ]; then "$SUDO" mkdir -p "$(dirname "$1")" "$SUDO" install -m "$2" "$tmp" "$1" else mkdir -p "$(dirname "$1")" install -m "$2" "$tmp" "$1" fi rm -f "$tmp" } # Asks a yes/no question. Reads from the terminal, since stdin is the script itself under `curl | sh`. ask() { [ "$ASSUME_YES" = 1 ] && return 0 if (: /dev/null; then # a terminal exists (not when run from cron or a container) printf '%s [Y/n] ' "$1" >/dev/tty read -r answer /\>/g' } # Quotes one argument for a systemd ExecStart= line. systemd_quote() { printf '"%s"' "$(printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/%/%%/g')" } download() { # download URL DEST if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 2 -o "$2" "$1" elif command -v wget >/dev/null 2>&1; then wget -q -O "$2" "$1" else "$PY" -c 'import sys, urllib.request; urllib.request.urlretrieve(sys.argv[1], sys.argv[2])' "$1" "$2" fi } # -- platform ------------------------------------------------------------------------------------- OS=$(uname -s) ARCH=$(uname -m) case "$OS" in Darwin) PLATFORM=macos ;; Linux) PLATFORM=linux ;; *) die "$OS isn't supported by this installer. On Windows, use Docker Desktop (see Server/README.md)." ;; esac MODEL="" if [ -r /proc/device-tree/model ]; then MODEL=$(tr -d '\0' /dev/null || true) fi HOME_DIR=${HOME:?HOME is not set} SYSTEM_PREFIX=/opt/xtreamtv-recorder USER_PREFIX="${XDG_DATA_HOME:-$HOME_DIR/.local/share}/xtreamtv-recorder" MAC_PREFIX="$HOME_DIR/Library/Application Support/XtreamTV Recorder" if [ "$PLATFORM" = macos ]; then MODE=macos elif [ "$USER_MODE" = 1 ]; then MODE=user elif [ "$ACTION" != install ] && [ ! -f "$SYSTEM_PREFIX/install.conf" ] && [ -f "$USER_PREFIX/install.conf" ]; then MODE=user # --status/--update/--uninstall of an existing --user install else MODE=system fi case "$MODE" in macos) PREFIX=$MAC_PREFIX DEFAULT_DATA="$PREFIX/data" PLIST="$HOME_DIR/Library/LaunchAgents/$LABEL.plist" LOG_FILE="$HOME_DIR/Library/Logs/XtreamTVRecorder.log" ;; user) PREFIX=$USER_PREFIX DEFAULT_DATA="$PREFIX/data" UNIT="${XDG_CONFIG_HOME:-$HOME_DIR/.config}/systemd/user/$SERVICE.service" ;; system) PREFIX=$SYSTEM_PREFIX DEFAULT_DATA=/var/lib/xtreamtv-recorder UNIT=/etc/systemd/system/$SERVICE.service AVAHI_FILE=/etc/avahi/services/$SERVICE.service ;; esac CONF="$PREFIX/install.conf" SUDO="" if [ "$MODE" = system ] && [ "$(id -u)" -ne 0 ]; then if command -v sudo >/dev/null 2>&1; then SUDO=sudo else die "a system-wide install needs root. Run as root, or use --user to install for your account only." fi fi if [ "$MODE" = macos ] && [ "$(id -u)" -eq 0 ]; then die "on macOS, run the installer as your normal user (without sudo)." fi has_systemd() { command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ] } # systemctl for this mode (system or --user). sctl() { if [ "$MODE" = user ]; then run systemctl --user "$@"; else as_root systemctl "$@"; fi } # -- python --------------------------------------------------------------------------------------- python_ok() { [ -x "$1" ] && "$1" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 9) else 1)' >/dev/null 2>&1 } find_python() { candidates="" if [ "$PLATFORM" = macos ]; then candidates="/opt/homebrew/bin/python3 /usr/local/bin/python3" # /usr/bin/python3 is a stub that opens an install dialog unless the Command Line Tools exist. if xcode-select -p >/dev/null 2>&1; then candidates="$candidates /usr/bin/python3"; fi else candidates="/usr/bin/python3 /usr/local/bin/python3" fi found=$(command -v python3 2>/dev/null || true) for candidate in $candidates $found; do if python_ok "$candidate"; then printf '%s' "$candidate" return 0 fi done return 1 } ensure_python() { if PY=$(find_python); then return fi if [ "$PLATFORM" = macos ]; then die "Python 3.9 or newer is needed. Install the Command Line Tools with \`xcode-select --install\` (or \`brew install python\`), then run this again." fi if command -v apt-get >/dev/null 2>&1; then if ask "Python 3 is needed. Install it now with apt-get?"; then apt_sudo="" [ "$(id -u)" -ne 0 ] && apt_sudo=sudo run $apt_sudo apt-get update run $apt_sudo apt-get install -y python3 [ "$DRY_RUN" = 1 ] && { PY=/usr/bin/python3; return; } PY=$(find_python) || die "python3 still isn't available." return fi die "Python 3.9 or newer is needed: sudo apt-get install -y python3" fi if command -v dnf >/dev/null 2>&1; then die "Python 3.9 or newer is needed: sudo dnf install -y python3"; fi if command -v apk >/dev/null 2>&1; then die "Python 3.9 or newer is needed: sudo apk add python3"; fi if command -v pacman >/dev/null 2>&1; then die "Python 3.9 or newer is needed: sudo pacman -S python"; fi die "Python 3.9 or newer is needed. Install python3 with your package manager and run this again." } # Runs the recorder script as the user the service runs as (so files it creates have the right owner). recorder_cli() { if [ "$MODE" != system ]; then "$PY" "$PREFIX/$SCRIPT" "$@" elif [ "$(id -u)" -ne 0 ]; then (cd / && sudo -u "$SERVICE_USER" "$PY" "$PREFIX/$SCRIPT" "$@") elif command -v runuser >/dev/null 2>&1; then (cd / && runuser -u "$SERVICE_USER" -- "$PY" "$PREFIX/$SCRIPT" "$@") else # shellcheck disable=SC2016 # "$0" "$@" are expanded by the inner shell (cd / && su -s /bin/sh "$SERVICE_USER" -c 'exec "$0" "$@"' -- "$PY" "$PREFIX/$SCRIPT" "$@") fi } ping_recorder() { "$PY" -c 'import sys, urllib.request; urllib.request.urlopen("http://127.0.0.1:%s/api/ping" % sys.argv[1], timeout=2)' \ "$1" >/dev/null 2>&1 } port_in_use() { "$PY" -c 'import socket, sys; s = socket.socket(); s.settimeout(0.5); sys.exit(0 if s.connect_ex(("127.0.0.1", int(sys.argv[1]))) == 0 else 1)' \ "$1" >/dev/null 2>&1 } load_conf() { INSTALLED=0 if [ -f "$CONF" ]; then INSTALLED=1 # shellcheck disable=SC1090 conf_port=$(. "$CONF" && printf '%s' "${PORT:-}") # shellcheck disable=SC1090 conf_data=$(. "$CONF" && printf '%s' "${DATA_DIR:-}") [ -n "$PORT" ] || PORT=$conf_port [ -n "$DATA_DIR" ] || DATA_DIR=$conf_data fi [ -n "$PORT" ] || PORT=$DEFAULT_PORT [ -n "$DATA_DIR" ] || DATA_DIR=$DEFAULT_DATA case "$DATA_DIR" in /*) ;; "~"/*) DATA_DIR="$HOME_DIR/${DATA_DIR#"~/"}" ;; *) DATA_DIR="$(pwd)/$DATA_DIR" ;; esac } # -- the recorder program ------------------------------------------------------------------------- # The installer's own folder when run from a checkout (not when piped from curl, nor the installed copy). local_source_dir() { case "$0" in */install.sh|install.sh) ;; *) return 1 ;; esac [ -f "$0" ] || return 1 dir=$(cd "$(dirname "$0")" && pwd) [ "$dir" != "$PREFIX" ] && [ -f "$dir/$SCRIPT" ] || return 1 printf '%s' "$dir" } fetch_program() { WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT if SOURCE_DIR=$(local_source_dir); then step "Using $SCRIPT from $SOURCE_DIR" cp "$SOURCE_DIR/$SCRIPT" "$WORK/$SCRIPT" cp "$SOURCE_DIR/install.sh" "$WORK/install.sh" else step "Downloading the recorder from $RAW_BASE" download "$RAW_BASE/Server/$SCRIPT" "$WORK/$SCRIPT" || die "couldn't download $RAW_BASE/Server/$SCRIPT" download "$RAW_BASE/Server/install.sh" "$WORK/install.sh" 2>/dev/null || rm -f "$WORK/install.sh" fi # Refuse anything that isn't the recorder (an HTML error page, a truncated download). if ! grep -q "TiviGrid Recorder" "$WORK/$SCRIPT" 2>/dev/null \ || ! "$PY" -c 'import ast, sys; ast.parse(open(sys.argv[1], "rb").read())' "$WORK/$SCRIPT"; then die "the downloaded $SCRIPT is not valid; nothing was changed." fi NEW_VERSION=$(sed -n 's/^VERSION = "\(.*\)"/\1/p' "$WORK/$SCRIPT" | head -n 1) } install_program() { step "Installing TiviGrid Recorder ${NEW_VERSION:-} to $PREFIX" if [ "$MODE" = system ]; then as_root mkdir -p "$PREFIX" as_root install -m 755 "$WORK/$SCRIPT" "$PREFIX/$SCRIPT" if [ -f "$WORK/install.sh" ]; then as_root install -m 755 "$WORK/install.sh" "$PREFIX/install.sh"; fi else run mkdir -p "$PREFIX" run install -m 755 "$WORK/$SCRIPT" "$PREFIX/$SCRIPT" if [ -f "$WORK/install.sh" ]; then run install -m 755 "$WORK/install.sh" "$PREFIX/install.sh"; fi fi write_file "$CONF" 644 </dev/null 2>&1; then return fi step "Creating the system user \"$SERVICE_USER\"" nologin=/usr/sbin/nologin [ -x "$nologin" ] || nologin=/sbin/nologin if command -v useradd >/dev/null 2>&1; then as_root useradd --system --user-group --no-create-home --home-dir "$DATA_DIR" --shell "$nologin" "$SERVICE_USER" elif command -v adduser >/dev/null 2>&1; then # BusyBox (Alpine) as_root addgroup -S "$SERVICE_USER" as_root adduser -S -D -H -h "$DATA_DIR" -s "$nologin" -G "$SERVICE_USER" "$SERVICE_USER" else die "can't create the user $SERVICE_USER (no useradd/adduser). Use --user instead." fi } prepare_data_dir() { if [ "$MODE" != system ]; then run mkdir -p "$DATA_DIR" return fi as_root mkdir -p "$DATA_DIR" # Take over the folder only if it's empty or already a recorder data folder: never chown -R a # folder that holds other things (e.g. --data /home/pi by mistake). if [ -z "$(root_read ls -A "$DATA_DIR" 2>/dev/null)" ] || root_read test -f "$DATA_DIR/config.json" \ || root_read test -f "$DATA_DIR/recordings.json"; then as_root chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR" else as_root chown "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR" warn "$DATA_DIR already contains other files; only the folder itself was given to $SERVICE_USER." fi as_root chmod 750 "$DATA_DIR" } # -- services ------------------------------------------------------------------------------------- install_avahi() { # Bonjour on Linux: a static avahi service file advertises the recorder for the app to discover. AVAHI_INSTALLED=0 [ "$MODE" = system ] || return 0 [ -d /etc/avahi/services ] || return 0 write_file "$AVAHI_FILE" 644 < TiviGrid Recorder on %h _xtreamtv._tcp $PORT v=1 EOF AVAHI_INSTALLED=1 ok "Bonjour: advertised as _xtreamtv._tcp through avahi" } install_systemd() { if has_systemd && [ "$MODE" = user ] && [ "$DRY_RUN" = 0 ] && ! systemctl --user show-environment >/dev/null 2>&1; then warn "your systemd user session isn't reachable (common after \`su\`; log in directly, e.g. over SSH)," warn "so no service was set up. Fix it and re-run, or install system-wide without --user. Meanwhile, run:" say " $PY \"$PREFIX/$SCRIPT\" --data \"$DATA_DIR\" --port $PORT" NO_SERVICE=1 return fi if ! has_systemd; then warn "systemd isn't running here, so no service was set up. Start the recorder with:" as_user="" [ "$MODE" = system ] && as_user="sudo -u $SERVICE_USER " say " $as_user$PY \"$PREFIX/$SCRIPT\" --data \"$DATA_DIR\" --port $PORT" NO_SERVICE=1 return fi exec_start="$(systemd_quote "$PY") $(systemd_quote "$PREFIX/$SCRIPT") --data $(systemd_quote "$DATA_DIR") --port $PORT" # avahi already advertises the system service; the recorder itself does it otherwise. if [ "${AVAHI_INSTALLED:-0}" = 1 ]; then exec_start="$exec_start --no-mdns"; fi # Wait for the disk holding the recordings (e.g. a USB drive on a Raspberry Pi) to be mounted. case "$DATA_DIR" in *" "*) mounts="# (data folder has a space: RequiresMountsFor not set)" ;; *) mounts="RequiresMountsFor=$DATA_DIR" ;; esac if [ "$MODE" = system ]; then write_file "$UNIT" 644 </dev/null \ || warn "to keep the recorder running after you log out, run: sudo loginctl enable-linger $(id -un)" fi fi } install_launchd() { py_xml=$(xml_escape "$PY") script_xml=$(xml_escape "$PREFIX/$SCRIPT") data_xml=$(xml_escape "$DATA_DIR") prefix_xml=$(xml_escape "$PREFIX") log_xml=$(xml_escape "$LOG_FILE") run mkdir -p "$HOME_DIR/Library/LaunchAgents" "$HOME_DIR/Library/Logs" # caffeinate -i keeps the Mac from idle-sleeping while the recorder runs (the display may still sleep). # The recorder advertises itself over Bonjour with dns-sd as long as it runs. write_file "$PLIST" 644 < Label $LABEL ProgramArguments /usr/bin/caffeinate -i $py_xml $script_xml --data $data_xml --port $PORT WorkingDirectory $prefix_xml EnvironmentVariables PATH /usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin PYTHONUNBUFFERED 1 RunAtLoad KeepAlive ThrottleInterval 10 StandardOutPath $log_xml StandardErrorPath $log_xml EOF domain="gui/$(id -u)" run launchctl bootout "$domain/$LABEL" 2>/dev/null || true run launchctl enable "$domain/$LABEL" 2>/dev/null || true if [ "$DRY_RUN" = 1 ]; then run launchctl bootstrap "$domain" "$PLIST" return fi # Right after bootout, bootstrap can fail briefly ("Input/output error"): retry a few times. tries=0 until launchctl bootstrap "$domain" "$PLIST" 2>/dev/null; do tries=$((tries + 1)) if [ "$tries" -ge 5 ]; then launchctl load -w "$PLIST" 2>/dev/null || die "launchctl couldn't start $LABEL (see $LOG_FILE)" break fi sleep 1 done } stop_service() { case "$MODE" in macos) run launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true ;; *) if has_systemd; then sctl disable --now "$SERVICE.service" 2>/dev/null || true fi ;; esac } service_state() { case "$MODE" in macos) if launchctl print "gui/$(id -u)/$LABEL" >/dev/null 2>&1; then launchctl print "gui/$(id -u)/$LABEL" 2>/dev/null | sed -n 's/^[[:space:]]*state = //p' | head -n 1 else printf 'not loaded' fi ;; user) systemctl --user is-active "$SERVICE.service" 2>/dev/null || true ;; system) systemctl is-active "$SERVICE.service" 2>/dev/null || true ;; esac } wait_until_up() { i=0 while [ $i -lt 30 ]; do if ping_recorder "$PORT"; then return 0; fi sleep 1 i=$((i + 1)) done return 1 } logs_hint() { case "$MODE" in macos) printf 'tail -f "%s"' "$LOG_FILE" ;; user) printf 'journalctl --user -u %s -f' "$SERVICE" ;; system) printf 'journalctl -u %s -f' "$SERVICE" ;; esac } firewall_hints() { if [ "$PLATFORM" = macos ]; then if /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null | grep -q enabled; then warn "The macOS firewall is on: if the iPhone can't connect, allow incoming connections for $PY" warn "(System Settings › Network › Firewall › Options)." fi return fi if command -v ufw >/dev/null 2>&1 && [ "$(id -u)" -eq 0 ] && ufw status 2>/dev/null | grep -q "Status: active"; then warn "ufw is active: allow the recorder with sudo ufw allow $PORT/tcp" elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then warn "firewalld is active: sudo firewall-cmd --permanent --add-port=$PORT/tcp && sudo firewall-cmd --reload" fi } print_pairing() { if [ "$DRY_RUN" = 1 ]; then say " (dry run: the address, access token and pairing link are printed here after a real install)" return fi if [ "$MODE" = system ] && ! id "$SERVICE_USER" >/dev/null 2>&1; then return fi qr="" [ -t 1 ] && qr=--qr # the output goes through sed, so tell the recorder it's a terminal recorder_cli --data "$DATA_DIR" --port "$PORT" --pairing $qr | sed 's/^/ /' say "" } print_manage() { manager="sh \"$PREFIX/install.sh\"" say " ${BOLD}Pair:${RESET} open the dashboard on the iPhone and tap ${BOLD}Open in TiviGrid${RESET}," say " scan the QR code with the iPhone camera, or enter the address and token in" say " TiviGrid › Settings › Recording Server." say " ${BOLD}Logs:${RESET} $(logs_hint)" say " ${BOLD}Status:${RESET} $manager --status" say " ${BOLD}Update:${RESET} $manager --update" say " ${BOLD}Uninstall:${RESET} $manager --uninstall (keeps recordings; add --purge to delete them)" } # -- actions -------------------------------------------------------------------------------------- describe_platform() { where=$PLATFORM [ -n "$MODEL" ] && where="$where, $MODEL" case "$MODE" in system) kind="system-wide service" ;; user) kind="user service" ;; macos) kind="LaunchAgent" ;; esac step "TiviGrid Recorder installer — $where ($ARCH), $kind" [ "$DRY_RUN" = 1 ] && say " ${DIM}dry run: nothing will be changed${RESET}" return 0 } do_install() { describe_platform ensure_python ok "Python: $PY ($("$PY" -c 'import platform; print(platform.python_version())'))" load_conf if [ "$ACTION" = update ] && [ "$INSTALLED" = 0 ]; then die "the recorder isn't installed here ($CONF is missing). Run the installer without --update." fi if [ "$INSTALLED" = 0 ] && [ "$DRY_RUN" = 0 ] && port_in_use "$PORT"; then die "port $PORT is already in use. Pick another one with --port." fi fetch_program [ "$MODE" = system ] && ensure_service_user prepare_data_dir install_program NO_SERVICE=0 case "$MODE" in macos) install_launchd ;; *) install_avahi; install_systemd ;; esac if [ "$DRY_RUN" = 1 ]; then say "" ok "Dry run finished." print_pairing return fi if [ "$NO_SERVICE" = 0 ]; then if wait_until_up; then ok "TiviGrid Recorder ${NEW_VERSION:-} is running on port $PORT" else warn "the recorder didn't answer on port $PORT yet. Check the logs: $(logs_hint)" fi fi say "" print_pairing print_manage firewall_hints if [ "$PLATFORM" = macos ]; then say "" say " ${DIM}The Mac won't idle-sleep while the recorder runs. A LaunchAgent runs while you're logged in;" say " for a headless Mac, turn on automatic login.${RESET}" fi } do_status() { load_conf if [ "$INSTALLED" = 0 ]; then say "TiviGrid Recorder isn't installed here (no $CONF)." if [ "$MODE" = system ] && [ -f "$USER_PREFIX/install.conf" ]; then say "A --user install exists: run with --user."; fi exit 1 fi ensure_python state=$(service_state) say "${BOLD}TiviGrid Recorder${RESET} ($MODE install)" say " Program: $PREFIX/$SCRIPT" say " Data: $DATA_DIR" say " Service: ${state:-unknown}" if ping_recorder "$PORT"; then say " Health: ${LIME}answering on port $PORT${RESET}" else say " Health: ${GOLD}not answering on port $PORT${RESET} — logs: $(logs_hint)" fi say "" print_pairing } do_uninstall() { load_conf service_file=${PLIST:-${UNIT:-}} if [ "$INSTALLED" = 0 ] && [ ! -f "$service_file" ] && [ ! -f "$PREFIX/$SCRIPT" ]; then say "TiviGrid Recorder isn't installed here ($MODE install: $PREFIX)." return 0 fi step "Removing TiviGrid Recorder ($MODE install)" stop_service case "$MODE" in macos) run rm -f "$PLIST" ;; user) run rm -f "$UNIT"; has_systemd && sctl daemon-reload || true ;; system) as_root rm -f "$UNIT" "$AVAHI_FILE" if has_systemd; then sctl daemon-reload || true; fi ;; esac # Remove only what the installer put there: the default data folder lives inside PREFIX on macOS # and with --user. if [ "$MODE" = system ]; then as_root rm -rf "$PREFIX/$SCRIPT" "$PREFIX/install.sh" "$PREFIX/install.conf" "$PREFIX/__pycache__" else run rm -rf "$PREFIX/$SCRIPT" "$PREFIX/install.sh" "$PREFIX/install.conf" "$PREFIX/__pycache__" fi if [ "$PURGE" = 1 ]; then step "Deleting recordings and settings in $DATA_DIR" # Only the recorder's own files, then the folder if that leaves it empty. if [ "$MODE" = system ]; then as_root rm -rf "$DATA_DIR/recordings" "$DATA_DIR/recordings.json" "$DATA_DIR/recordings.json.tmp" "$DATA_DIR/config.json" as_root rmdir "$DATA_DIR" 2>/dev/null || true else run rm -rf "$DATA_DIR/recordings" "$DATA_DIR/recordings.json" "$DATA_DIR/recordings.json.tmp" "$DATA_DIR/config.json" run rmdir "$DATA_DIR" 2>/dev/null || true fi if [ "$MODE" = system ] && id "$SERVICE_USER" >/dev/null 2>&1; then as_root userdel "$SERVICE_USER" 2>/dev/null || as_root deluser "$SERVICE_USER" 2>/dev/null || true fi fi if [ "$MODE" = system ]; then as_root rmdir "$PREFIX" 2>/dev/null || true; else run rmdir "$PREFIX" 2>/dev/null || true; fi if [ "$DRY_RUN" = 1 ]; then ok "Dry run finished."; return 0; fi ok "TiviGrid Recorder was removed." if [ "$PURGE" = 0 ] && root_read test -d "$DATA_DIR"; then say " Your recordings and access token are still in $DATA_DIR." say " Installing again picks them up; --uninstall --purge deletes them." fi } case "$ACTION" in install|update) do_install ;; status) do_status ;; uninstall) do_uninstall ;; esac