#!/bin/sh # # Терминальный плеер SURPRISE.FM. # # curl -fsSL https://surprise.fm/cli | sh # # Скрипт намеренно короткий и читаемый: его запускают через `curl | sh`, а # единственная защита читателя — возможность сперва посмотреть, что внутри. # # curl -fsSL https://surprise.fm/cli | less # # Исходники плеера: https://github.com/hespermen/surprise-cli set -eu # Всё тело — внутри main(), которая вызывается ПОСЛЕДНЕЙ строкой файла. # # `sh` исполняет команды по мере чтения из конвейера, а не файл целиком. Без # обёртки оборванная загрузка выполняла бы ровно ту часть, что успела прийти, — # и это была бы худшая часть: снятие старого пакета и удаление шима идут В # НАЧАЛЕ, а установка нового в конце. Человек остался бы без команды вовсе, а # `sh` сообщил бы об этом синтаксической ошибкой в stderr, которую при # `curl | sh` никто не читает. # # С обёрткой недокачанный файл не исполняет ничего: определение функции без # закрывающей скобки — синтаксическая ошибка целиком, а вызова просто нет. main() { PKG="surprise-cli" OLD_PKG="surprise-fm" MIN_NODE=20 # ---------------------------------------------------------------- сообщения # Сообщения по-английски: установщик короткий, слова в нём простые, а # исполняют его в том числе там, где кириллица в терминале выглядит мусором — # по ssh со старой локалью, в чужом CI. Сам плеер спросит язык при первом # запуске, так что русский человек увидит русский интерфейс через несколько # секунд после этих трёх строк. say() { printf '%s\n' "$1"; } die() { printf '%s\n' "$1" >&2 exit 1 } # ------------------------------------------------------------- что уже есть command -v node >/dev/null 2>&1 || die "Node.js $MIN_NODE or newer is required — https://nodejs.org" command -v npm >/dev/null 2>&1 || die "Node is here but npm is not. They normally ship together — reinstall Node.js." node_major=$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || true) case "$node_major" in '' | *[!0-9]*) die "Could not determine the Node.js version. Is 'node' a wrapper that prints to stdout?" ;; esac if [ "$node_major" -lt "$MIN_NODE" ]; then die "Node.js $node_major is too old; $MIN_NODE or newer is required." fi # ------------------------------------------------- куда вообще ставить # Не ждём загрузки, чтобы только потом получить сырой EACCES. Проверяем # ближайшего существующего предка: у нового пользовательского префикса сам # каталог node_modules может ещё не существовать, хотя создать его можно. npm_root=$(npm root -g 2>/dev/null || true) [ -n "$npm_root" ] || die "Could not determine npm's global directory." probe="$npm_root" while [ ! -e "$probe" ] && [ "$probe" != "/" ]; do probe=$(dirname "$probe") done if [ ! -w "$probe" ] && [ "$(id -u)" != 0 ]; then say "The global npm directory is not writable: $npm_root" say "" say "Point npm at your home directory instead of using sudo:" say " npm config set prefix ~/.local" say " export PATH=\"\$HOME/.local/bin:\$PATH\"" say "" die "Then run the installer again." fi # ---------------------------------------------------------- версия пакета # npm 12 запрещает git-источники по умолчанию (EALLOWGIT). Пакет берём из # реестра, а latest сначала превращаем в точную неизменяемую версию. Затем # эту же версию сверяем с ответом CLI — «npm завершился с кодом 0» ещё не # означает, что PATH действительно ведёт на только что установленный пакет. tmp=$(mktemp -d) || die "Could not create a temporary directory." trap 'rm -rf "$tmp"' EXIT trap 'rm -rf "$tmp"; exit 130' INT TERM if ! expected=$(npm view "$PKG@latest" version 2>"$tmp/npm-view.log"); then cat "$tmp/npm-view.log" >&2 die "Could not find $PKG in the npm registry." fi case "$expected" in '' | *[!0-9A-Za-z.+-]*) die "npm returned an invalid $PKG version: $expected" ;; esac # ------------------------------------------- старый пакет под тем же именем # До переименования плеер назывался surprise-fm и ставил ТУ ЖЕ команду. # Два пакета не могут владеть одной командой: npm упрётся в EEXIST, а если # и поставит, команда всё равно уведёт на старую копию. Разбираемся заранее — # иначе человек увидит ошибку, которая ничего ему не объясняет. if npm ls -g --depth=0 "$OLD_PKG" >/dev/null 2>&1; then say "Removing the old $OLD_PKG package…" npm rm -g "$OLD_PKG" >/dev/null 2>&1 || true fi # Шим мог остаться сиротой: npm rm снимает его не всегда, если пакет # переименовали. Удаляем ТОЛЬКО если он и правда ведёт в старый пакет — # под именем surprise у человека может стоять что угодно своё. existing=$(command -v surprise 2>/dev/null || true) if [ -n "$existing" ]; then # Удаляем ТОЛЬКО осиротевший шим npm, и проверяем это тремя условиями сразу. # # Прежняя версия искала подстроку «surprise-fm» где угодно в цели симлинка — # и совпадала на ~/projects/surprise-fm/tools/surprise, то есть сносила # разработчику его собственную команду, сделанную через npm link. Под именем # surprise у человека может стоять что угодно своё, и «похоже на наше» — # недостаточное основание, чтобы это стереть. prefix_bin="$(npm prefix -g 2>/dev/null)/bin" target=$(readlink "$existing" 2>/dev/null || true) case "$existing:$target" in # 1) лежит в bin глобального префикса npm, 2) это симлинк, # 3) ведёт именно в каталог пакета node_modules/surprise-fm/ "$prefix_bin"/*:*node_modules/"$OLD_PKG"/*) say "Removing the orphaned command: $existing" rm -f "$existing" ;; *) if [ -n "$target" ] || [ -e "$existing" ]; then say "Note: $existing is not ours — leaving it alone." fi ;; esac fi # ------------------------------------------------------------------ ставим say "Installing…" if ! npm i -g "$PKG@$expected" >"$tmp/npm-install.log" 2>&1; then say "npm failed to install. Details:" cat "$tmp/npm-install.log" >&2 exit 1 fi # -------------------------------------------------------------- проверяем # shell кэширует найденные пути; после снятия старого шима сбрасываем кэш, # иначе проверка способна запустить уже удалённый файл. hash -r 2>/dev/null || true installed=$(surprise --version 2>/dev/null || true) case "$installed" in "$PKG $expected") say "Done: $installed" bin=$(command -v surprise) ;; "$PKG "*) say "Expected $PKG $expected, but the surprise command reports: $installed" die "Check which command is being used: command -v surprise" ;; "") say "Installed, but the surprise command is not on PATH." say "Add to PATH: $(npm prefix -g)/bin" exit 1 ;; *) # Ответ без имени пакета — это старая установка, которая всё ещё владеет # командой. Сама по себе она не уйдёт, и молчать об этом нельзя. say "The surprise command points at another install: $installed" say "Remove it and try again: rm \$(command -v surprise)" exit 1 ;; esac # ---------------------------------------------------------------- про звук # Предупреждаем, но не ставим: mpv тянет за собой заметное дерево, и решать # за человека, что положить ему в систему, установщик не должен. has_player=0 if command -v mpv >/dev/null 2>&1; then has_player=1 elif command -v ffplay >/dev/null 2>&1; then has_player=1 say "" say "mpv not found — ffplay will play instead: no smooth seeking or volume." fi if [ "$has_player" = 0 ]; then say "" say "You need mpv — it plays the audio:" say " brew install mpv · apt install mpv · winget install mpv" say "" say "Then run: surprise" # Запускать сейчас нечего: плеер упрётся в отсутствие звука и закроется. # Показать человеку мгновенную ошибку вместо интерфейса — худший способ # начать знакомство, чем одна honest строчка с командой. exit 0 fi # ------------------------------------------------------------------ запуск # Запускаем сам, но только если есть куда: в CI и в пайпе полноэкранный # интерфейс повис бы, не дождавшись клавиш. # # Терминал подключается ЯВНО, через /dev/tty. При `curl … | sh` стандартный # ввод занят текстом самого скрипта — плеер прочитал бы из него остаток # установщика вместо нажатий и закрылся на первом же символе. Это та самая # причина, по которой установщики обычно просят запускать команду руками; # одной перенаправленной строкой она снимается. if [ -t 1 ] && [ -r /dev/tty ]; then say "" say "Starting…" # Временный каталог убираем ДО exec: exec заменяет процесс, и ловушка на # выходе уже не сработает — архив остался бы лежать навсегда. rm -rf "$tmp" trap - EXIT INT TERM exec "$bin" < /dev/tty fi say "" say "Run it: surprise" } main "$@"