mirror of
https://github.com/mistergibson/radio.git
synced 2026-09-08 22:09:51 -07:00
459 lines
14 KiB
Shell
Executable file
459 lines
14 KiB
Shell
Executable file
#!/bin/bash
|
|
#
|
|
# install_for_python - Setup for liquidsoap radio automation stack
|
|
# Target: Linux Mint 22.3 (Ubuntu 24.04 Noble based)
|
|
#
|
|
# Usage: sudo ./install_for_python
|
|
# Must be run from within the target directory (e.g., /srv/audio/)
|
|
#
|
|
set -euo pipefail
|
|
|
|
# -------------------------------------------------------
|
|
# Determine install directory from script location
|
|
# -------------------------------------------------------
|
|
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
INSTALL_DIR="$(dirname "$SCRIPT_PATH")"
|
|
SERVICE_NAME="$(basename "$INSTALL_DIR")"
|
|
LIQUIDSOAP_USER="liquidsoap"
|
|
|
|
echo "=== Radio Automation Installer ==="
|
|
echo "Install directory: $INSTALL_DIR"
|
|
echo "Service name: $SERVICE_NAME"
|
|
echo ""
|
|
|
|
# -------------------------------------------------------
|
|
# 0. Validate environment
|
|
# -------------------------------------------------------
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "ERROR: This script must be run as root (sudo)."
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -d "$INSTALL_DIR" ]; then
|
|
echo "ERROR: Install directory $INSTALL_DIR does not exist."
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -w "$INSTALL_DIR" ]; then
|
|
echo "ERROR: Cannot write to $INSTALL_DIR. Check permissions."
|
|
exit 1
|
|
fi
|
|
|
|
REQUIRED_DIRS=(
|
|
"music"
|
|
"podcasts"
|
|
"jingles"
|
|
"announcements"
|
|
"playlists"
|
|
"state"
|
|
"logs"
|
|
)
|
|
|
|
# -------------------------------------------------------
|
|
# Helper: prompt with default value
|
|
# -------------------------------------------------------
|
|
prompt() {
|
|
local var_name="$1"
|
|
local prompt_text="$2"
|
|
local default_value="${3:-}"
|
|
local hide_input="${4:-false}"
|
|
|
|
if [ -n "$default_value" ]; then
|
|
read -rp "$prompt_text [$default_value]: " input
|
|
if [ -z "$input" ]; then
|
|
eval "$var_name=$default_value"
|
|
else
|
|
eval "$var_name=\$input"
|
|
fi
|
|
else
|
|
if [ "$hide_input" = "true" ]; then
|
|
read -rsp "$prompt_text: " input
|
|
echo ""
|
|
eval "$var_name=\$input"
|
|
else
|
|
read -rp "$prompt_text: " input
|
|
eval "$var_name=\$input"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
prompt_confirm() {
|
|
local var_name="$1"
|
|
local prompt_text="$2"
|
|
local default_value="${3:-no}"
|
|
read -rp "$prompt_text [$default_value]: " input
|
|
if [ -z "$input" ]; then
|
|
input="$default_value"
|
|
fi
|
|
case "$input" in
|
|
[Yy]* ) eval "$var_name=true" ;;
|
|
* ) eval "$var_name=false" ;;
|
|
esac
|
|
}
|
|
|
|
# -------------------------------------------------------
|
|
# 1. Interactive configuration
|
|
# -------------------------------------------------------
|
|
CONFIG_FILE="$INSTALL_DIR/config.json"
|
|
|
|
# Load existing config as defaults if present
|
|
EXISTING_CONFIG=false
|
|
if [ -f "$CONFIG_FILE" ]; then
|
|
EXISTING_CONFIG=true
|
|
echo "--- Loading existing config.json for defaults ---"
|
|
ICECAST_HOST_DEFAULT=$(jq -r '.icecast.host // "localhost"' "$CONFIG_FILE")
|
|
ICECAST_PORT_DEFAULT=$(jq -r '.icecast.port // 7777' "$CONFIG_FILE")
|
|
ICECAST_MOUNT_DEFAULT=$(jq -r '.icecast.mount // "/audio.mp3"' "$CONFIG_FILE")
|
|
ICECAST_USERNAME_DEFAULT=$(jq -r '.icecast.username // "source"' "$CONFIG_FILE")
|
|
GPODDER_ENABLE_DEFAULT=$(jq -r 'if .gpodder.enable == true then "yes" else "no" end' "$CONFIG_FILE")
|
|
GPODDER_HOST_DEFAULT=$(jq -r '.gpodder.host // "https://gpodder.net"' "$CONFIG_FILE")
|
|
GPODDER_USERNAME_DEFAULT=$(jq -r '.gpodder.username // ""' "$CONFIG_FILE")
|
|
else
|
|
ICECAST_HOST_DEFAULT="localhost"
|
|
ICECAST_PORT_DEFAULT="7777"
|
|
ICECAST_MOUNT_DEFAULT="/audio.mp3"
|
|
ICECAST_USERNAME_DEFAULT="source"
|
|
GPODDER_ENABLE_DEFAULT="no"
|
|
GPODDER_HOST_DEFAULT="https://gpodder.net"
|
|
GPODDER_USERNAME_DEFAULT=""
|
|
fi
|
|
|
|
echo ""
|
|
echo "--- Icecast Settings ---"
|
|
echo ""
|
|
|
|
ICECAST_HOST=""
|
|
ICECAST_PORT=""
|
|
ICECAST_MOUNT=""
|
|
ICECAST_USERNAME=""
|
|
ICECAST_PASSWORD=""
|
|
|
|
while true; do
|
|
prompt ICECAST_HOST "Icecast host" "$ICECAST_HOST_DEFAULT"
|
|
[[ "$ICECAST_HOST" =~ ^[a-zA-Z0-9._-]+$ ]] && break
|
|
echo "Invalid hostname. Try again."
|
|
done
|
|
|
|
while true; do
|
|
prompt ICECAST_PORT "Source port" "$ICECAST_PORT_DEFAULT"
|
|
[[ "$ICECAST_PORT" =~ ^[0-9]{1,5}$ ]] && (( ICECAST_PORT >= 1 )) && (( ICECAST_PORT <= 65535 )) && break
|
|
echo "Port must be a number between 1 and 65535. Try again."
|
|
done
|
|
|
|
while true; do
|
|
prompt ICECAST_MOUNT "Mount point" "$ICECAST_MOUNT_DEFAULT"
|
|
[[ "$ICECAST_MOUNT" == /* ]] && break
|
|
echo "Mount point must start with /. Try again."
|
|
done
|
|
|
|
prompt ICECAST_USERNAME "Source username" "$ICECAST_USERNAME_DEFAULT"
|
|
|
|
while true; do
|
|
prompt ICECAST_PASSWORD "Source password" "" "true"
|
|
[ -n "$ICECAST_PASSWORD" ] && break
|
|
echo "Password cannot be empty. Try again."
|
|
done
|
|
|
|
echo ""
|
|
echo "--- gPodder.net Sync Settings ---"
|
|
echo ""
|
|
|
|
GPODDER_ENABLE=false
|
|
if [ "$GPODDER_ENABLE_DEFAULT" = "yes" ]; then
|
|
prompt_confirm GPODDER_ENABLE "Enable gPodder.net subscription sync?" "yes"
|
|
else
|
|
prompt_confirm GPODDER_ENABLE "Enable gPodder.net subscription sync?" "no"
|
|
fi
|
|
|
|
GPODDER_HOST=""
|
|
GPODDER_USERNAME=""
|
|
GPODDER_PASSWORD=""
|
|
|
|
if [ "$GPODDER_ENABLE" = "true" ]; then
|
|
prompt GPODDER_HOST "gPodder host" "$GPODDER_HOST_DEFAULT"
|
|
while true; do
|
|
prompt GPODDER_USERNAME "gPodder username/email" "$GPODDER_USERNAME_DEFAULT"
|
|
[ -n "$GPODDER_USERNAME" ] && break
|
|
echo "Username cannot be empty. Try again."
|
|
done
|
|
while true; do
|
|
prompt GPODDER_PASSWORD "gPodder password" "" "true"
|
|
[ -n "$GPODDER_PASSWORD" ] && break
|
|
echo "Password cannot be empty. Try again."
|
|
done
|
|
fi
|
|
|
|
echo ""
|
|
echo "--- Summary ---"
|
|
echo " Icecast: ${ICECAST_HOST}:${ICECAST_PORT}${ICECAST_MOUNT} (user: ${ICECAST_USERNAME})"
|
|
echo " gPodder: $( [ "$GPODDER_ENABLE" = "true" ] && echo "enabled (${GPODDER_USERNAME}@${GPODDER_HOST})" || echo "disabled" )"
|
|
echo ""
|
|
|
|
CONFIRM_INSTALL=false
|
|
prompt_confirm CONFIRM_INSTALL "Proceed with these settings?" "yes"
|
|
if [ "$CONFIRM_INSTALL" != "true" ]; then
|
|
echo "Aborted by user."
|
|
exit 0
|
|
fi
|
|
|
|
# -------------------------------------------------------
|
|
# 2. System packages
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Installing system packages ---"
|
|
apt-get update
|
|
apt-get install -y \
|
|
liquidsoap \
|
|
icecast2 \
|
|
jq \
|
|
python3 \
|
|
python3-pip \
|
|
python3-venv \
|
|
ffmpeg \
|
|
lame \
|
|
libtag1-dev \
|
|
curl \
|
|
ca-certificates
|
|
|
|
# -------------------------------------------------------
|
|
# 3. Python virtual environment
|
|
# -------------------------------------------------------
|
|
echo "--- Setting up Python virtualenv ---"
|
|
VENV_DIR="$INSTALL_DIR/.venv"
|
|
python3 -m venv "$VENV_DIR"
|
|
"$VENV_DIR/bin/pip" install --upgrade pip
|
|
"$VENV_DIR/bin/pip" install \
|
|
feedparser \
|
|
requests \
|
|
mutagen
|
|
|
|
PYTHON_BIN="$VENV_DIR/bin/python3"
|
|
|
|
# -------------------------------------------------------
|
|
# 4. Create and verify directory structure
|
|
# -------------------------------------------------------
|
|
echo "--- Creating directory structure ---"
|
|
MISSING=()
|
|
for dir in "${REQUIRED_DIRS[@]}"; do
|
|
full_path="$INSTALL_DIR/$dir"
|
|
if [ ! -d "$full_path" ]; then
|
|
mkdir -p "$full_path"
|
|
MISSING+=("$dir")
|
|
fi
|
|
done
|
|
|
|
if [ ${#MISSING[@]} -gt 0 ]; then
|
|
echo " Created: ${MISSING[*]}"
|
|
else
|
|
echo " All directories already present."
|
|
fi
|
|
|
|
FAILED=()
|
|
for dir in "${REQUIRED_DIRS[@]}"; do
|
|
if [ ! -d "$INSTALL_DIR/$dir" ]; then
|
|
FAILED+=("$dir")
|
|
fi
|
|
done
|
|
|
|
if [ ${#FAILED[@]} -gt 0 ]; then
|
|
echo "ERROR: Failed to create directories: ${FAILED[*]}"
|
|
exit 1
|
|
fi
|
|
|
|
# -------------------------------------------------------
|
|
# 5. Generate config.json from collected settings
|
|
# -------------------------------------------------------
|
|
echo "--- Generating config.json ---"
|
|
|
|
cat > "$CONFIG_FILE" << EOF
|
|
{
|
|
"icecast": {
|
|
"host": "${ICECAST_HOST}",
|
|
"port": ${ICECAST_PORT},
|
|
"mount": "${ICECAST_MOUNT}",
|
|
"username": "${ICECAST_USERNAME}",
|
|
"password": "${ICECAST_PASSWORD}"
|
|
},
|
|
"gpodder": {
|
|
"enable": ${GPODDER_ENABLE},
|
|
"host": "${GPODDER_HOST}",
|
|
"username": "${GPODDER_USERNAME}",
|
|
"password": "${GPODDER_PASSWORD}"
|
|
}
|
|
}
|
|
EOF
|
|
|
|
chmod 600 "$CONFIG_FILE"
|
|
echo " Written: $CONFIG_FILE"
|
|
|
|
# -------------------------------------------------------
|
|
# 6. Create schedule.txt template (if not present)
|
|
# -------------------------------------------------------
|
|
SCHEDULE_FILE="$INSTALL_DIR/schedule.txt"
|
|
if [ ! -f "$SCHEDULE_FILE" ]; then
|
|
echo "--- Creating schedule.txt template ---"
|
|
cat > "$SCHEDULE_FILE" << 'EOF'
|
|
# Radio Schedule
|
|
# Format: min hour dom mon dow TYPE TARGET [RUNLENGTH_SECONDS]
|
|
# TYPE: show | stream
|
|
# RUNLENGTH required for stream entries, ignored for show entries
|
|
#
|
|
# Examples:
|
|
# 0 8 * * 2 show hardcore_history
|
|
# 0 6 * * 1 stream http://example.org:8000/live.mp3 3600
|
|
EOF
|
|
else
|
|
echo "--- schedule.txt already exists, skipping ---"
|
|
fi
|
|
|
|
# -------------------------------------------------------
|
|
# 7. Verify required project files exist
|
|
# -------------------------------------------------------
|
|
echo "--- Verifying project files ---"
|
|
PROJECT_FILES=("station.liq" "fetch_podcasts.py" "update_playlists.py")
|
|
FILE_ERRORS=()
|
|
for f in "${PROJECT_FILES[@]}"; do
|
|
if [ ! -f "$INSTALL_DIR/$f" ]; then
|
|
FILE_ERRORS+=("$f")
|
|
else
|
|
echo " Found: $f"
|
|
fi
|
|
done
|
|
|
|
if [ ${#FILE_ERRORS[@]} -gt 0 ]; then
|
|
echo "WARNING: Missing expected files: ${FILE_ERRORS[*]}"
|
|
echo " The service will not start until these are in place."
|
|
fi
|
|
|
|
# -------------------------------------------------------
|
|
# 8. Create systemd service named after the directory
|
|
# -------------------------------------------------------
|
|
echo "--- Creating systemd service: ${SERVICE_NAME}.service ---"
|
|
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
|
|
[Unit]
|
|
Description=Liquidsoap radio automation (${SERVICE_NAME})
|
|
After=network.target icecast2.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${LIQUIDSOAP_USER}
|
|
WorkingDirectory=${INSTALL_DIR}
|
|
ExecStart=/usr/bin/liquidsoap ${INSTALL_DIR}/station.liq
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
StandardOutput=append:${INSTALL_DIR}/logs/liquidsoap.log
|
|
StandardError=append:${INSTALL_DIR}/logs/liquidsoap.log
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
# -------------------------------------------------------
|
|
# 9. Create cron jobs (hourly fetch + hourly playlist update)
|
|
# -------------------------------------------------------
|
|
echo "--- Setting up cron jobs ---"
|
|
|
|
FETCH_CRON="0 * * * * cd $INSTALL_DIR && $PYTHON_BIN fetch_podcasts.py >> $INSTALL_DIR/logs/fetch.log 2>&1"
|
|
UPDATE_CRON="30 * * * * cd $INSTALL_DIR && $PYTHON_BIN update_playlists.py >> $INSTALL_DIR/logs/update.log 2>&1"
|
|
|
|
# Pull existing crontab (empty if none)
|
|
EXISTING_CRON=$(crontab -l -u "$LIQUIDSOAP_USER" 2>/dev/null || true)
|
|
|
|
# Remove any stale entries for these scripts to avoid duplicates on re-runs
|
|
CLEANED_CRON=$(echo "$EXISTING_CRON" | grep -v "fetch_podcasts.py\|update_playlists.py" || true)
|
|
|
|
# Add fetch job if not present
|
|
if ! echo "$EXISTING_CRON" | grep -q "fetch_podcasts.py"; then
|
|
CLEANED_CRON="${CLEANED_CRON:+$CLEANED_CRON
|
|
}$FETCH_CRON"
|
|
echo " Added: fetch_podcasts.py (hourly)"
|
|
else
|
|
echo " Skipped: fetch_podcasts.py job already exists"
|
|
fi
|
|
|
|
# Add update job if not present
|
|
if ! echo "$EXISTING_CRON" | grep -q "update_playlists.py"; then
|
|
CLEANED_CRON="${CLEANED_CRON:+$CLEANED_CRON
|
|
}$UPDATE_CRON"
|
|
echo " Added: update_playlists.py (hourly at :30)"
|
|
else
|
|
echo " Skipped: update_playlists.py job already exists"
|
|
fi
|
|
|
|
# Write back the combined crontab
|
|
echo "$CLEANED_CRON" | crontab -u "$LIQUIDSOAP_USER" -
|
|
|
|
# -------------------------------------------------------
|
|
# 10. Permissions
|
|
# -------------------------------------------------------
|
|
echo "--- Setting ownership and permissions ---"
|
|
id -u "$LIQUIDSOAP_USER" &>/dev/null || useradd --system --shell /usr/sbin/nologin "$LIQUIDSOAP_USER"
|
|
chown -R "${LIQUIDSOAP_USER}:${LIQUIDSOAP_USER}" "$INSTALL_DIR"
|
|
chmod 700 "$INSTALL_DIR/state"
|
|
chmod 700 "$INSTALL_DIR/logs"
|
|
|
|
systemctl daemon-reload
|
|
|
|
# -------------------------------------------------------
|
|
# 11. Final verification
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "=== Final Verification ==="
|
|
ALL_OK=true
|
|
for dir in "${REQUIRED_DIRS[@]}"; do
|
|
if [ -d "$INSTALL_DIR/$dir" ] && [ -r "$INSTALL_DIR/$dir" ] && [ -w "$INSTALL_DIR/$dir" ]; then
|
|
printf " [OK] %-20s\n" "$dir/"
|
|
else
|
|
printf " [FAIL] %-20s (missing or inaccessible)\n" "$dir/"
|
|
ALL_OK=false
|
|
fi
|
|
done
|
|
|
|
if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
|
|
echo " [OK] Service file: ${SERVICE_NAME}.service"
|
|
else
|
|
echo " [FAIL] Service file not created"
|
|
ALL_OK=false
|
|
fi
|
|
|
|
if [ -f "$CONFIG_FILE" ]; then
|
|
if jq empty "$CONFIG_FILE" 2>/dev/null; then
|
|
echo " [OK] Config: config.json (valid JSON)"
|
|
else
|
|
echo " [FAIL] Config: config.json (invalid JSON)"
|
|
ALL_OK=false
|
|
fi
|
|
else
|
|
echo " [FAIL] Config file missing"
|
|
ALL_OK=false
|
|
fi
|
|
|
|
if [ -f "$SCHEDULE_FILE" ]; then
|
|
echo " [OK] Schedule: schedule.txt"
|
|
else
|
|
echo " [FAIL] Schedule file missing"
|
|
ALL_OK=false
|
|
fi
|
|
|
|
echo ""
|
|
if [ "$ALL_OK" = true ]; then
|
|
echo "=== Installation Complete ==="
|
|
else
|
|
echo "=== Installation Finished With Errors ==="
|
|
echo "Review the [FAIL] items above before starting the service."
|
|
fi
|
|
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Place your music in $INSTALL_DIR/music/"
|
|
echo " 2. Edit $SCHEDULE_FILE with your show/stream schedule"
|
|
echo " 3. Review/edit $INSTALL_DIR/station.liq"
|
|
echo " 4. Test: sudo systemctl start icecast2 && sudo systemctl start ${SERVICE_NAME}"
|
|
echo " 5. Check logs: tail -f $INSTALL_DIR/logs/liquidsoap.log"
|
|
echo ""
|
|
echo "Manage shows:"
|
|
echo " List: $PYTHON_BIN $INSTALL_DIR/fetch_podcasts.py --list-shows --detail"
|
|
echo " Delete: $PYTHON_BIN $INSTALL_DIR/fetch_podcasts.py --delete-show <slug>"
|
|
echo " Import OPML: $PYTHON_BIN $INSTALL_DIR/fetch_podcasts.py --import-opml <file.opml>"
|
|
echo " Add show: $PYTHON_BIN $INSTALL_DIR/fetch_podcasts.py --add-show <feed-url>"
|
|
echo " JSON state: $PYTHON_BIN $INSTALL_DIR/update_playlists.py --json"
|