mirror of
https://github.com/mistergibson/radio.git
synced 2026-09-08 22:09:51 -07:00
313 lines
11 KiB
Shell
Executable file
313 lines
11 KiB
Shell
Executable file
#!/usr/bin/env bash
|
|
# install_for_python - Radio automation installer for Linux Mint 22.3
|
|
#
|
|
# Installs the liquidsoap-based radio automation stack:
|
|
# - Creates the 'liquidsoap' system user
|
|
# - Prompts for storage path, Icecast credentials, gpodder.net credentials
|
|
# - Generates config.json from the answers
|
|
# - Sets up directory structure under <storage>/
|
|
# - Ensures SQLite databases exist with correct schema
|
|
# - Installs JRuby gems (sequel, jdbc-sqlite3, nokogiri, json) one at a time
|
|
# - Writes the self-locating run_radio.sh launcher
|
|
# - Installs crontab entries (as liquidsoap) for periodic fetch + update
|
|
# - Optionally enables the systemd service
|
|
#
|
|
# Usage: sudo ./install_for_python
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Derive service name from containing directory name
|
|
# ---------------------------------------------------------------------------
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SERVICE_NAME="$(basename "$SCRIPT_DIR")"
|
|
SYSTEMD_UNIT="/etc/systemd/system/${SERVICE_NAME}.service"
|
|
|
|
echo "=== ${SERVICE_NAME} Installer ==="
|
|
echo
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Root check
|
|
# ---------------------------------------------------------------------------
|
|
if [[ $EUID -ne 0 ]]; then
|
|
echo "ERROR: Please run as root (sudo)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Create liquidsoap system user if missing
|
|
# ---------------------------------------------------------------------------
|
|
if ! id -u liquidsoap >/dev/null 2>&1; then
|
|
echo "Creating system user 'liquidsoap'..."
|
|
useradd --system --create-home --shell /usr/sbin/nologin liquidsoap
|
|
echo "Done."
|
|
else
|
|
echo "User 'liquidsoap' already exists."
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Prompt for configuration values
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
read -rp "Station data directory [/mnt/storage/radio]: " STORAGE
|
|
STORAGE="${STORAGE:-/mnt/storage/radio}"
|
|
|
|
read -rp "Icecast host [192.168.0.200]: " ICECAST_HOST
|
|
ICECAST_HOST="${ICECAST_HOST:-192.168.0.200}"
|
|
|
|
read -rp "Icecast source port [7777]: " ICECAST_PORT
|
|
ICECAST_PORT="${ICECAST_PORT:-7777}"
|
|
|
|
read -rp "Icecast mount point [/data]: " ICECAST_MOUNT
|
|
ICECAST_MOUNT="${ICECAST_MOUNT:-/data}"
|
|
|
|
read -rp "Icecast source username [source]: " SOURCE_USER
|
|
SOURCE_USER="${SOURCE_USER:-source}"
|
|
|
|
read -rsp "Icecast source password: " SOURCE_PASS
|
|
echo
|
|
|
|
GPODDER_ENABLE=""
|
|
until [[ "$GPODDER_ENABLE" =~ ^[YyNn]$ ]]; do
|
|
read -rp "Enable gPodder.net sync? [y/N]: " GPODDER_ENABLE
|
|
done
|
|
GPODDER_ENABLED=false
|
|
[[ "$GPODDER_ENABLE" =~ ^[Yy]$ ]] && GPODDER_ENABLED=true
|
|
|
|
GPODDER_HOST="gpodder.net"
|
|
GPODDER_USER=""
|
|
GPODDER_PASS=""
|
|
if [[ "$GPODDER_ENABLED" == true ]]; then
|
|
read -rp "gPodder.net host [gpodder.net]: " GPODDER_HOST_IN
|
|
GPODDER_HOST="${GPODDER_HOST_IN:-gpodder.net}"
|
|
read -rp "gPodder.net username: " GPODDER_USER
|
|
read -rsp "gPodder.net password: " GPODDER_PASS
|
|
echo
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Create directory structure
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "Creating directory structure under ${STORAGE} ..."
|
|
mkdir -p "${STORAGE}"/{state,podcasts,music,jingles,announcements,playlists,logs}
|
|
chown -R liquidsoap:liquidsoap "${STORAGE}"
|
|
chmod -R u+rwX,g+rwX,o+rX "${STORAGE}"
|
|
echo "Done."
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Generate config.json
|
|
# ---------------------------------------------------------------------------
|
|
CONFIG_FILE="${SCRIPT_DIR}/config.json"
|
|
cat > "$CONFIG_FILE" <<EOF
|
|
{
|
|
"storage": "${STORAGE}",
|
|
"icecast": {
|
|
"host": "${ICECAST_HOST}",
|
|
"port": ${ICECAST_PORT},
|
|
"mount": "${ICECAST_MOUNT}",
|
|
"source_username": "${SOURCE_USER}",
|
|
"source_password": "${SOURCE_PASS}"
|
|
},
|
|
"gpodder": {
|
|
"enable": ${GPODDER_ENABLED},
|
|
"host": "https://${GPODDER_HOST}",
|
|
"username": "${GPODDER_USER}",
|
|
"password": "${GPODDER_PASS}"
|
|
}
|
|
}
|
|
EOF
|
|
chown liquidsoap:liquidsoap "$CONFIG_FILE"
|
|
chmod 640 "$CONFIG_FILE"
|
|
echo "Wrote ${CONFIG_FILE}"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ensure SQLite databases exist with correct schema
|
|
# ---------------------------------------------------------------------------
|
|
STATE_DIR="${STORAGE}/state"
|
|
SUBS_DB="${STATE_DIR}/subscriptions.db"
|
|
PLAYED_DB="${STATE_DIR}/played.db"
|
|
|
|
ensure_schema() {
|
|
local db="$1" table_sql="$2" index_sql="$3"
|
|
sqlite3 "$db" <<<"$table_sql"
|
|
[[ -n "$index_sql" ]] && sqlite3 "$db" <<<"$index_sql"
|
|
sqlite3 "$db" "PRAGMA journal_mode=WAL;"
|
|
}
|
|
|
|
SHOWS_SQL='CREATE TABLE IF NOT EXISTS shows (
|
|
slug TEXT PRIMARY KEY,
|
|
guid TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
feed_url TEXT NOT NULL UNIQUE,
|
|
source TEXT DEFAULT '"'"'manual'"'"',
|
|
opml_import INTEGER DEFAULT 0,
|
|
archived INTEGER DEFAULT 1,
|
|
media_class TEXT,
|
|
created_at TEXT
|
|
);'
|
|
|
|
EPISODES_SQL='CREATE TABLE IF NOT EXISTS episodes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
show_slug TEXT NOT NULL,
|
|
guid TEXT NOT NULL,
|
|
title TEXT,
|
|
file_path TEXT,
|
|
enclosure_url TEXT,
|
|
runlength INTEGER,
|
|
played INTEGER DEFAULT 0,
|
|
played_at TEXT,
|
|
UNIQUE (show_slug, guid)
|
|
);'
|
|
|
|
INDEX_EPISODES_SQL='CREATE INDEX IF NOT EXISTS idx_episodes_show_played ON episodes (show_slug, played);'
|
|
|
|
echo
|
|
echo "Initializing SQLite databases..."
|
|
ensure_schema "$SUBS_DB" "$SHOWS_SQL" ""
|
|
ensure_schema "$PLAYED_DB" "$EPISODES_SQL" "$INDEX_EPISODES_SQL"
|
|
chown liquidsoap:liquidsoap "$SUBS_DB" "$PLAYED_DB"
|
|
echo "Done."
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Install JRuby runtime dependencies (one gem per command to bound memory)
|
|
# ---------------------------------------------------------------------------
|
|
JRUBY_BIN="/opt/jruby/bin/jruby"
|
|
GEM_BIN="/opt/jruby/bin/gem"
|
|
GEMS_HOME="${SCRIPT_DIR}/.gems"
|
|
|
|
if [[ -x "$JRUBY_BIN" ]]; then
|
|
echo
|
|
echo "Installing JRuby gem dependencies into ${GEMS_HOME} ..."
|
|
mkdir -p "$GEMS_HOME"
|
|
chown liquidsoap:liquidsoap "$GEMS_HOME"
|
|
|
|
install_gem() {
|
|
local gem_name="$1"
|
|
shift
|
|
local extra_args=("$@")
|
|
echo " -> gem install ${gem_name} ${extra_args[*]}"
|
|
sudo -u liquidsoap bash -c "
|
|
export GEM_HOME=${GEMS_HOME}
|
|
export GEM_PATH=${GEMS_HOME}:/opt/jruby/lib/ruby/gems/shared
|
|
${GEM_BIN} install ${gem_name} ${extra_args[*]} --no-document
|
|
" || echo " WARNING: failed to install ${gem_name}"
|
|
}
|
|
|
|
# Core persistence
|
|
install_gem "sequel"
|
|
install_gem "jdbc-sqlite3"
|
|
|
|
# XML parsing. Pure-Java build under JRuby (--platform java) bundles
|
|
# Xerces/NekoHTML/Xalan as JARs; no libxml2-dev or compiler required.
|
|
install_gem "nokogiri" "--platform" "java"
|
|
|
|
# Convenience JSON (usually stdlib, but ensure availability)
|
|
install_gem "json"
|
|
|
|
echo "JRUBY DEPENDENCIES COMPLETE"
|
|
else
|
|
echo
|
|
echo "WARNING: ${JRUBY_BIN} not found; skipping JRuby gem installation."
|
|
echo " Install JRuby manually, then re-run this installer or run:"
|
|
echo " sudo -u liquidsoap ${GEM_BIN} install sequel jdbc-sqlite3 json"
|
|
echo " sudo -u liquidsoap ${GEM_BIN} install nokogiri --platform java"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Self-locating launcher (sets GEM_HOME/GEM_PATH before JRuby boots)
|
|
# ---------------------------------------------------------------------------
|
|
LAUNCHER="${SCRIPT_DIR}/run_radio.sh"
|
|
cat > "$LAUNCHER" <<'LAUNCH_EOF'
|
|
#!/usr/bin/env bash
|
|
# run_radio.sh - self-locating JRuby launcher for the radio automation scripts.
|
|
# Exports GEM_HOME/GEM_PATH BEFORE jruby boots, because mutating them inside a
|
|
# running JRuby process does not reliably affect gem resolution (JRuby #5269).
|
|
#
|
|
# Usage: ./run_radio.sh <script.rb> [args...]
|
|
set -euo pipefail
|
|
|
|
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
GEMS_HOME="${SELF_DIR}/.gems"
|
|
JRUBY_HOME="/opt/jruby"
|
|
|
|
export GEM_HOME="${GEMS_HOME}"
|
|
export GEM_PATH="${GEMS_HOME}:${JRUBY_HOME}/lib/ruby/gems/shared"
|
|
|
|
exec "${JRUBY_HOME}/bin/jruby" "${SELF_DIR}/$@"
|
|
LAUNCH_EOF
|
|
chmod +x "$LAUNCHER"
|
|
chown liquidsoap:liquidsoap "$LAUNCHER"
|
|
echo "Wrote ${LAUNCHER}"
|
|
|
|
# Make the Ruby scripts executable
|
|
for rb in "${SCRIPT_DIR}"/*.rb; do
|
|
[[ -f "$rb" ]] && chmod +x "$rb" && chown liquidsoap:liquidsoap "$rb"
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Crontab entries (installed into liquidsoap's crontab, not root's)
|
|
# ---------------------------------------------------------------------------
|
|
CRON_FETCH="*/30 * * * * cd ${SCRIPT_DIR} && ./run_radio.sh fetch_podcasts.rb >> ${STORAGE}/logs/cron_fetch.log 2>&1"
|
|
CRON_UPDATE="15 * * * * cd ${SCRIPT_DIR} && ./run_radio.sh update_playlists.rb >> ${STORAGE}/logs/cron_update.log 2>&1"
|
|
|
|
echo
|
|
echo "Installing crontab entries for user 'liquidsoap'..."
|
|
( crontab -l -u liquidsoap 2>/dev/null || true ) | grep -vF "run_radio.sh" | \
|
|
{ cat; echo "$CRON_FETCH"; echo "$CRON_UPDATE"; } | crontab -u liquidsoap -
|
|
echo "Crontab updated."
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Optional systemd service
|
|
# ---------------------------------------------------------------------------
|
|
ENABLE_SERVICE=""
|
|
until [[ "$ENABLE_SERVICE" =~ ^[YyNn]$ ]]; do
|
|
read -rp "Enable ${SERVICE_NAME} systemd service now? [y/N]: " ENABLE_SERVICE
|
|
done
|
|
|
|
if [[ "$ENABLE_SERVICE" =~ ^[Yy]$ ]]; then
|
|
echo
|
|
echo "Writing ${SYSTEMD_UNIT} ..."
|
|
cat > "$SYSTEMD_UNIT" <<UNIT_EOF
|
|
[Unit]
|
|
Description=${SERVICE_NAME} radio automation (liquidsoap)
|
|
After=network-online.target icecast2.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=liquidsoap
|
|
WorkingDirectory=${SCRIPT_DIR}
|
|
ExecStart=${SCRIPT_DIR}/station.liq
|
|
Restart=on-failure
|
|
RestartSec=10
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNIT_EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable --now "${SERVICE_NAME}"
|
|
echo "Service ${SERVICE_NAME} enabled and started."
|
|
else
|
|
echo "Skipping systemd service (you can enable it later with:"
|
|
echo " sudo cp ${SCRIPT_DIR}/${SERVICE_NAME}.service /etc/systemd/system/ && sudo systemctl enable --now ${SERVICE_NAME})"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Summary
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "=============================================="
|
|
echo " Installation complete."
|
|
echo "----------------------------------------------"
|
|
echo " Storage: ${STORAGE}"
|
|
echo " Config: ${CONFIG_FILE}"
|
|
echo " Launcher: ${LAUNCHER}"
|
|
echo " Service: ${SERVICE_NAME}"
|
|
echo "----------------------------------------------"
|
|
echo " Quick start:"
|
|
echo " cd ${SCRIPT_DIR}"
|
|
echo " sudo -u liquidsoap ./run_radio.sh fetch_podcasts.rb --list-shows"
|
|
echo " sudo -u liquidsoap ./run_radio.sh fetch_podcasts.rb --add-show <rss-url>"
|
|
echo " sudo -u liquidsoap ./run_radio.sh update_playlists.rb"
|
|
echo "=============================================="
|