Last active
June 3, 2026 09:21
-
-
Save PedroCavaleiro/e1d5e2b01da159d95b10e5c2356afeaf to your computer and use it in GitHub Desktop.
Upload Assistant Installer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Installs Audionut's fork of Upload Assistant https://github.com/Audionut/Upload-Assistant | |
| # install-upload-assistant.sh | |
| # Must be run as root (sudo or direct root — works in both) | |
| set -euo pipefail | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m' | |
| BOLD='\033[1m'; RESET='\033[0m' | |
| info() { echo -e "${CYAN}${BOLD}[INFO]${RESET} $*"; } | |
| success() { echo -e "${GREEN}${BOLD}[OK]${RESET} $*"; } | |
| warn() { echo -e "${YELLOW}${BOLD}[WARN]${RESET} $*"; } | |
| die() { echo -e "${RED}${BOLD}[ERROR]${RESET} $*" >&2; exit 1; } | |
| [[ "$EUID" -eq 0 ]] || die "This script must be run as root." | |
| if [[ -n "${SUDO_USER:-}" ]]; then | |
| REAL_USER="$SUDO_USER" | |
| REAL_HOME="$(getent passwd "$REAL_USER" | cut -d: -f6)" | |
| else | |
| REAL_USER="root" | |
| REAL_HOME="$HOME" | |
| fi | |
| NVM_DIR="${REAL_HOME}/.nvm" | |
| INSTALL_DIR="/opt/Upload-Assistant" | |
| CONFIG_JSON="${INSTALL_DIR}/data/config.json" | |
| UA_BIN="/usr/bin/upload-assistant" | |
| info "Cloning Upload-Assistant into ${INSTALL_DIR} ..." | |
| if [[ -d "$INSTALL_DIR/.git" ]]; then | |
| warn "Directory already exists — fetching latest instead of cloning." | |
| git -C "$INSTALL_DIR" fetch --all --tags --quiet | |
| else | |
| git clone https://github.com/Audionut/Upload-Assistant.git "$INSTALL_DIR" | |
| fi | |
| cd "$INSTALL_DIR" | |
| info "Fetching all tags ..." | |
| git fetch --all --tags --quiet | |
| LATEST_TAG="$(git tag --sort=-version:refname | head -n 1)" | |
| if [[ -z "$LATEST_TAG" ]]; then | |
| warn "No tags found; staying on default branch." | |
| else | |
| info "Checking out latest tag: ${BOLD}${LATEST_TAG}${RESET}" | |
| git checkout "tags/${LATEST_TAG}" --quiet | |
| success "Checked out ${LATEST_TAG}" | |
| fi | |
| info "Installing system packages (python3-venv, pip, ffmpeg, curl) ..." | |
| apt-get update -qq | |
| apt-get install -y python3-venv python3-pip ffmpeg curl -qq | |
| success "System packages installed." | |
| info "Installing nvm for user: ${REAL_USER} (home: ${REAL_HOME}) ..." | |
| _run_as_real_user() { | |
| if [[ "$REAL_USER" == "root" ]] || [[ "$(whoami)" == "$REAL_USER" ]]; then | |
| HOME="$REAL_HOME" bash -c "$1" | |
| elif command -v sudo &>/dev/null; then | |
| sudo -u "$REAL_USER" HOME="$REAL_HOME" bash -c "$1" | |
| else | |
| die "Cannot run commands as '${REAL_USER}': sudo is not available." | |
| fi | |
| } | |
| _run_as_real_user " | |
| export HOME='${REAL_HOME}' | |
| export NVM_DIR='${NVM_DIR}' | |
| curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash | |
| . \"\${NVM_DIR}/nvm.sh\" | |
| nvm install 24 | |
| nvm use 24 | |
| nvm alias default 24 | |
| " | |
| # Load nvm into the current shell session | |
| export NVM_DIR="${NVM_DIR}" | |
| # shellcheck disable=SC1091 | |
| [ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh" | |
| # Symlink node/npm/npx into /usr/local/bin so they are on PATH for all users | |
| # without needing to source nvm in every shell | |
| NODE_BIN_DIR="$(dirname "$(. "${NVM_DIR}/nvm.sh" && nvm which current)")" | |
| info "Symlinking node, npm, npx from ${NODE_BIN_DIR} into /usr/local/bin ..." | |
| ln -sf "${NODE_BIN_DIR}/node" /usr/local/bin/node | |
| ln -sf "${NODE_BIN_DIR}/npm" /usr/local/bin/npm | |
| ln -sf "${NODE_BIN_DIR}/npx" /usr/local/bin/npx | |
| success "Node.js $(node --version) installed via nvm." | |
| # Install pm2 globally | |
| if ! command -v pm2 &>/dev/null; then | |
| info "Installing pm2 via npm ..." | |
| npm install -g pm2 --quiet | |
| fi | |
| # Symlink pm2 into /usr/local/bin as well | |
| PM2_PATH="$(which pm2 2>/dev/null || true)" | |
| if [[ -n "$PM2_PATH" ]] && [[ "$PM2_PATH" != "/usr/local/bin/pm2" ]]; then | |
| ln -sf "$PM2_PATH" /usr/local/bin/pm2 | |
| fi | |
| success "pm2 $(pm2 --version) installed and symlinked." | |
| info "Creating Python virtual environment ..." | |
| python3 -m venv "${INSTALL_DIR}/venv" | |
| # shellcheck disable=SC1091 | |
| source "${INSTALL_DIR}/venv/bin/activate" | |
| success "Virtual environment activated." | |
| REQ_FILE="" | |
| for f in requirements.txt requirements; do | |
| [[ -f "${INSTALL_DIR}/${f}" ]] && REQ_FILE="${INSTALL_DIR}/${f}" && break | |
| done | |
| if [[ -n "$REQ_FILE" ]]; then | |
| info "Installing Python requirements from ${REQ_FILE} ..." | |
| pip install --quiet -r "$REQ_FILE" | |
| success "Python requirements installed." | |
| else | |
| warn "No requirements file found — skipping pip install." | |
| fi | |
| echo "" | |
| echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| echo -e "${BOLD} Configuration Setup${RESET}" | |
| echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| echo -e " ${CYAN}a${RESET}) Automatically generate config (runs config-generator.py)" | |
| echo -e " ${CYAN}m${RESET}) Manually edit config (copies example and opens nano)" | |
| echo "" | |
| read -rp "$(echo -e "${BOLD}Your choice [a/m]:${RESET} ")" CONFIG_CHOICE | |
| case "${CONFIG_CHOICE,,}" in | |
| a|auto) | |
| info "Running automatic config generator ..." | |
| python3 "${INSTALL_DIR}/config-generator.py" || die "config-generator.py failed." | |
| success "Config generated automatically." | |
| ;; | |
| m|manual|*) | |
| info "Copying example config and opening nano ..." | |
| cp "${INSTALL_DIR}/data/example-config.py" "${INSTALL_DIR}/config.py" | |
| nano "${INSTALL_DIR}/config.py" | |
| success "Manual config saved." | |
| ;; | |
| esac | |
| echo "" | |
| echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| echo -e "${BOLD} Storage Paths${RESET}" | |
| echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| echo -e "Enter one or more paths where media files are stored." | |
| echo -e "Wrap paths that contain spaces in ${BOLD}double quotes${RESET}." | |
| echo -e "Example: ${CYAN}/mnt/sda1/downloads \"/mnt/sda1/my downloads\" /mnt/sdb1/media${RESET}" | |
| echo "" | |
| read -rp "$(echo -e "${BOLD}Paths:${RESET} ")" RAW_PATHS | |
| MEDIA_PATHS=() | |
| eval "MEDIA_PATHS=($RAW_PATHS)" | |
| if [[ ${#MEDIA_PATHS[@]} -eq 0 ]]; then | |
| die "No paths provided." | |
| fi | |
| echo "" | |
| echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| echo -e "${BOLD} Web UI Bind Address${RESET}" | |
| echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| read -rp "$(echo -e "${BOLD}Bind address [default: 0.0.0.0:8080]:${RESET} ")" BIND_INPUT | |
| BIND="${BIND_INPUT:-0.0.0.0:8080}" | |
| mkdir -p "${INSTALL_DIR}/data" | |
| PATHS_JSON="[" | |
| for i in "${!MEDIA_PATHS[@]}"; do | |
| path="${MEDIA_PATHS[$i]}" | |
| path_escaped="${path//\\/\\\\}" | |
| path_escaped="${path_escaped//\"/\\\"}" | |
| [[ $i -gt 0 ]] && PATHS_JSON+="," | |
| PATHS_JSON+="\"${path_escaped}\"" | |
| done | |
| PATHS_JSON+="]" | |
| cat > "$CONFIG_JSON" <<EOF | |
| { | |
| "paths": ${PATHS_JSON}, | |
| "bind": "${BIND}" | |
| } | |
| EOF | |
| success "Saved configuration to ${CONFIG_JSON}" | |
| info "Generating ${UA_BIN} control script ..." | |
| cat > "$UA_BIN" << 'UASCRIPT_EOF' | |
| #!/usr/bin/env bash | |
| # upload-assistant — control script for Upload-Assistant | |
| # Auto-generated by install-upload-assistant.sh | |
| set -euo pipefail | |
| INSTALL_DIR="/opt/Upload-Assistant" | |
| CONFIG_JSON="${INSTALL_DIR}/data/config.json" | |
| VENV_PYTHON="${INSTALL_DIR}/venv/bin/python3" | |
| PM2_APP_NAME="upload-assistant" | |
| # node/npm/pm2 are symlinked into /usr/local/bin at install time — | |
| # no nvm sourcing needed at runtime. | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m' | |
| BOLD='\033[1m'; RESET='\033[0m' | |
| info() { echo -e "${CYAN}${BOLD}[INFO]${RESET} $*"; } | |
| success() { echo -e "${GREEN}${BOLD}[OK]${RESET} $*"; } | |
| warn() { echo -e "${YELLOW}${BOLD}[WARN]${RESET} $*"; } | |
| die() { echo -e "${RED}${BOLD}[ERROR]${RESET} $*" >&2; exit 1; } | |
| # ── Sanity checks ───────────────────────────────────────────────────────────── | |
| command -v pm2 &>/dev/null || die "pm2 not found on PATH. Re-run the installer." | |
| command -v python3 &>/dev/null || die "python3 not found on PATH." | |
| # ── Parse config.json helpers ───────────────────────────────────────────────── | |
| _read_bind() { | |
| python3 -c "import json; d=json.load(open('${CONFIG_JSON}')); print(d.get('bind','0.0.0.0:8080'))" | |
| } | |
| _read_paths_args() { | |
| python3 - <<'PYEOF' | |
| import json, shlex | |
| with open("/opt/Upload-Assistant/data/config.json") as f: | |
| d = json.load(f) | |
| print(" ".join(shlex.quote(p) for p in d.get("paths", []))) | |
| PYEOF | |
| } | |
| # ── Commands ────────────────────────────────────────────────────────────────── | |
| cmd_start() { | |
| info "Starting ${PM2_APP_NAME} ..." | |
| pm2 start "${INSTALL_DIR}/upload.py" \ | |
| --name "${PM2_APP_NAME}" \ | |
| --interpreter "${VENV_PYTHON}" \ | |
| -- $(_read_paths_args) --webui "$(_read_bind)" | |
| success "Upload-Assistant started." | |
| } | |
| cmd_stop() { | |
| info "Stopping ${PM2_APP_NAME} ..." | |
| pm2 stop "${PM2_APP_NAME}" | |
| success "Upload-Assistant stopped." | |
| } | |
| cmd_restart() { | |
| info "Restarting ${PM2_APP_NAME} ..." | |
| pm2 restart "${PM2_APP_NAME}" | |
| success "Upload-Assistant restarted." | |
| } | |
| cmd_enable() { | |
| info "Enabling startup on boot ..." | |
| pm2 startup | |
| pm2 save | |
| success "Upload-Assistant will start on boot." | |
| } | |
| cmd_disable() { | |
| info "Disabling startup on boot ..." | |
| pm2 unstartup | |
| success "Boot startup disabled." | |
| } | |
| cmd_edit_config() { | |
| nano "${INSTALL_DIR}/config.py" | |
| info "Config saved. Restarting Upload-Assistant ..." | |
| pm2 restart "${PM2_APP_NAME}" | |
| success "Upload-Assistant restarted with new config." | |
| } | |
| cmd_set_paths() { | |
| echo "" | |
| echo -e "${BOLD}Current paths:${RESET}" | |
| python3 - <<'PYEOF' | |
| import json | |
| with open("/opt/Upload-Assistant/data/config.json") as f: | |
| d = json.load(f) | |
| for i, p in enumerate(d.get("paths", []), 1): | |
| print(f" {i}. {p}") | |
| PYEOF | |
| echo "" | |
| read -rp "$(echo -e "${BOLD}Enter numbers of paths to REMOVE (space-separated, or blank to keep all):${RESET} ")" REMOVE_INPUT | |
| echo "" | |
| echo -e "Enter new paths to ADD (quoted if they contain spaces, blank to skip):" | |
| read -rp "$(echo -e "${BOLD}New paths:${RESET} ")" RAW_NEW | |
| python3 - "${REMOVE_INPUT}" "${RAW_NEW}" <<'PYEOF' | |
| import json, sys, shlex | |
| config_path = "/opt/Upload-Assistant/data/config.json" | |
| with open(config_path) as f: | |
| d = json.load(f) | |
| paths = d.get("paths", []) | |
| remove_input = sys.argv[1].strip() | |
| raw_new = sys.argv[2].strip() | |
| if remove_input: | |
| to_remove = set() | |
| for token in remove_input.split(): | |
| try: | |
| to_remove.add(int(token) - 1) | |
| except ValueError: | |
| pass | |
| paths = [p for i, p in enumerate(paths) if i not in to_remove] | |
| if raw_new: | |
| paths.extend(shlex.split(raw_new)) | |
| d["paths"] = paths | |
| with open(config_path, "w") as f: | |
| json.dump(d, f, indent=2) | |
| print("Updated paths:") | |
| for p in paths: | |
| print(f" • {p}") | |
| PYEOF | |
| info "Restarting Upload-Assistant with new paths ..." | |
| pm2 restart "${PM2_APP_NAME}" | |
| success "Paths updated and service restarted." | |
| } | |
| cmd_set_bind() { | |
| local current new_bind | |
| current="$(_read_bind)" | |
| echo -e "${BOLD}Current bind address:${RESET} ${current}" | |
| read -rp "$(echo -e "${BOLD}New bind address [ip:port]:${RESET} ")" new_bind | |
| [[ -z "$new_bind" ]] && { warn "No input — keeping current bind address."; return; } | |
| python3 - "${new_bind}" <<'PYEOF' | |
| import json, sys | |
| config_path = "/opt/Upload-Assistant/data/config.json" | |
| with open(config_path) as f: | |
| d = json.load(f) | |
| d["bind"] = sys.argv[1] | |
| with open(config_path, "w") as f: | |
| json.dump(d, f, indent=2) | |
| print(f"Bind address updated to: {sys.argv[1]}") | |
| PYEOF | |
| info "Restarting Upload-Assistant with new bind address ..." | |
| pm2 restart "${PM2_APP_NAME}" | |
| success "Bind address updated and service restarted." | |
| } | |
| cmd_up() { | |
| if [[ "${VIRTUAL_ENV:-}" == "${INSTALL_DIR}/venv" ]]; then | |
| python3 "${INSTALL_DIR}/upload.py" "$@" | |
| else | |
| "${VENV_PYTHON}" "${INSTALL_DIR}/upload.py" "$@" | |
| fi | |
| } | |
| cmd_help() { | |
| echo "" | |
| echo -e "${BOLD}upload-assistant${RESET} — Upload-Assistant control utility" | |
| echo "" | |
| echo -e " ${CYAN}start${RESET} Start the service via pm2" | |
| echo -e " ${CYAN}stop${RESET} Stop the service via pm2" | |
| echo -e " ${CYAN}restart${RESET} Restart the service via pm2" | |
| echo -e " ${CYAN}enable${RESET} Enable auto-start on boot (pm2 startup + save)" | |
| echo -e " ${CYAN}disable${RESET} Disable auto-start on boot" | |
| echo -e " ${CYAN}edit-config${RESET} Edit config.py in nano, then auto-restart" | |
| echo -e " ${CYAN}set-paths${RESET} Interactively update media paths, then restart" | |
| echo -e " ${CYAN}set-bind${RESET} Change the WebUI bind address, then restart" | |
| echo -e " ${CYAN}up [args]${RESET} Run upload.py directly (always inside venv)" | |
| echo "" | |
| } | |
| # ── Dispatch ────────────────────────────────────────────────────────────────── | |
| COMMAND="${1:-help}" | |
| shift || true | |
| case "$COMMAND" in | |
| start) cmd_start ;; | |
| stop) cmd_stop ;; | |
| restart) cmd_restart ;; | |
| enable) cmd_enable ;; | |
| disable) cmd_disable ;; | |
| edit-config) cmd_edit_config ;; | |
| set-paths) cmd_set_paths ;; | |
| set-bind) cmd_set_bind ;; | |
| up) cmd_up "$@" ;; | |
| help|--help|-h) cmd_help ;; | |
| *) | |
| die "Unknown command: '${COMMAND}'. Run 'upload-assistant help' for usage." | |
| ;; | |
| esac | |
| UASCRIPT_EOF | |
| chmod +x "$UA_BIN" | |
| success "Control script installed at ${UA_BIN}" | |
| command -v upload-assistant &>/dev/null \ | |
| && success "upload-assistant is available on PATH (${UA_BIN})" \ | |
| || warn "upload-assistant not found on PATH — you may need to open a new shell." | |
| echo "" | |
| echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| echo -e "${BOLD} pm2 Startup${RESET}" | |
| echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| read -rp "$(echo -e "${BOLD}Start Upload-Assistant now and enable boot startup? [Y/n]:${RESET} ")" START_NOW | |
| START_NOW="${START_NOW:-Y}" | |
| if [[ "${START_NOW,,}" == "y" ]]; then | |
| info "Starting Upload-Assistant via pm2 ..." | |
| PATHS_ARGS_ARR=() | |
| for p in "${MEDIA_PATHS[@]}"; do | |
| PATHS_ARGS_ARR+=("$p") | |
| done | |
| pm2 start "${INSTALL_DIR}/upload.py" \ | |
| --name "upload-assistant" \ | |
| --interpreter "${INSTALL_DIR}/venv/bin/python3" \ | |
| -- "${PATHS_ARGS_ARR[@]}" --webui "${BIND}" | |
| pm2 startup | |
| pm2 save | |
| success "Upload-Assistant is running and configured to start on boot." | |
| else | |
| info "Skipping startup. Run 'upload-assistant start' when ready." | |
| info "Run 'upload-assistant enable' to enable boot startup later." | |
| fi | |
| echo "" | |
| echo -e "${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| echo -e "${GREEN}${BOLD} Installation complete!${RESET}" | |
| echo -e "${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" | |
| echo "" | |
| echo -e " ${BOLD}Install directory:${RESET} ${INSTALL_DIR}" | |
| echo -e " ${BOLD}Config file:${RESET} ${INSTALL_DIR}/config.py" | |
| echo -e " ${BOLD}Data config:${RESET} ${CONFIG_JSON}" | |
| echo -e " ${BOLD}Control script:${RESET} ${UA_BIN}" | |
| echo -e " ${BOLD}WebUI bind:${RESET} ${BIND}" | |
| echo "" | |
| echo -e " Run ${CYAN}${BOLD}upload-assistant help${RESET} to see all available commands." | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment