feat: initial commit — unarr CLI
Search, inspect, stream, and download torrents from the terminal. Replaces the entire *arr stack with a single binary.
This commit is contained in:
commit
29cf0a0126
85 changed files with 10178 additions and 0 deletions
333
install.sh
Executable file
333
install.sh
Executable file
|
|
@ -0,0 +1,333 @@
|
|||
#!/bin/sh
|
||||
# unarr — cross-platform installer (Linux / macOS)
|
||||
# Usage: curl -fsSL https://get.unarr.com/install.sh | sh
|
||||
# or: curl -fsSL https://raw.githubusercontent.com/torrentclaw/torrentclaw-cli/main/install.sh | sh
|
||||
#
|
||||
# Options (env vars):
|
||||
# INSTALL_DIR=/usr/local/bin — where to place the binary (default: /usr/local/bin or ~/.local/bin)
|
||||
# VERSION=0.5.0 — specific version (default: latest)
|
||||
# METHOD=binary|docker — force install method (default: auto-detect)
|
||||
set -e
|
||||
|
||||
REPO="torrentclaw/torrentclaw-cli"
|
||||
BINARY="unarr"
|
||||
|
||||
# ---- Colors (only if terminal) ----
|
||||
if [ -t 1 ]; then
|
||||
BOLD="\033[1m"
|
||||
GREEN="\033[32m"
|
||||
YELLOW="\033[33m"
|
||||
RED="\033[31m"
|
||||
CYAN="\033[36m"
|
||||
RESET="\033[0m"
|
||||
else
|
||||
BOLD="" GREEN="" YELLOW="" RED="" CYAN="" RESET=""
|
||||
fi
|
||||
|
||||
info() { printf "${CYAN}→${RESET} %s\n" "$1"; }
|
||||
ok() { printf "${GREEN}✓${RESET} %s\n" "$1"; }
|
||||
warn() { printf "${YELLOW}!${RESET} %s\n" "$1"; }
|
||||
error() { printf "${RED}✗${RESET} %s\n" "$1" >&2; exit 1; }
|
||||
|
||||
# ---- Detect OS ----
|
||||
detect_os() {
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
case "$OS" in
|
||||
linux*) OS="linux" ;;
|
||||
darwin*) OS="darwin" ;;
|
||||
mingw*|msys*|cygwin*) OS="windows" ;;
|
||||
*) error "Unsupported OS: $OS. Use install.ps1 for Windows." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---- Detect architecture ----
|
||||
detect_arch() {
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ARCH="amd64" ;;
|
||||
aarch64|arm64) ARCH="arm64" ;;
|
||||
*) error "Unsupported architecture: $ARCH" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---- Detect if we can write to /usr/local/bin ----
|
||||
detect_install_dir() {
|
||||
if [ -n "$INSTALL_DIR" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -w "/usr/local/bin" ]; then
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
elif [ -d "$HOME/.local/bin" ]; then
|
||||
INSTALL_DIR="$HOME/.local/bin"
|
||||
else
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
INSTALL_DIR="$HOME/.local/bin"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- Check if command exists ----
|
||||
has() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# ---- HTTP download (curl or wget) ----
|
||||
download() {
|
||||
url="$1"
|
||||
output="$2"
|
||||
if has curl; then
|
||||
curl -fsSL -o "$output" "$url"
|
||||
elif has wget; then
|
||||
wget -qO "$output" "$url"
|
||||
else
|
||||
error "Neither curl nor wget found. Install one and retry."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- Fetch text (for API) ----
|
||||
fetch() {
|
||||
url="$1"
|
||||
if has curl; then
|
||||
curl -fsSL "$url"
|
||||
elif has wget; then
|
||||
wget -qO- "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- Get latest version from GitHub ----
|
||||
get_latest_version() {
|
||||
if [ -n "$VERSION" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
info "Checking latest version..."
|
||||
|
||||
# Try GitHub API first
|
||||
response=$(fetch "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$response" ]; then
|
||||
# Parse tag_name from JSON (works without jq)
|
||||
VERSION=$(echo "$response" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/')
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
# Fallback: follow redirect from /releases/latest
|
||||
if has curl; then
|
||||
VERSION=$(curl -fsSI "https://github.com/$REPO/releases/latest" 2>/dev/null | grep -i '^location:' | sed 's|.*/v\{0,1\}||' | tr -d '\r\n')
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
error "Could not determine latest version. Set VERSION=x.y.z and retry."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- Install via binary ----
|
||||
install_binary() {
|
||||
get_latest_version
|
||||
|
||||
# Strip leading 'v' if present
|
||||
VERSION="${VERSION#v}"
|
||||
|
||||
archive="${BINARY}_${VERSION}_${OS}_${ARCH}.tar.gz"
|
||||
url="https://github.com/$REPO/releases/download/v${VERSION}/${archive}"
|
||||
|
||||
info "Downloading $BINARY v$VERSION for $OS/$ARCH..."
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
|
||||
download "$url" "$tmpdir/$archive"
|
||||
|
||||
info "Extracting..."
|
||||
tar -xzf "$tmpdir/$archive" -C "$tmpdir"
|
||||
|
||||
# Find binary (may be at root or in a subdir)
|
||||
bin_path=$(find "$tmpdir" -name "$BINARY" -type f | head -1)
|
||||
if [ -z "$bin_path" ]; then
|
||||
rm -rf "$tmpdir"
|
||||
error "Binary not found in archive"
|
||||
fi
|
||||
|
||||
chmod +x "$bin_path"
|
||||
|
||||
# Install
|
||||
if [ -w "$INSTALL_DIR" ]; then
|
||||
mv "$bin_path" "$INSTALL_DIR/$BINARY"
|
||||
else
|
||||
info "Requires sudo to install to $INSTALL_DIR"
|
||||
sudo mv "$bin_path" "$INSTALL_DIR/$BINARY"
|
||||
fi
|
||||
|
||||
rm -rf "$tmpdir"
|
||||
|
||||
ok "Installed $BINARY v$VERSION to $INSTALL_DIR/$BINARY"
|
||||
|
||||
# Check PATH
|
||||
case ":$PATH:" in
|
||||
*":$INSTALL_DIR:"*) ;;
|
||||
*)
|
||||
warn "$INSTALL_DIR is not in your PATH."
|
||||
printf " Add it with: ${BOLD}export PATH=\"%s:\$PATH\"${RESET}\n" "$INSTALL_DIR"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---- Install via Docker ----
|
||||
install_docker() {
|
||||
if ! has docker; then
|
||||
error "Docker not found. Install Docker first: https://docs.docker.com/get-docker/"
|
||||
fi
|
||||
|
||||
info "Pulling torrentclaw/unarr:latest..."
|
||||
docker pull torrentclaw/unarr:latest 2>/dev/null || {
|
||||
info "Image not on Docker Hub yet, building from source..."
|
||||
tmpdir=$(mktemp -d)
|
||||
|
||||
if has git; then
|
||||
git clone --depth 1 "https://github.com/$REPO.git" "$tmpdir/unarr"
|
||||
docker build -t torrentclaw/unarr:latest "$tmpdir/unarr"
|
||||
rm -rf "$tmpdir"
|
||||
else
|
||||
rm -rf "$tmpdir"
|
||||
error "git not found. Install git or pull the image manually."
|
||||
fi
|
||||
}
|
||||
|
||||
ok "Docker image ready: torrentclaw/unarr:latest"
|
||||
|
||||
printf "\n${BOLD}Quick start:${RESET}\n"
|
||||
cat <<'DOCKER_USAGE'
|
||||
|
||||
# 1. Create config directory
|
||||
mkdir -p ~/.config/unarr
|
||||
|
||||
# 2. Run setup (interactive)
|
||||
docker run -it --rm \
|
||||
-v ~/.config/unarr:/config \
|
||||
torrentclaw/unarr setup
|
||||
|
||||
# 3. Start daemon
|
||||
docker run -d --name unarr \
|
||||
--restart unless-stopped \
|
||||
--network host \
|
||||
--read-only \
|
||||
--memory 512m \
|
||||
-v ~/.config/unarr:/config \
|
||||
-v ~/Media:/downloads \
|
||||
torrentclaw/unarr
|
||||
|
||||
# Or use the provided docker-compose.yml:
|
||||
# curl -fsSL https://raw.githubusercontent.com/torrentclaw/torrentclaw-cli/main/docker-compose.yml > docker-compose.yml
|
||||
# docker compose up -d
|
||||
|
||||
DOCKER_USAGE
|
||||
}
|
||||
|
||||
# ---- Uninstall ----
|
||||
uninstall() {
|
||||
info "Uninstalling $BINARY..."
|
||||
|
||||
# Remove binary
|
||||
for dir in /usr/local/bin "$HOME/.local/bin" /usr/bin; do
|
||||
if [ -f "$dir/$BINARY" ]; then
|
||||
if [ -w "$dir" ]; then
|
||||
rm -f "$dir/$BINARY"
|
||||
else
|
||||
sudo rm -f "$dir/$BINARY"
|
||||
fi
|
||||
ok "Removed $dir/$BINARY"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove Docker
|
||||
if has docker; then
|
||||
docker rm -f unarr 2>/dev/null && ok "Removed Docker container 'unarr'"
|
||||
docker rmi torrentclaw/unarr:latest 2>/dev/null && ok "Removed Docker image"
|
||||
fi
|
||||
|
||||
ok "Uninstalled. Config remains at ~/.config/unarr/ (delete manually if desired)."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---- Interactive menu ----
|
||||
interactive_menu() {
|
||||
printf "\n"
|
||||
printf " ${BOLD}unarr Installer${RESET}\n"
|
||||
printf " ────────────────────────\n"
|
||||
printf "\n"
|
||||
printf " Detected: ${CYAN}$OS/$ARCH${RESET}\n"
|
||||
printf "\n"
|
||||
printf " Install method:\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}1)${RESET} Binary — standalone executable, no dependencies\n"
|
||||
printf " ${BOLD}2)${RESET} Docker — sandboxed, isolated filesystem access ${GREEN}(recommended)${RESET}\n"
|
||||
|
||||
if [ "$OS" = "darwin" ] || has brew; then
|
||||
printf " ${BOLD}3)${RESET} Homebrew — brew install torrentclaw/tap/unarr\n"
|
||||
fi
|
||||
|
||||
printf " ${BOLD}u)${RESET} Uninstall\n"
|
||||
printf "\n"
|
||||
printf " Choice [1/2"
|
||||
if [ "$OS" = "darwin" ] || has brew; then printf "/3"; fi
|
||||
printf "]: "
|
||||
|
||||
read -r choice
|
||||
|
||||
case "$choice" in
|
||||
1) METHOD="binary" ;;
|
||||
2) METHOD="docker" ;;
|
||||
3)
|
||||
if [ "$OS" = "darwin" ] || has brew; then
|
||||
METHOD="brew"
|
||||
else
|
||||
error "Invalid choice"
|
||||
fi
|
||||
;;
|
||||
u|U) uninstall ;;
|
||||
*) error "Invalid choice: $choice" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---- Main ----
|
||||
main() {
|
||||
detect_os
|
||||
detect_arch
|
||||
detect_install_dir
|
||||
|
||||
# Non-interactive if METHOD is set
|
||||
if [ -z "$METHOD" ]; then
|
||||
# If piped (non-interactive), default to binary
|
||||
if [ ! -t 0 ]; then
|
||||
METHOD="binary"
|
||||
else
|
||||
interactive_menu
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
|
||||
case "$METHOD" in
|
||||
binary)
|
||||
install_binary
|
||||
printf "\n Run ${BOLD}unarr setup${RESET} to get started.\n\n"
|
||||
;;
|
||||
docker)
|
||||
install_docker
|
||||
;;
|
||||
brew)
|
||||
if ! has brew; then
|
||||
error "Homebrew not found. Install it first: https://brew.sh"
|
||||
fi
|
||||
info "Installing via Homebrew..."
|
||||
brew install torrentclaw/tap/unarr
|
||||
ok "Installed via Homebrew"
|
||||
printf "\n Run ${BOLD}unarr setup${RESET} to get started.\n\n"
|
||||
;;
|
||||
*)
|
||||
error "Unknown method: $METHOD"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
Add table
Add a link
Reference in a new issue