#!/usr/bin/env bash # ABOUTME: One-shot installer for sendspin-player on 64-bit Raspberry Pi OS. # ABOUTME: Fetches the latest arm64 release tarball, installs systemd unit, starts daemon. set -euo pipefail on_exit() { local rc=$? if [[ "${rc}" -ne 0 ]]; then printf '\nIf install failed mid-way, re-running the script is safe — it is idempotent.\n' >&2 fi } trap on_exit EXIT readonly REPO_OWNER="Sendspin" readonly REPO_NAME="sendspin-go" readonly REPO_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}" readonly RAW_URL_BASE="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}" readonly BINARY_NAME="sendspin-player" readonly INSTALL_PATH="/usr/local/bin/${BINARY_NAME}" readonly UNIT_PATH="/etc/systemd/system/${BINARY_NAME}.service" readonly ENV_PATH="/etc/default/${BINARY_NAME}" readonly CONFIG_DIR="/etc/sendspin" readonly CONFIG_PATH="${CONFIG_DIR}/player.yaml" # Set by parse_args ARG_NAME="" ARG_DEVICE="" ARG_VERSION="" ARG_UNINSTALL=0 # Resolved by resolve_version RESOLVED_TAG="" RESOLVED_REF="" usage() { cat <] [--device ] [--version ] [--uninstall] Installs sendspin-player as a systemd daemon on 64-bit Raspberry Pi OS. Options: --name Friendly player name (default: -sendspin-player). --device Exact audio device name. Run 'sendspin-player --list-audio-devices' after install to discover available names. --version Pin to a specific release tag (e.g. v1.6.2). Default: latest. --uninstall Stop the service and remove the binary and unit file. Config is preserved. -h, --help Show this help. Run with sudo: curl -fsSL ${RAW_URL_BASE}/main/scripts/quickstart-pi.sh | sudo bash curl -fsSL ${RAW_URL_BASE}/main/scripts/quickstart-pi.sh | sudo bash -s -- --name "Living Room" EOF } log() { printf '==> %s\n' "$*"; } warn() { printf 'WARN: %s\n' "$*" >&2; } die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --name) [[ $# -ge 2 ]] || die "--name requires a value" ARG_NAME="$2" shift 2 ;; --device) [[ $# -ge 2 ]] || die "--device requires a value" ARG_DEVICE="$2" shift 2 ;; --version) [[ $# -ge 2 ]] || die "--version requires a value (e.g. v1.6.2)" ARG_VERSION="$2" shift 2 ;; --uninstall) ARG_UNINSTALL=1 shift ;; -h|--help) usage exit 0 ;; *) printf 'Unknown argument: %s\n\n' "$1" >&2 usage >&2 exit 2 ;; esac done } preflight() { if [[ "${EUID}" -ne 0 ]]; then die "Root required. Re-run with sudo: curl -fsSL ${RAW_URL_BASE}/main/scripts/quickstart-pi.sh | sudo bash" fi local arch arch="$(uname -m)" if [[ "${arch}" != "aarch64" ]]; then die "Unsupported architecture: ${arch}. This script supports 64-bit Raspberry Pi OS only (aarch64). For Pi 3 / 4 / 5 / Zero 2 W, install the 64-bit Pi OS image: https://www.raspberrypi.com/software/operating-systems/ Pi 1 / Zero (v1) / Zero W are not supported (32-bit ARMv6 only)." fi if [[ ! -f /etc/debian_version ]]; then die "Unsupported OS. This script targets Debian-based distros (Pi OS, Raspberry Pi OS Lite). For other distros see the README install steps: ${REPO_URL}#installation" fi if ! command -v systemctl >/dev/null 2>&1; then die "systemctl not found. The quickstart installs sendspin-player as a systemd service; non-systemd hosts must follow the manual install steps." fi } do_uninstall() { log "Uninstalling sendspin-player..." if systemctl list-unit-files "${BINARY_NAME}.service" >/dev/null 2>&1; then systemctl disable --now "${BINARY_NAME}.service" 2>/dev/null || true fi rm -f "${INSTALL_PATH}" rm -f "${UNIT_PATH}" systemctl daemon-reload log "Uninstall complete." log "Config preserved at ${CONFIG_DIR}/ and ${ENV_PATH}." log "Remove manually for a full purge:" log " sudo rm -rf ${CONFIG_DIR} ${ENV_PATH}" } install_apt_deps() { log "Installing runtime dependencies..." DEBIAN_FRONTEND=noninteractive apt-get update -qq # FLAC decode/encode is pure-Go (mewkiz/flac); the released binary is # built with -tags=nolibopusfile so libopusfile is not linked either. # apt resolves libasound2 to libasound2t64 automatically on time_t-64 # distros (Trixie, Ubuntu 24.04). DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ libopus0 \ libasound2 \ ca-certificates \ curl \ tar } resolve_version() { if [[ -n "${ARG_VERSION}" ]]; then RESOLVED_TAG="${ARG_VERSION}" RESOLVED_REF="${ARG_VERSION}" log "Installing pinned version: ${RESOLVED_TAG}" else # Use GitHub's latest-release redirect: no API call, no JSON parsing. # The redirect target tells us the resolved tag. local redirect_url redirect_url="$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ "${REPO_URL}/releases/latest")" \ || die "Failed to resolve latest release tag from ${REPO_URL}/releases/latest" RESOLVED_TAG="${redirect_url##*/}" # The dist/ files at "main" are forward-compatible enough for the # latest-tagged release; pin them to the same tag for consistency. RESOLVED_REF="${RESOLVED_TAG}" log "Installing latest release: ${RESOLVED_TAG}" fi } stop_service() { if systemctl is-active --quiet "${BINARY_NAME}.service"; then log "Stopping running ${BINARY_NAME} service..." systemctl stop "${BINARY_NAME}.service" fi } install_binary() { local tarball_url tarball_name binary_in_tarball tmpdir binary_in_tarball="${BINARY_NAME}-linux-arm64" tarball_name="${binary_in_tarball}.tar.gz" tarball_url="${REPO_URL}/releases/download/${RESOLVED_TAG}/${tarball_name}" tmpdir="$(mktemp -d)" # shellcheck disable=SC2064 trap "rm -rf '${tmpdir}'; on_exit" EXIT log "Downloading ${tarball_url}..." curl -fSL "${tarball_url}" -o "${tmpdir}/${tarball_name}" \ || die "Failed to download release tarball from ${tarball_url}" log "Extracting..." tar -xzf "${tmpdir}/${tarball_name}" -C "${tmpdir}" if [[ ! -f "${tmpdir}/${binary_in_tarball}" ]]; then die "Tarball did not contain expected binary '${binary_in_tarball}'" fi log "Installing ${INSTALL_PATH}..." install -m 755 "${tmpdir}/${binary_in_tarball}" "${INSTALL_PATH}" } install_unit() { local unit_url unit_url="${RAW_URL_BASE}/${RESOLVED_REF}/dist/systemd/${BINARY_NAME}.service" log "Installing systemd unit ${UNIT_PATH}..." curl -fSL "${unit_url}" -o "${UNIT_PATH}" \ || die "Failed to download unit file from ${unit_url}" chmod 644 "${UNIT_PATH}" } # shell_quote: wrap a string in single quotes, escaping any embedded single # quotes via the standard '\'' pattern. Safe for arbitrary user input. shell_quote() { local s="$1" s="${s//\'/\'\\\'\'}" printf "'%s'" "${s}" } install_env() { if [[ -n "${ARG_NAME}" || -n "${ARG_DEVICE}" ]]; then log "Writing ${ENV_PATH} with --name/--device from flags..." local opts="" if [[ -n "${ARG_NAME}" ]]; then opts+="--name $(shell_quote "${ARG_NAME}") " fi if [[ -n "${ARG_DEVICE}" ]]; then opts+="--audio-device $(shell_quote "${ARG_DEVICE}") " fi # Trim trailing space. opts="${opts% }" cat >"${ENV_PATH}" <