|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# ============================================================================ |
| 5 | +# music-cli Installer |
| 6 | +# A command-line music player for coders: radio, local files, AI generation |
| 7 | +# |
| 8 | +# Quick install: |
| 9 | +# curl -sSL https://raw.githubusercontent.com/luongnv89/music-cli/main/install.sh | bash |
| 10 | +# |
| 11 | +# With optional extras: |
| 12 | +# EXTRAS=youtube curl -sSL https://raw.githubusercontent.com/luongnv89/music-cli/main/install.sh | bash |
| 13 | +# EXTRAS=ai curl -sSL https://raw.githubusercontent.com/luongnv89/music-cli/main/install.sh | bash |
| 14 | +# EXTRAS="youtube,ai" curl -sSL https://raw.githubusercontent.com/luongnv89/music-cli/main/install.sh | bash |
| 15 | +# |
| 16 | +# Environment variables: |
| 17 | +# EXTRAS Comma-separated optional extras: youtube, ai (default: none) |
| 18 | +# INSTALL_DIR Virtual-env install location (default: ~/.local/share/music-cli) |
| 19 | +# PYTHON Path to python interpreter to use (default: auto-detected) |
| 20 | +# SKIP_FFMPEG Set to 1 to skip FFmpeg installation check |
| 21 | +# ============================================================================ |
| 22 | + |
| 23 | +TOOL_NAME="music-cli" |
| 24 | +PYPI_PACKAGE="coder-music-cli" |
| 25 | +REPO="luongnv89/music-cli" |
| 26 | +BRANCH="main" |
| 27 | + |
| 28 | +EXTRAS="${EXTRAS:-}" |
| 29 | +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/share/music-cli}" |
| 30 | +SKIP_FFMPEG="${SKIP_FFMPEG:-0}" |
| 31 | + |
| 32 | +# --- Color Output --- |
| 33 | +RED='\033[0;31m' |
| 34 | +GREEN='\033[0;32m' |
| 35 | +YELLOW='\033[1;33m' |
| 36 | +BLUE='\033[0;34m' |
| 37 | +CYAN='\033[0;36m' |
| 38 | +BOLD='\033[1m' |
| 39 | +NC='\033[0m' |
| 40 | + |
| 41 | +info() { printf "${BLUE}[INFO]${NC} %s\n" "$*"; } |
| 42 | +ok() { printf "${GREEN}[ OK ]${NC} %s\n" "$*"; } |
| 43 | +warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; } |
| 44 | +step() { printf "\n${BOLD}${CYAN}==> %s${NC}\n" "$*"; } |
| 45 | +err() { printf "${RED}[ERR ]${NC} %s\n" "$*" >&2; } |
| 46 | +die() { err "$@"; exit 1; } |
| 47 | + |
| 48 | +# --- OS / Arch Detection --- |
| 49 | +detect_os() { |
| 50 | + local os |
| 51 | + os="$(uname -s | tr '[:upper:]' '[:lower:]')" |
| 52 | + case "$os" in |
| 53 | + linux*) echo "linux" ;; |
| 54 | + darwin*) echo "macos" ;; |
| 55 | + mingw*|msys*|cygwin*) echo "windows" ;; |
| 56 | + *) die "Unsupported operating system: $os" ;; |
| 57 | + esac |
| 58 | +} |
| 59 | + |
| 60 | +detect_package_manager() { |
| 61 | + if command -v brew &>/dev/null; then echo "brew" |
| 62 | + elif command -v apt-get &>/dev/null; then echo "apt" |
| 63 | + elif command -v dnf &>/dev/null; then echo "dnf" |
| 64 | + elif command -v yum &>/dev/null; then echo "yum" |
| 65 | + elif command -v pacman &>/dev/null; then echo "pacman" |
| 66 | + elif command -v zypper &>/dev/null; then echo "zypper" |
| 67 | + else echo "unknown" |
| 68 | + fi |
| 69 | +} |
| 70 | + |
| 71 | +need_sudo() { |
| 72 | + if [ "$(id -u)" -ne 0 ]; then |
| 73 | + if command -v sudo &>/dev/null; then echo "sudo" |
| 74 | + else echo "" # caller will warn if sudo is needed |
| 75 | + fi |
| 76 | + else |
| 77 | + echo "" |
| 78 | + fi |
| 79 | +} |
| 80 | + |
| 81 | +# --- Dependency Checks --- |
| 82 | +check_command() { |
| 83 | + command -v "$1" &>/dev/null |
| 84 | +} |
| 85 | + |
| 86 | +require_python() { |
| 87 | + # Honour explicit PYTHON override |
| 88 | + if [ -n "${PYTHON:-}" ]; then |
| 89 | + if ! "$PYTHON" --version &>/dev/null; then |
| 90 | + die "PYTHON='$PYTHON' is not executable" |
| 91 | + fi |
| 92 | + echo "$PYTHON" |
| 93 | + return |
| 94 | + fi |
| 95 | + |
| 96 | + for candidate in python3 python; do |
| 97 | + if check_command "$candidate"; then |
| 98 | + local ver |
| 99 | + ver="$("$candidate" -c 'import sys; print("%d.%d" % sys.version_info[:2])')" |
| 100 | + local major minor |
| 101 | + major="${ver%%.*}" |
| 102 | + minor="${ver##*.}" |
| 103 | + if [ "$major" -ge 3 ] && [ "$minor" -ge 10 ]; then |
| 104 | + echo "$candidate" |
| 105 | + return |
| 106 | + fi |
| 107 | + fi |
| 108 | + done |
| 109 | + |
| 110 | + die "Python 3.10+ is required but was not found. |
| 111 | + Install it from https://www.python.org/downloads/ or via your package manager, then re-run this script." |
| 112 | +} |
| 113 | + |
| 114 | +install_ffmpeg() { |
| 115 | + local os="$1" |
| 116 | + local pm="$2" |
| 117 | + local sudo_cmd="$3" |
| 118 | + |
| 119 | + if check_command ffmpeg; then |
| 120 | + ok "FFmpeg already installed ($(ffmpeg -version 2>&1 | head -1 | awk '{print $3}'))" |
| 121 | + return |
| 122 | + fi |
| 123 | + |
| 124 | + info "Installing FFmpeg..." |
| 125 | + case "$os" in |
| 126 | + macos) |
| 127 | + if [ "$pm" = "brew" ]; then |
| 128 | + brew install ffmpeg |
| 129 | + else |
| 130 | + die "Homebrew is required to install FFmpeg on macOS. |
| 131 | + Install Homebrew: https://brew.sh |
| 132 | + Then re-run this script." |
| 133 | + fi |
| 134 | + ;; |
| 135 | + linux) |
| 136 | + case "$pm" in |
| 137 | + apt) $sudo_cmd apt-get update -qq && $sudo_cmd apt-get install -y -qq ffmpeg ;; |
| 138 | + dnf) $sudo_cmd dnf install -y -q ffmpeg ;; |
| 139 | + yum) $sudo_cmd yum install -y -q ffmpeg ;; |
| 140 | + pacman) $sudo_cmd pacman -Sy --noconfirm ffmpeg ;; |
| 141 | + zypper) $sudo_cmd zypper install -y ffmpeg ;; |
| 142 | + *) |
| 143 | + warn "Cannot auto-install FFmpeg (unknown package manager)." |
| 144 | + warn "Please install FFmpeg manually: https://ffmpeg.org/download.html" |
| 145 | + warn "Then re-run this script." |
| 146 | + return 1 |
| 147 | + ;; |
| 148 | + esac |
| 149 | + ;; |
| 150 | + windows) |
| 151 | + warn "On Windows, please install FFmpeg manually." |
| 152 | + warn "Options:" |
| 153 | + warn " winget install ffmpeg" |
| 154 | + warn " choco install ffmpeg" |
| 155 | + warn " Or download from: https://ffmpeg.org/download.html" |
| 156 | + warn "Then re-run this script." |
| 157 | + return 1 |
| 158 | + ;; |
| 159 | + esac |
| 160 | + ok "FFmpeg installed" |
| 161 | +} |
| 162 | + |
| 163 | +install_pip_if_needed() { |
| 164 | + local python="$1" |
| 165 | + if ! "$python" -m pip --version &>/dev/null; then |
| 166 | + info "pip not found — installing via ensurepip..." |
| 167 | + "$python" -m ensurepip --upgrade || die "Failed to install pip. Install it manually: https://pip.pypa.io/en/stable/installation/" |
| 168 | + fi |
| 169 | +} |
| 170 | + |
| 171 | +# --- Main music-cli Install --- |
| 172 | +install_music_cli() { |
| 173 | + local python="$1" |
| 174 | + |
| 175 | + step "Creating isolated environment at $INSTALL_DIR" |
| 176 | + "$python" -m venv "$INSTALL_DIR" |
| 177 | + local venv_python="$INSTALL_DIR/bin/python" |
| 178 | + local venv_pip="$INSTALL_DIR/bin/pip" |
| 179 | + |
| 180 | + step "Upgrading pip inside venv" |
| 181 | + "$venv_pip" install --quiet --upgrade pip |
| 182 | + |
| 183 | + # Build extras specifier |
| 184 | + local pkg="$PYPI_PACKAGE" |
| 185 | + if [ -n "$EXTRAS" ]; then |
| 186 | + pkg="${PYPI_PACKAGE}[$EXTRAS]" |
| 187 | + fi |
| 188 | + |
| 189 | + step "Installing $pkg from PyPI" |
| 190 | + "$venv_pip" install --quiet "$pkg" |
| 191 | + ok "Installed $pkg" |
| 192 | +} |
| 193 | + |
| 194 | +# --- Symlink / PATH setup --- |
| 195 | +link_binary() { |
| 196 | + local venv_bin="$INSTALL_DIR/bin/music-cli" |
| 197 | + local link_target="$HOME/.local/bin/music-cli" |
| 198 | + |
| 199 | + mkdir -p "$HOME/.local/bin" |
| 200 | + |
| 201 | + if [ -L "$link_target" ] || [ -f "$link_target" ]; then |
| 202 | + rm -f "$link_target" |
| 203 | + fi |
| 204 | + ln -s "$venv_bin" "$link_target" |
| 205 | + ok "Linked $link_target -> $venv_bin" |
| 206 | + |
| 207 | + # Shell PATH advice |
| 208 | + if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then |
| 209 | + warn "'$HOME/.local/bin' is not in your PATH." |
| 210 | + warn "Add the following line to your shell profile (~/.bashrc / ~/.zshrc):" |
| 211 | + warn "" |
| 212 | + warn " export PATH=\"\$HOME/.local/bin:\$PATH\"" |
| 213 | + warn "" |
| 214 | + warn "Then reload: source ~/.bashrc (or open a new terminal)" |
| 215 | + fi |
| 216 | +} |
| 217 | + |
| 218 | +# --- Verification --- |
| 219 | +verify_installation() { |
| 220 | + local venv_bin="$INSTALL_DIR/bin/music-cli" |
| 221 | + step "Verifying installation" |
| 222 | + if [ -x "$venv_bin" ]; then |
| 223 | + local ver |
| 224 | + ver="$("$venv_bin" --version 2>/dev/null || true)" |
| 225 | + ok "$TOOL_NAME $ver is ready" |
| 226 | + else |
| 227 | + die "Installation verification failed — $venv_bin not found or not executable" |
| 228 | + fi |
| 229 | +} |
| 230 | + |
| 231 | +# --- Entry Point --- |
| 232 | +main() { |
| 233 | + printf "\n${BOLD}${GREEN}music-cli installer${NC}\n" |
| 234 | + printf "${CYAN} Code. Listen. Iterate.${NC}\n\n" |
| 235 | + printf " Repo : https://github.com/$REPO\n" |
| 236 | + printf " PyPI : https://pypi.org/project/$PYPI_PACKAGE\n" |
| 237 | + printf " Extras : ${EXTRAS:-none}\n" |
| 238 | + printf " Dest : $INSTALL_DIR\n\n" |
| 239 | + |
| 240 | + local os pm sudo_cmd python |
| 241 | + os="$(detect_os)" |
| 242 | + pm="$(detect_package_manager)" |
| 243 | + sudo_cmd="$(need_sudo)" |
| 244 | + |
| 245 | + info "Platform: $os | Package manager: $pm" |
| 246 | + |
| 247 | + # ── Python ────────────────────────────────────────────── |
| 248 | + step "Checking Python 3.10+" |
| 249 | + python="$(require_python)" |
| 250 | + ok "Using Python: $python ($("$python" --version))" |
| 251 | + |
| 252 | + # ── pip ───────────────────────────────────────────────── |
| 253 | + step "Checking pip" |
| 254 | + install_pip_if_needed "$python" |
| 255 | + ok "pip available" |
| 256 | + |
| 257 | + # ── FFmpeg ────────────────────────────────────────────── |
| 258 | + if [ "$SKIP_FFMPEG" = "0" ]; then |
| 259 | + step "Checking FFmpeg" |
| 260 | + if ! install_ffmpeg "$os" "$pm" "$sudo_cmd"; then |
| 261 | + warn "FFmpeg installation skipped. music-cli requires FFmpeg for audio playback." |
| 262 | + warn "Install it manually and ensure it is in your PATH before using music-cli." |
| 263 | + fi |
| 264 | + else |
| 265 | + warn "Skipping FFmpeg check (SKIP_FFMPEG=1)" |
| 266 | + fi |
| 267 | + |
| 268 | + # ── music-cli ──────────────────────────────────────────── |
| 269 | + install_music_cli "$python" |
| 270 | + |
| 271 | + # ── Symlink ────────────────────────────────────────────── |
| 272 | + step "Setting up PATH" |
| 273 | + link_binary |
| 274 | + |
| 275 | + # ── Done ───────────────────────────────────────────────── |
| 276 | + verify_installation |
| 277 | + |
| 278 | + printf "\n${BOLD}${GREEN}============================================${NC}\n" |
| 279 | + ok "Installation complete!" |
| 280 | + printf "${BOLD}${GREEN}============================================${NC}\n\n" |
| 281 | + printf " Get started:\n\n" |
| 282 | + printf " ${CYAN}music-cli play${NC} # context-aware radio\n" |
| 283 | + printf " ${CYAN}music-cli play --mood focus${NC} # focus music\n" |
| 284 | + printf " ${CYAN}music-cli status${NC} # what's playing\n" |
| 285 | + printf " ${CYAN}music-cli --help${NC} # all commands\n\n" |
| 286 | + |
| 287 | + if [ -n "$EXTRAS" ]; then |
| 288 | + printf " Extras installed: ${YELLOW}$EXTRAS${NC}\n" |
| 289 | + if echo "$EXTRAS" | grep -q "ai"; then |
| 290 | + printf " AI models are downloaded on first use (~1.5–6 GB each).\n" |
| 291 | + printf " Try: ${CYAN}music-cli ai play --mood focus${NC}\n" |
| 292 | + fi |
| 293 | + if echo "$EXTRAS" | grep -q "youtube"; then |
| 294 | + printf " YouTube streaming is ready.\n" |
| 295 | + printf " Try: ${CYAN}music-cli play -m youtube -s \"<URL>\"${NC}\n" |
| 296 | + fi |
| 297 | + fi |
| 298 | + printf "\n" |
| 299 | +} |
| 300 | + |
| 301 | +main "$@" |
0 commit comments