feat: Add auto-scan on startup and live Icecast/Liquidsoap status checks

- Auto-scan music library on startup to load existing tracks
- Add check-icecast-status() function to query Icecast API
- Add check-liquidsoap-status() function to check Docker container
- Update admin dashboard to show real-time streaming status
- Eliminates need to manually copy files from incoming on every restart
This commit is contained in:
Glenn Thompson 2025-10-04 05:44:33 +03:00 committed by Brian O'Reilly
parent ce39a0ca1a
commit 24feeddfa8
16 changed files with 2880 additions and 34 deletions

24
.gitignore vendored
View File

@ -17,19 +17,6 @@
*.wx32fsl
/slime.lisp
asteroid
buildapp
quicklisp-manifest.txt
notes/
run-asteroid.sh
build-sbcl.sh
# Music files - don't commit audio files to repository
*.mp3
*.flac
*.ogg
*.wav
*.m4a
*.aac
*.wma
# Docker music directory - keep folder but ignore music files
@ -56,15 +43,6 @@ docker-compose.yml.backup.*
# Log files
*.log
logs/
performance-logs/
# Temporary files
*.tmp
*.temp
.DS_Store
Thumbs.db
# Shell scripts (exclude from repository)
*.sh
# Exception: Docker utility scripts should be included
!docker/start.sh
!docker/stop.sh

271
analyze-performance.py Normal file
View File

@ -0,0 +1,271 @@
#!/usr/bin/env python3
"""
Asteroid Radio Performance Analysis Tool
Generates graphs and reports from performance test data
"""
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import glob
import os
from datetime import datetime
import numpy as np
# Set up plotting style
plt.style.use('dark_background')
sns.set_palette("husl")
def load_performance_data():
"""Load all CSV performance data files"""
csv_files = glob.glob('performance-logs/*_data_*.csv')
data_frames = {}
for file in csv_files:
# Extract test type from filename
filename = os.path.basename(file)
if 'aac' in filename:
test_type = 'AAC 96kbps'
elif 'mp3-high' in filename:
test_type = 'MP3 128kbps'
elif 'mp3-low' in filename:
test_type = 'MP3 64kbps'
else:
test_type = filename.split('_')[1]
try:
df = pd.read_csv(file)
df['test_type'] = test_type
df['timestamp'] = pd.to_datetime(df['timestamp'])
data_frames[test_type] = df
print(f"✅ Loaded {len(df)} records from {test_type} test")
except Exception as e:
print(f"❌ Error loading {file}: {e}")
return data_frames
def create_performance_dashboard(data_frames):
"""Create comprehensive performance dashboard"""
# Combine all data
all_data = pd.concat(data_frames.values(), ignore_index=True)
# Create figure with subplots
fig, axes = plt.subplots(2, 3, figsize=(20, 12))
fig.suptitle('🎵 Asteroid Radio Performance Analysis Dashboard', fontsize=16, y=0.98)
# 1. CPU Usage Over Time (Asteroid App)
ax1 = axes[0, 0]
for test_type, df in data_frames.items():
if 'asteroid_cpu' in df.columns:
ax1.plot(df.index, df['asteroid_cpu'], label=test_type, linewidth=2)
ax1.set_title('Asteroid App CPU Usage Over Time')
ax1.set_xlabel('Time (samples)')
ax1.set_ylabel('CPU %')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 2. Memory Usage Over Time (Asteroid App)
ax2 = axes[0, 1]
for test_type, df in data_frames.items():
if 'asteroid_mem_mb' in df.columns:
ax2.plot(df.index, df['asteroid_mem_mb'], label=test_type, linewidth=2)
ax2.set_title('Asteroid App Memory Usage Over Time')
ax2.set_xlabel('Time (samples)')
ax2.set_ylabel('Memory (MB)')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 3. Docker Container CPU Usage
ax3 = axes[0, 2]
for test_type, df in data_frames.items():
if 'icecast_cpu' in df.columns and 'liquidsoap_cpu' in df.columns:
ax3.plot(df.index, df['icecast_cpu'], label=f'{test_type} - Icecast', linestyle='--', alpha=0.7)
ax3.plot(df.index, df['liquidsoap_cpu'], label=f'{test_type} - Liquidsoap', linestyle='-', alpha=0.9)
ax3.set_title('Docker Container CPU Usage')
ax3.set_xlabel('Time (samples)')
ax3.set_ylabel('CPU %')
ax3.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
ax3.grid(True, alpha=0.3)
# 4. System Memory Usage
ax4 = axes[1, 0]
for test_type, df in data_frames.items():
if 'system_mem_used_gb' in df.columns and 'system_mem_total_gb' in df.columns:
memory_percent = (df['system_mem_used_gb'] / df['system_mem_total_gb']) * 100
ax4.plot(df.index, memory_percent, label=test_type, linewidth=2)
ax4.set_title('System Memory Usage')
ax4.set_xlabel('Time (samples)')
ax4.set_ylabel('Memory Usage %')
ax4.legend()
ax4.grid(True, alpha=0.3)
# 5. Average Performance Comparison
ax5 = axes[1, 1]
metrics = ['cpu_percent', 'memory_mb', 'stream_response_ms', 'web_response_ms']
test_types = list(data_frames.keys())
performance_summary = {}
for test_type, df in data_frames.items():
performance_summary[test_type] = {
'Asteroid CPU (%)': df['asteroid_cpu'].mean() if 'asteroid_cpu' in df.columns else 0,
'Asteroid Mem (MB)': df['asteroid_mem_mb'].mean() if 'asteroid_mem_mb' in df.columns else 0,
'Icecast CPU (%)': df['icecast_cpu'].mean() if 'icecast_cpu' in df.columns else 0,
'Liquidsoap CPU (%)': df['liquidsoap_cpu'].mean() if 'liquidsoap_cpu' in df.columns else 0
}
summary_df = pd.DataFrame(performance_summary).T
summary_df.plot(kind='bar', ax=ax5)
ax5.set_title('Average Performance Metrics')
ax5.set_ylabel('Value')
ax5.tick_params(axis='x', rotation=45)
ax5.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
# 6. CPU Load Distribution
ax6 = axes[1, 2]
if 'asteroid_cpu' in all_data.columns:
# Create boxplot data manually since pandas boxplot by group is tricky
cpu_data = []
labels = []
for test_type, df in data_frames.items():
if 'asteroid_cpu' in df.columns:
cpu_data.append(df['asteroid_cpu'].values)
labels.append(test_type.replace(' ', '\n'))
if cpu_data:
ax6.boxplot(cpu_data, labels=labels)
ax6.set_title('Asteroid CPU Load Distribution')
ax6.set_xlabel('Stream Type')
ax6.set_ylabel('CPU %')
ax6.tick_params(axis='x', rotation=0)
plt.tight_layout()
plt.savefig('performance-logs/asteroid_performance_dashboard.png', dpi=300, bbox_inches='tight')
print("📊 Dashboard saved as: performance-logs/asteroid_performance_dashboard.png")
return fig
def generate_performance_report(data_frames):
"""Generate detailed performance report"""
report = []
report.append("🎵 ASTEROID RADIO PERFORMANCE ANALYSIS REPORT")
report.append("=" * 50)
report.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
report.append("")
for test_type, df in data_frames.items():
report.append(f"📡 {test_type} Stream Analysis:")
report.append("-" * 30)
if 'asteroid_cpu' in df.columns:
cpu_stats = df['asteroid_cpu'].describe()
report.append(f" Asteroid App CPU:")
report.append(f" Average: {cpu_stats['mean']:.1f}%")
report.append(f" Peak: {cpu_stats['max']:.1f}%")
report.append(f" Minimum: {cpu_stats['min']:.1f}%")
if 'asteroid_mem_mb' in df.columns:
mem_stats = df['asteroid_mem_mb'].describe()
report.append(f" Asteroid App Memory:")
report.append(f" Average: {mem_stats['mean']:.1f} MB")
report.append(f" Peak: {mem_stats['max']:.1f} MB")
report.append(f" Minimum: {mem_stats['min']:.1f} MB")
if 'icecast_cpu' in df.columns:
icecast_stats = df['icecast_cpu'].describe()
report.append(f" Icecast CPU:")
report.append(f" Average: {icecast_stats['mean']:.2f}%")
report.append(f" Peak: {icecast_stats['max']:.2f}%")
if 'liquidsoap_cpu' in df.columns:
liquidsoap_stats = df['liquidsoap_cpu'].describe()
report.append(f" Liquidsoap CPU:")
report.append(f" Average: {liquidsoap_stats['mean']:.1f}%")
report.append(f" Peak: {liquidsoap_stats['max']:.1f}%")
if 'stream_response_ms' in df.columns:
stream_stats = df['stream_response_ms'].dropna().describe()
if len(stream_stats) > 0:
report.append(f" Stream Response:")
report.append(f" Average: {stream_stats['mean']:.1f} ms")
report.append(f" 95th percentile: {stream_stats.quantile(0.95):.1f} ms")
if 'web_response_ms' in df.columns:
web_stats = df['web_response_ms'].dropna().describe()
if len(web_stats) > 0:
report.append(f" Web Response:")
report.append(f" Average: {web_stats['mean']:.1f} ms")
report.append(f" 95th percentile: {web_stats.quantile(0.95):.1f} ms")
report.append("")
# Performance recommendations
report.append("🎯 PERFORMANCE RECOMMENDATIONS:")
report.append("-" * 30)
# Find best performing stream
avg_cpu = {}
for test_type, df in data_frames.items():
if 'asteroid_cpu' in df.columns:
avg_cpu[test_type] = df['asteroid_cpu'].mean()
if avg_cpu:
best_stream = min(avg_cpu, key=avg_cpu.get)
worst_stream = max(avg_cpu, key=avg_cpu.get)
report.append(f" • Most efficient stream: {best_stream} ({avg_cpu[best_stream]:.1f}% avg CPU)")
report.append(f" • Most resource-intensive: {worst_stream} ({avg_cpu[worst_stream]:.1f}% avg CPU)")
if avg_cpu[worst_stream] > 80:
report.append(" ⚠️ High CPU usage detected - consider optimizing or scaling")
elif avg_cpu[best_stream] < 20:
report.append(" ✅ Excellent resource efficiency - system has headroom for more users")
report.append("")
report.append("📈 SCALING INSIGHTS:")
report.append("-" * 20)
total_tests = sum(len(df) for df in data_frames.values())
report.append(f" • Total test duration: ~{total_tests} minutes across all streams")
report.append(f" • System stability: {'✅ Excellent' if total_tests > 40 else '⚠️ Needs more testing'}")
# Save report
with open('performance-logs/asteroid_performance_report.txt', 'w') as f:
f.write('\n'.join(report))
print("📄 Report saved as: performance-logs/asteroid_performance_report.txt")
return '\n'.join(report)
def main():
print("🎵 Asteroid Radio Performance Analyzer")
print("=" * 40)
# Load data
data_frames = load_performance_data()
if not data_frames:
print("❌ No performance data found!")
return
# Create visualizations
print("\n📊 Creating performance dashboard...")
create_performance_dashboard(data_frames)
# Generate report
print("\n📄 Generating performance report...")
report = generate_performance_report(data_frames)
print("\n✅ Analysis complete!")
print("\nGenerated files:")
print(" 📊 performance-logs/asteroid_performance_dashboard.png")
print(" 📄 performance-logs/asteroid_performance_report.txt")
print(f"\n🎯 Quick Summary:")
print(f" Tests completed: {len(data_frames)}")
total_records = sum(len(df) for df in data_frames.values())
print(f" Data points collected: {total_records}")
print(f" Stream formats tested: {', '.join(data_frames.keys())}")
if __name__ == "__main__":
main()

View File

@ -262,6 +262,28 @@
(serve-file (merge-pathnames (concatenate 'string "static/" path)
(asdf:system-source-directory :asteroid))))
;; Status check functions
(defun check-icecast-status ()
"Check if Icecast server is running and accessible"
(handler-case
(let ((response (drakma:http-request "http://localhost:8000/status-json.xsl"
:want-stream nil
:connection-timeout 2)))
(if response "🟢 Running" "🔴 Not Running"))
(error () "🔴 Not Running")))
(defun check-liquidsoap-status ()
"Check if Liquidsoap is running via Docker"
(handler-case
(let* ((output (with-output-to-string (stream)
(uiop:run-program '("docker" "ps" "--filter" "name=liquidsoap" "--format" "{{.Status}}")
:output stream
:error-output nil
:ignore-error-status t)))
(running-p (search "Up" output)))
(if running-p "🟢 Running" "🔴 Not Running"))
(error () "🔴 Not Running")))
;; Admin page (requires authentication)
(define-page admin #@"/admin" ()
"Admin dashboard"
@ -278,8 +300,8 @@
:database-status (handler-case
(if (db:connected-p) "🟢 Connected" "🔴 Disconnected")
(error () "🔴 No Database Backend"))
:liquidsoap-status "🔴 Not Running"
:icecast-status "🔴 Not Running"
:liquidsoap-status (check-liquidsoap-status)
:icecast-status (check-icecast-status)
:track-count (format nil "~d" track-count)
:library-path "/home/glenn/Projects/Code/asteroid/music/library/")))
@ -404,5 +426,13 @@
;; Initialize user management before server starts
(initialize-user-system)
;; Scan music library on startup to load existing tracks
(format t "Scanning music library for existing tracks...~%")
(handler-case
(let ((tracks-added (scan-music-library)))
(format t "✅ Loaded ~a tracks from library~%" tracks-added))
(error (e)
(format t "⚠️ Library scan failed: ~a~%" e)))
(run-server))

263
comprehensive-performance-test.sh Executable file
View File

@ -0,0 +1,263 @@
#!/bin/bash
# Comprehensive Asteroid Performance Testing Script
# Tests Docker streaming + Asteroid web app together
# Usage: ./comprehensive-performance-test.sh [aac|mp3-high|mp3-low]
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOCKER_DIR="$SCRIPT_DIR/docker"
TEST_DURATION=900 # 15 minutes in seconds
STREAM_TYPE="${1:-aac}" # Default to AAC if not specified
# Log file names based on stream type
case "$STREAM_TYPE" in
"aac")
LOG_PREFIX="test-aac"
STREAM_DESC="AAC 96kbps"
;;
"mp3-high")
LOG_PREFIX="test-mp3-high"
STREAM_DESC="MP3 128kbps"
;;
"mp3-low")
LOG_PREFIX="test-mp3-low"
STREAM_DESC="MP3 64kbps"
;;
*)
echo "Usage: $0 [aac|mp3-high|mp3-low]"
exit 1
;;
esac
# Create logs directory
LOGS_DIR="$SCRIPT_DIR/performance-logs"
mkdir -p "$LOGS_DIR"
# Timestamp for this test run
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
LOG_FILE="$LOGS_DIR/${LOG_PREFIX}_${TIMESTAMP}.log"
echo "=== Comprehensive Asteroid Performance Test ===" | tee "$LOG_FILE"
echo "Stream Type: $STREAM_DESC" | tee -a "$LOG_FILE"
echo "Test Duration: 15 minutes" | tee -a "$LOG_FILE"
echo "Started at: $(date)" | tee -a "$LOG_FILE"
echo "Log file: $LOG_FILE" | tee -a "$LOG_FILE"
echo "=========================================" | tee -a "$LOG_FILE"
# Function to cleanup on exit
cleanup() {
echo "" | tee -a "$LOG_FILE"
echo "=== CLEANUP STARTED ===" | tee -a "$LOG_FILE"
# Stop Asteroid application
if [ ! -z "$ASTEROID_PID" ] && kill -0 "$ASTEROID_PID" 2>/dev/null; then
echo "Stopping Asteroid application (PID: $ASTEROID_PID)..." | tee -a "$LOG_FILE"
kill "$ASTEROID_PID" 2>/dev/null || true
sleep 2
kill -9 "$ASTEROID_PID" 2>/dev/null || true
fi
# Stop Docker containers
echo "Stopping Docker containers..." | tee -a "$LOG_FILE"
cd "$DOCKER_DIR"
docker compose down 2>/dev/null || true
# Stop monitoring
if [ ! -z "$MONITOR_PID" ] && kill -0 "$MONITOR_PID" 2>/dev/null; then
echo "Stopping monitoring..." | tee -a "$LOG_FILE"
kill "$MONITOR_PID" 2>/dev/null || true
fi
echo "Cleanup completed at: $(date)" | tee -a "$LOG_FILE"
echo "=== TEST FINISHED ===" | tee -a "$LOG_FILE"
}
# Set trap for cleanup
trap cleanup EXIT INT TERM
# Step 1: Start Docker containers
echo "" | tee -a "$LOG_FILE"
echo "=== STARTING DOCKER CONTAINERS ===" | tee -a "$LOG_FILE"
cd "$DOCKER_DIR"
# Stop any existing containers
docker compose down 2>/dev/null || true
sleep 2
# Start containers
echo "Starting Icecast2 and Liquidsoap containers..." | tee -a "$LOG_FILE"
docker compose up -d 2>&1 | tee -a "$LOG_FILE"
# Wait for containers to be ready
echo "Waiting for containers to initialize..." | tee -a "$LOG_FILE"
sleep 10
# Verify containers are running
if ! docker compose ps | grep -q "Up"; then
echo "ERROR: Docker containers failed to start!" | tee -a "$LOG_FILE"
exit 1
fi
echo "Docker containers started successfully" | tee -a "$LOG_FILE"
# Step 2: Start Asteroid application
echo "" | tee -a "$LOG_FILE"
echo "=== STARTING ASTEROID APPLICATION ===" | tee -a "$LOG_FILE"
cd "$SCRIPT_DIR"
# Build if needed
if [ ! -f "./asteroid" ]; then
echo "Building Asteroid executable..." | tee -a "$LOG_FILE"
make 2>&1 | tee -a "$LOG_FILE"
fi
# Start Asteroid in background
echo "Starting Asteroid web application..." | tee -a "$LOG_FILE"
./asteroid > "$LOGS_DIR/${LOG_PREFIX}_asteroid_${TIMESTAMP}.log" 2>&1 &
ASTEROID_PID=$!
# Wait for Asteroid to start
echo "Waiting for Asteroid to initialize..." | tee -a "$LOG_FILE"
sleep 5
# Verify Asteroid is running
if ! kill -0 "$ASTEROID_PID" 2>/dev/null; then
echo "ERROR: Asteroid application failed to start!" | tee -a "$LOG_FILE"
exit 1
fi
echo "Asteroid application started successfully (PID: $ASTEROID_PID)" | tee -a "$LOG_FILE"
# Step 3: Wait for full system initialization
echo "" | tee -a "$LOG_FILE"
echo "=== SYSTEM INITIALIZATION ===" | tee -a "$LOG_FILE"
echo "Waiting for full system initialization..." | tee -a "$LOG_FILE"
sleep 10
# Test connectivity
echo "Testing system connectivity..." | tee -a "$LOG_FILE"
# Test Icecast
if curl -s "http://localhost:8000/" > /dev/null; then
echo "✓ Icecast2 responding on port 8000" | tee -a "$LOG_FILE"
else
echo "⚠ Icecast2 not responding" | tee -a "$LOG_FILE"
fi
# Test Asteroid web interface
if curl -s "http://localhost:8080/asteroid/" > /dev/null; then
echo "✓ Asteroid web interface responding on port 8080" | tee -a "$LOG_FILE"
else
echo "⚠ Asteroid web interface not responding" | tee -a "$LOG_FILE"
fi
# Step 4: Start monitoring
echo "" | tee -a "$LOG_FILE"
echo "=== STARTING PERFORMANCE MONITORING ===" | tee -a "$LOG_FILE"
echo "Stream: $STREAM_DESC" | tee -a "$LOG_FILE"
echo "Duration: 15 minutes" | tee -a "$LOG_FILE"
echo "Monitoring started at: $(date)" | tee -a "$LOG_FILE"
# Create monitoring function
monitor_performance() {
local monitor_log="$LOGS_DIR/${LOG_PREFIX}_monitor_${TIMESTAMP}.log"
local csv_log="$LOGS_DIR/${LOG_PREFIX}_data_${TIMESTAMP}.csv"
# CSV header
echo "timestamp,icecast_cpu,icecast_mem_mb,liquidsoap_cpu,liquidsoap_mem_mb,asteroid_cpu,asteroid_mem_mb,system_mem_used_gb,system_mem_total_gb" > "$csv_log"
local start_time=$(date +%s)
local end_time=$((start_time + TEST_DURATION))
while [ $(date +%s) -lt $end_time ]; do
local current_time=$(date '+%Y-%m-%d %H:%M:%S')
# Get Docker container stats
local icecast_stats=$(docker stats asteroid-icecast --no-stream --format "{{.CPUPerc}},{{.MemUsage}}" 2>/dev/null || echo "0.00%,0B / 0B")
local liquidsoap_stats=$(docker stats asteroid-liquidsoap --no-stream --format "{{.CPUPerc}},{{.MemUsage}}" 2>/dev/null || echo "0.00%,0B / 0B")
# Parse Docker stats
local icecast_cpu=$(echo "$icecast_stats" | cut -d',' -f1 | sed 's/%//')
local icecast_mem_raw=$(echo "$icecast_stats" | cut -d',' -f2 | cut -d'/' -f1 | sed 's/[^0-9.]//g')
local icecast_mem_mb=$(echo "$icecast_mem_raw" | awk '{print $1/1024/1024}')
local liquidsoap_cpu=$(echo "$liquidsoap_stats" | cut -d',' -f1 | sed 's/%//')
local liquidsoap_mem_raw=$(echo "$liquidsoap_stats" | cut -d',' -f2 | cut -d'/' -f1 | sed 's/[^0-9.]//g')
local liquidsoap_mem_mb=$(echo "$liquidsoap_mem_raw" | awk '{print $1/1024/1024}')
# Get Asteroid process stats
local asteroid_cpu="0.0"
local asteroid_mem_mb="0.0"
if kill -0 "$ASTEROID_PID" 2>/dev/null; then
local asteroid_stats=$(ps -p "$ASTEROID_PID" -o %cpu,rss --no-headers 2>/dev/null || echo "0.0 0")
asteroid_cpu=$(echo "$asteroid_stats" | awk '{print $1}')
local asteroid_mem_kb=$(echo "$asteroid_stats" | awk '{print $2}')
asteroid_mem_mb=$(echo "$asteroid_mem_kb" | awk '{print $1/1024}')
fi
# Get system memory
local mem_info=$(free -g | grep "^Mem:")
local system_mem_used=$(echo "$mem_info" | awk '{print $3}')
local system_mem_total=$(echo "$mem_info" | awk '{print $2}')
# Log to console and file
printf "[%s] Icecast: %s%% CPU, %.1fMB | Liquidsoap: %s%% CPU, %.1fMB | Asteroid: %s%% CPU, %.1fMB | System: %sGB/%sGB\n" \
"$current_time" "$icecast_cpu" "$icecast_mem_mb" "$liquidsoap_cpu" "$liquidsoap_mem_mb" \
"$asteroid_cpu" "$asteroid_mem_mb" "$system_mem_used" "$system_mem_total" | tee -a "$LOG_FILE"
# Log to CSV
printf "%s,%.2f,%.1f,%.2f,%.1f,%.2f,%.1f,%s,%s\n" \
"$current_time" "$icecast_cpu" "$icecast_mem_mb" "$liquidsoap_cpu" "$liquidsoap_mem_mb" \
"$asteroid_cpu" "$asteroid_mem_mb" "$system_mem_used" "$system_mem_total" >> "$csv_log"
sleep 5 # Sample every 5 seconds
done
echo "" | tee -a "$LOG_FILE"
echo "Monitoring completed at: $(date)" | tee -a "$LOG_FILE"
}
# Start monitoring in background
monitor_performance &
MONITOR_PID=$!
# Step 5: Generate some web traffic during monitoring
echo "" | tee -a "$LOG_FILE"
echo "=== GENERATING WEB TRAFFIC ===" | tee -a "$LOG_FILE"
# Function to generate light web traffic
generate_traffic() {
sleep 60 # Wait 1 minute before starting traffic
for i in {1..10}; do
# Test main pages
curl -s "http://localhost:8080/asteroid/" > /dev/null 2>&1 || true
sleep 30
# Test API endpoints
curl -s "http://localhost:8080/asteroid/api/icecast-status" > /dev/null 2>&1 || true
sleep 30
# Test player page
curl -s "http://localhost:8080/asteroid/player/" > /dev/null 2>&1 || true
sleep 30
done
} &
# Wait for monitoring to complete
wait $MONITOR_PID
echo "" | tee -a "$LOG_FILE"
echo "=== TEST SUMMARY ===" | tee -a "$LOG_FILE"
echo "Stream Type: $STREAM_DESC" | tee -a "$LOG_FILE"
echo "Test completed at: $(date)" | tee -a "$LOG_FILE"
echo "Log files created:" | tee -a "$LOG_FILE"
echo " - Main log: $LOG_FILE" | tee -a "$LOG_FILE"
echo " - CSV data: $LOGS_DIR/${LOG_PREFIX}_data_${TIMESTAMP}.csv" | tee -a "$LOG_FILE"
echo " - Asteroid log: $LOGS_DIR/${LOG_PREFIX}_asteroid_${TIMESTAMP}.log" | tee -a "$LOG_FILE"
echo "" | tee -a "$LOG_FILE"
echo "To run next test, switch stream format and run:" | tee -a "$LOG_FILE"
echo " ./comprehensive-performance-test.sh [aac|mp3-high|mp3-low]" | tee -a "$LOG_FILE"

View File

@ -0,0 +1,5 @@
; meta (:version 1.0 :package "COMMON-LISP-USER")
[hash-table equal
(:sessions
[hash-table equalp
(#1="AC457BD7-3E40-469A-83FA-E805C1514C6D" [session:session #1#])])]

View File

@ -33,12 +33,12 @@ Fade requested setting up Liquidsoap in Docker for the Asteroid Radio project, a
** Check Status
#+BEGIN_SRC bash
docker-compose ps
docker compose ps
#+END_SRC
** View Logs
#+BEGIN_SRC bash
docker-compose logs -f
docker compose logs -f
#+END_SRC
** Stop Services

176
docs/API-REFERENCE.org Normal file
View File

@ -0,0 +1,176 @@
#+TITLE: Asteroid Radio - Interface Reference
#+AUTHOR: Interface Team
#+DATE: 2025-10-03
* Current Interfaces
Asteroid Radio currently operates as a Docker-based streaming platform using Icecast2 and Liquidsoap. The system provides streaming interfaces and control mechanisms rather than a traditional REST API.
** Available Interfaces
*** Streaming Endpoints
- **High Quality MP3**: http://localhost:8000/asteroid.mp3 (128kbps)
- **High Quality AAC**: http://localhost:8000/asteroid.aac (96kbps)
- **Low Quality MP3**: http://localhost:8000/asteroid-low.mp3 (64kbps)
*** Administrative Interfaces
- **Icecast Admin**: http://localhost:8000/admin/ (admin/asteroid_admin_2024)
- **Liquidsoap Control**: =telnet localhost 1234= (telnet interface)
* Streaming Interface
** Stream Access
All streams are accessible via standard HTTP and can be played in any media player that supports internet radio streams.
*** Testing Stream Connectivity
#+BEGIN_SRC bash
# Test all three streams
curl -I http://localhost:8000/asteroid.mp3 # 128kbps MP3
curl -I http://localhost:8000/asteroid.aac # 96kbps AAC
curl -I http://localhost:8000/asteroid-low.mp3 # 64kbps MP3
#+END_SRC
*** Playing Streams
#+BEGIN_SRC bashfutu
# With VLC
vlc http://localhost:8000/asteroid.mp3
# With mpv
mpv http://localhost:8000/asteroid.aac
# With curl (save to file)
curl http://localhost:8000/asteroid-low.mp3 > stream.mp3
#+END_SRC
* Icecast Admin Interface
** Web Administration
Access the Icecast admin interface at http://localhost:8000/admin/
*** Login Credentials
- **Username**: admin
- **Password**: asteroid_admin_2024
*** Available Functions
- **Stream Status**: View current streams and listener counts
- **Mount Points**: Manage stream mount points
- **Listener Statistics**: Real-time listener data
- **Server Configuration**: View server settings
- **Log Files**: Access server logs
** Icecast Status XML
Get server status in XML format:
#+BEGIN_SRC bash
curl http://localhost:8000/admin/stats.xml
#+END_SRC
** Stream Statistics
Get individual stream stats:
#+BEGIN_SRC bash
curl http://localhost:8000/admin/stats.xml?mount=/asteroid.mp3
curl http://localhost:8000/admin/stats.xml?mount=/asteroid.aac
curl http://localhost:8000/admin/stats.xml?mount=/asteroid-low.mp3
#+END_SRC
* Liquidsoap Control Interface
** Telnet Access
Connect to Liquidsoap's telnet interface for real-time control:
#+BEGIN_SRC bash
telnet localhost 1234
#+END_SRC
** Available Commands
Once connected via telnet, you can use these commands:
*** Basic Information
#+BEGIN_SRC
help # List all available commands
version # Show Liquidsoap version
uptime # Show server uptime
#+END_SRC
*** Source Control
#+BEGIN_SRC
request.queue # Show current queue
request.push <uri> # Add track to queue
request.skip # Skip current track
#+END_SRC
*** Metadata
#+BEGIN_SRC
request.metadata # Show current track metadata
request.on_air # Show what's currently playing
#+END_SRC
*** Volume and Audio
#+BEGIN_SRC
var.get amplify # Get current amplification level
var.set amplify 1.2 # Set amplification level
#+END_SRC
** Telnet Scripting
You can script Liquidsoap commands:
#+BEGIN_SRC bash
# Get current track info
echo "request.metadata" | nc localhost 1234
# Skip current track
echo "request.skip" | nc localhost 1234
# Check queue status
echo "request.queue" | nc localhost 1234
#+END_SRC
* Docker Container Management
** Container Status
#+BEGIN_SRC bash
# Check running containers
docker compose ps
# View logs
docker compose logs icecast
docker compose logs liquidsoap
# Restart services
docker compose restart
#+END_SRC
** Music Library Management
#+BEGIN_SRC bash
# Add music files (container will detect automatically)
cp ~/path/to/music/*.mp3 docker/music/
cp ~/path/to/music/*.flac docker/music/
# Check what Liquidsoap is seeing
echo "request.queue" | nc localhost 1234
#+END_SRC
* Future Development
** Potential REST API
A REST API may be developed in the future if deemed necessary for:
- **Web Interface**: Browser-based control panel
- **Mobile Applications**: Native mobile apps
- **Third-party Integration**: External service integration
- **User Management**: Account and playlist management
Such an API would likely be built using the RADIANCE Common Lisp web framework and would provide endpoints for:
- Track and playlist management
- User authentication and profiles
- Streaming control and statistics
- System administration
However, the current Docker streaming setup provides all essential functionality through existing interfaces (Icecast admin, Liquidsoap telnet, and direct stream access).
* Getting Help
For support with interfaces and streaming setup:
- Check project documentation and troubleshooting guides
- Review Docker container logs for error messages
- Join our IRC chat room: **#asteroid.music** on **irc.libera.chat**
- Submit issues with detailed system information
This interface reference covers all currently available methods for interacting with Asteroid Radio's streaming infrastructure.
#+END_SRC

412
docs/DEVELOPMENT.org Normal file
View File

@ -0,0 +1,412 @@
#+TITLE: Asteroid Radio - Development Guide
#+AUTHOR: Development Team
#+DATE: 2025-10-03
* Development Setup
** Prerequisites
*** System Dependencies
- SBCL (Steel Bank Common Lisp)
- Quicklisp package manager
- Git version control
- Docker and Docker Compose
- taglib for metadata extraction (for local development)
*** Ubuntu/Debian Installation
#+BEGIN_SRC bash
# Install system packages
sudo apt update
sudo apt install sbcl git docker.io docker compose
# Add user to docker group
sudo usermod -a -G docker $USER
# Log out and back in for group changes to take effect
# Install Quicklisp (if not already installed)
curl -O https://beta.quicklisp.org/quicklisp.lisp
sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --quit
#+END_SRC
** Project Setup
*** Clone Repository
#+BEGIN_SRC bash
git clone <repository-url>
cd asteroid
#+END_SRC
*** Install Lisp Dependencies
#+BEGIN_SRC bash
# Start SBCL and load the system
sbcl
(ql:quickload :asteroid)
#+END_SRC
*** ASDF Configuration (Optional but Recommended)
For easier development, configure ASDF to find the asteroid system:
#+BEGIN_SRC bash
# Create ASDF source registry configuration
mkdir -p ~/.config/common-lisp
cat > ~/.config/common-lisp/source-registry.conf
;; -*-lisp-*-
(:source-registry
(:tree "/path/to/your/projects/")
:inherit-configuration)
#+END_SRC
This allows you to load the asteroid system from any directory without changing paths.
* Development Workflow
** Local Development Server
*** Starting Development Environment
#+BEGIN_SRC bash
# Start Docker streaming services
cd docker/
docker compose up -d
# Verify containers are running
docker compose ps
# View logs
docker compose logs -f
# Start RADIANCE web server (local development)
sbcl --eval "(ql:quickload :asteroid)" --eval "(asteroid:start-server)"
#+END_SRC
*** Development URLs
- *Web Interface*: http://localhost:8080/asteroid/
- *Admin Panel*: http://localhost:8080/asteroid/admin
- *Live Stream*: http://localhost:8000/asteroid.mp3
- *Icecast Admin*: http://localhost:8000/admin/ (admin/asteroid_admin_2024)
** Music Library Management
*** Directory Structure
The music directory structure is:
#+BEGIN_SRC
asteroid/docker/music/ # Host directory (mounted to containers)
├── artist1/
│ ├── album1/
│ │ ├── track1.mp3
│ │ └── track2.flac
│ └── album2/
│ └── track3.ogg
└── artist2/
└── single.wav
#+END_SRC
*** Recursive Scanning Capabilities
The Asteroid application includes built-in recursive directory scanning:
- *Function*: =scan-music-library= in =stream-media.lisp=
- *Supports*: MP3, FLAC, OGG, WAV formats
- *Recursive*: Automatically scans all subdirectories
- *Metadata*: Extracts title, artist, album, duration using taglib
- *Database*: Stores track information in RADIANCE database
*** Adding Music to Development Environment
#+BEGIN_SRC bash
# Option 1: Copy music files directly
cp -r /path/to/your/music/* docker/music/
# Option 2: Mount remote directory (for large collections)
# Edit docker-compose.yml to change volume mount:
# volumes:
# - /mnt/remote-music:/app/music:ro
# Option 3: Symlink to existing collection
ln -s /path/to/existing/music docker/music/collection
# Trigger library scan via API
curl -X POST http://localhost:8080/asteroid/api/scan-library
#+END_SRC
** Code Organization
*** Main Components
- =asteroid.lisp= - Main server with RADIANCE routes and API endpoints
- =asteroid.asd= - System definition with dependencies
- =template/= - CLIP HTML templates for web interface
- =static/= - CSS stylesheets and static assets
- =asteroid-radio.liq= - Liquidsoap streaming configuration
*** Key Modules
- *Web Routes*: RADIANCE framework with =#@= URL patterns
- *Database*: RADIANCE DB abstraction for track metadata
- *Streaming*: Docker containers with Icecast2 and Liquidsoap
- *File Processing*: Metadata extraction and library management
- *Docker Integration*: Containerized streaming infrastructure
** Development Practices
*** Code Style
- Use 2-space indentation for Lisp code
- Follow Common Lisp naming conventions
- Document functions with docstrings
- Use meaningful variable and function names
*** Database Development
#+BEGIN_SRC lisp
;; Always use quoted symbols for field names
(db:select 'tracks (db:query (:= 'artist "Artist Name")))
;; Primary key is "_id" internally, "id" in JSON responses
(gethash "_id" track-record)
#+END_SRC
*** Template Development
- Use CLIP templating with =data-text= attributes
- Keep templates in =template/= directory
- Test template changes with browser refresh
- Maintain responsive design principles
*** CSS Development with LASS
- CSS is generated dynamically from =static/asteroid.lass= using LASS (Lisp Augmented Style Sheets)
- Edit the =.lass= file, not the generated =.css= file
- CSS is automatically compiled when the server starts via =compile-styles= function
- Use Lisp syntax for CSS: =(body :background "#0a0a0a" :color "#00ffff")=
- Supports nested selectors, variables, and programmatic CSS generation
** Testing
*** Manual Testing Checklist
- [ ] Web interface loads correctly
- [ ] Admin panel functions work
- [ ] File upload and processing works
- [ ] Live stream plays audio
- [ ] Database queries return expected results
- [ ] API endpoints respond correctly
*** Docker Container Testing
#+BEGIN_SRC bash
# Check container status
docker compose ps
# Test stream connectivity
curl -I http://localhost:8000/asteroid.mp3
# Test with media player
vlc http://localhost:8000/asteroid.mp3
# Check container logs
docker compose logs icecast
docker compose logs liquidsoap
#+END_SRC
*** API Testing
#+BEGIN_SRC bash
# Test track listing
curl http://localhost:8080/asteroid/api/tracks
# Test file processing
curl -X POST http://localhost:8080/asteroid/api/copy-files
#+END_SRC
** Debugging
*** Common Development Issues
**** Stream Not Playing
- Check Docker container status: =docker compose ps=
- Check Liquidsoap container logs: =docker compose logs liquidsoap=
- Check Icecast2 container logs: =docker compose logs icecast=
- Verify music files exist in =docker/music/library/=
- Restart containers: =docker compose restart=
**** Database Errors
- Ensure proper field name quoting in queries
- Check RADIANCE database configuration
- Verify database file permissions
**** Template Rendering Issues
- Check CLIP template syntax
- Verify template file paths
- Test with simplified templates first
*** Debug Configuration
#+BEGIN_SRC bash
# Enable verbose logging in Docker containers
# Edit docker/liquidsoap/asteroid-radio.liq
settings.log.level := 4
settings.log.stdout := true
settings.log.file := true
settings.log.file.path := "/var/log/liquidsoap/asteroid.log"
# View real-time container logs
docker compose logs -f liquidsoap
docker compose logs -f icecast
#+END_SRC
** Contributing Guidelines
*** Branch Strategy
- =main= - Stable production code
- =develop= - Integration branch for new features
- =feature/*= - Individual feature development
- =bugfix/*= - Bug fixes and patches
*** Commit Messages
- Use clear, descriptive commit messages
- Reference issue numbers when applicable
- Keep commits focused on single changes
*** Pull Request Process
1. Create feature branch from =develop=
2. Implement changes with tests
3. Update documentation if needed
4. Submit pull request with description
5. Address code review feedback
6. Merge after approval
*** Code Review Checklist
- [ ] Code follows project style guidelines
- [ ] Functions are properly documented
- [ ] No hardcoded values or credentials
- [ ] Error handling is appropriate
- [ ] Performance considerations addressed
** Development Tools
*** Recommended Editor Setup
- *Emacs*: SLIME for interactive Lisp development
*** Useful Development Commands
#+BEGIN_SRC lisp
;; Reload system during development
(ql:quickload :asteroid :force t)
;; Restart RADIANCE server
(radiance:shutdown)
(asteroid:start-server)
;; Clear database for testing
(db:drop 'tracks)
(asteroid:setup-database)
#+END_SRC
** Performance Considerations
*** Development vs Production
- Use smaller music libraries in =docker/music/= for faster testing
- Enable debug logging in Docker containers only when needed
- Consider memory usage with large track collections in containers
- Test with realistic concurrent user loads using Docker scaling
- Use =docker compose.dev.yml= for development-specific settings
*** Optimization Tips
- Cache database queries where appropriate
- Optimize playlist generation for large libraries
- Monitor memory usage during development
- Profile streaming performance under load
* Configuration Files
- =radiance-core.conf.lisp= - RADIANCE framework configuration
- =docker/liquidsoap/asteroid-radio.liq= - Liquidsoap streaming setup
- =docker/icecast.xml= - Icecast2 server configuration
- =docker/docker-compose.yml= - Container orchestration
** Docker Development
#+BEGIN_SRC bash
# Start development containers
cd docker/
docker compose up -d
# Build development container with changes
docker compose up --build
# Access container shell for debugging
docker compose exec liquidsoap bash
docker compose exec icecast bash
# Stop all containers
docker compose down
#+END_SRC
* Troubleshooting
** Development Environment Issues
*** SBCL/Quicklisp Problems
- Ensure Quicklisp is properly installed
- Check for conflicting Lisp installations
- Verify system dependencies are installed
*** Docker Container Issues
- Check container status: =docker compose ps=
- Verify Docker daemon is running: =docker info=
- Check container logs: =docker compose logs [service]=
- Restart containers: =docker compose restart=
*** Network Access Issues
- Check firewall settings for ports 8000, 8080
- Verify WSL networking configuration if applicable
- Test container networking: =docker compose exec liquidsoap ping icecast=
- Check port binding: =docker compose port icecast 8000=
*** File Permission Issues
- Ensure =docker/music/= directory is accessible
- Check ownership: =ls -la docker/music/=
- Fix permissions: =sudo chown -R $USER:$USER docker/music/=
- Verify container volume mounts in =docker-compose.yml=
- For remote mounts: ensure network storage is accessible
*** Music Library Issues
- Check if music files exist: =find docker/music/ -name "*.mp3" -o -name "*.flac"=
- Verify supported formats: MP3, FLAC, OGG, WAV
- Test recursive scanning: =curl -X POST http://localhost:8080/asteroid/api/scan-library=
- Check database for tracks: =curl http://localhost:8080/asteroid/api/tracks=
- For large collections: avoid network mounts, use local storage (see memory about 175+ files causing timeouts)
** Getting Help
- Check existing issues in project repository
- Review RADIANCE framework documentation
- Consult Liquidsoap manual for streaming issues
- Join our IRC chat room: **#asteroid.music** on **irc.libera.chat**
- Ask questions in project discussions
This development guide provides the foundation for contributing to Asteroid Radio. For deployment and production considerations, see the Installation Guide and Performance Testing documentation.
* Development Stack Links
** Core Technologies
- **SBCL** (Steel Bank Common Lisp): https://www.sbcl.org/
- **Quicklisp** (Common Lisp package manager): https://www.quicklisp.org/
- **ASDF** (Another System Definition Facility): https://common-lisp.net/project/asdf/
** Web Framework & Libraries
- **RADIANCE** (Web framework): https://shirakumo.github.io/radiance/
- **CLIP** (HTML templating): https://shinmera.github.io/clip/
- **LASS** (CSS in Lisp): https://shinmera.github.io/LASS/
- **Alexandria** (Utility library): https://alexandria.common-lisp.dev/
- **Local-Time** (Time handling): https://common-lisp.net/project/local-time/
** Audio & Streaming
- **Docker** (Containerization): https://www.docker.com/
- **Icecast2** (Streaming server): https://icecast.org/
- **Liquidsoap** (Audio streaming): https://www.liquidsoap.info/
- **TagLib** (Audio metadata): https://taglib.org/
** Database & Data
- **cl-json** (JSON handling): https://common-lisp.net/project/cl-json/
- **cl-fad** (File/directory utilities): https://edicl.github.io/cl-fad/
- **Ironclad** (Cryptography): https://github.com/sharplispers/ironclad
- **Babel** (Character encoding): https://common-lisp.net/project/babel/
** Development Tools
- **Emacs** (Editor): https://www.gnu.org/software/emacs/
- **SLIME** (Emacs Lisp IDE): https://common-lisp.net/project/slime/
- **Slynk** (SLIME backend): https://github.com/joaotavora/sly
- **Git** (Version control): https://git-scm.com/
** System Libraries
- **Bordeaux-Threads** (Threading): https://common-lisp.net/project/bordeaux-threads/
- **Drakma** (HTTP client): https://edicl.github.io/drakma/
- **CIFS-Utils** (Network file systems): https://wiki.samba.org/index.php/LinuxCIFS_utils
** Documentation & Standards
- **Common Lisp HyperSpec**: http://www.lispworks.com/documentation/HyperSpec/Front/
- **Docker Compose**: https://docs.docker.com/compose/
- **Org Mode** (Documentation format): https://orgmode.org/

614
docs/DOCKER-STREAMING.org Normal file
View File

@ -0,0 +1,614 @@
#+TITLE: Asteroid Radio - Docker Streaming Setup
#+AUTHOR: Docker Team
#+DATE: 2025-10-03
* Docker Streaming Overview
This guide covers the complete Docker-based streaming setup for Asteroid Radio using Icecast2 and Liquidsoap containers. This approach provides a containerized, portable streaming infrastructure that's easy to deploy and maintain.
* Architecture
** Container Stack
- *Icecast2 Container*: Streaming server handling client connections
- *Liquidsoap Container*: Audio processing and stream generation
- *Shared Volumes*: Music library and configuration sharing
** Stream Formats
- *High Quality MP3*: 128kbps MP3 stream at /asteroid.mp3
- *High Quality AAC*: 96kbps AAC stream at /asteroid.aac (better efficiency than MP3)
- *Low Quality MP3*: 64kbps MP3 stream at /asteroid-low.mp3 (compatibility)
** Network Configuration
- *Icecast2*: Port 8000 (streaming and admin)
- *Liquidsoap Telnet*: Port 1234 (remote control)
- *Internal Network*: Container-to-container communication
* Quick Start
** Prerequisites
#+BEGIN_SRC bash
# Install Docker and Docker Compose
sudo apt update
sudo apt install docker.io docker compose
sudo usermod -a -G docker $USER
# Log out and back in for group changes
#+END_SRC
** One-Command Setup
#+BEGIN_SRC bash
# Clone and start
git clone <repository-url> asteroid-radio
cd asteroid-radio/docker
docker compose up -d
#+END_SRC
** Verify Setup
#+BEGIN_SRC bash
# Check container status
docker compose ps
# Test streaming (all three formats)
curl -I http://localhost:8000/asteroid.mp3 # 128kbps MP3
curl -I http://localhost:8000/asteroid.aac # 96kbps AAC
curl -I http://localhost:8000/asteroid-low.mp3 # 64kbps MP3
#+END_SRC
* Docker Compose Configuration
** Complete docker-compose.yml
#+BEGIN_SRC yaml
version: '3.8'
services:
icecast:
image: infiniteproject/icecast:latest
container_name: asteroid-icecast
ports:
- "8000:8000"
volumes:
- ./icecast.xml:/etc/icecast.xml
environment:
- ICECAST_SOURCE_PASSWORD=H1tn31EhsyLrfRmo
- ICECAST_ADMIN_PASSWORD=asteroid_admin_2024
- ICECAST_RELAY_PASSWORD=asteroid_relay_2024
restart: unless-stopped
networks:
- asteroid-network
liquidsoap:
build:
context: .
dockerfile: Dockerfile.liquidsoap
container_name: asteroid-liquidsoap
ports:
- "1234:1234" # Telnet control port
depends_on:
- icecast
volumes:
- ./music:/app/music:ro
- ./asteroid-radio-docker.liq:/app/asteroid-radio.liq:ro
restart: unless-stopped
networks:
- asteroid-network
networks:
asteroid-network:
driver: bridge
#+END_SRC
* Container Configurations
** Icecast2 Container Setup
*** Custom Icecast Configuration (icecast.xml)
#+BEGIN_SRC xml
<icecast>
<location>Asteroid Radio Docker</location>
<admin>admin@asteroid-radio.docker</admin>
<limits>
<clients>100</clients>
<sources>10</sources>
<queue-size>524288</queue-size>
<client-timeout>30</client-timeout>
<header-timeout>15</header-timeout>
<source-timeout>10</source-timeout>
<burst-on-connect>1</burst-on-connect>
</limits>
<authentication>
<source-password>H1tn31EhsyLrfRmo</source-password>
<relay-password>asteroid_relay_2024</relay-password>
<admin-user>admin</admin-user>
<admin-password>asteroid_admin_2024</admin-password>
</authentication>
<hostname>icecast</hostname>
<listen-socket>
<port>8000</port>
<bind-address>0.0.0.0</bind-address>
</listen-socket>
<!-- High Quality Stream -->
<mount type="normal">
<mount-name>/asteroid.mp3</mount-name>
<username>source</username>
<password>H1tn31EhsyLrfRmo</password>
<max-listeners>50</max-listeners>
<public>1</public>
<stream-name>Asteroid Radio - High Quality</stream-name>
<stream-url>http://localhost:8080/asteroid/</stream-url>
<genre>Electronic/Alternative</genre>
<bitrate>128</bitrate>
</mount>
<!-- AAC High Quality Stream -->
<mount type="normal">
<mount-name>/asteroid.aac</mount-name>
<username>source</username>
<password>H1tn31EhsyLrfRmo</password>
<max-listeners>50</max-listeners>
<public>1</public>
<stream-name>Asteroid Radio - AAC</stream-name>
<stream-description>Music for Hackers - 96kbps AAC</stream-description>
<stream-url>http://localhost:8080/asteroid/</stream-url>
<genre>Electronic/Alternative</genre>
<bitrate>96</bitrate>
</mount>
<!-- Low Quality Stream -->
<mount type="normal">
<mount-name>/asteroid-low.mp3</mount-name>
<username>source</username>
<password>H1tn31EhsyLrfRmo</password>
<max-listeners>100</max-listeners>
<public>1</public>
<stream-name>Asteroid Radio - Low Quality</stream-name>
<stream-description>Music for Hackers - 64kbps</stream-description>
<stream-url>http://localhost:8080/asteroid/</stream-url>
<genre>Electronic/Alternative</genre>
<bitrate>64</bitrate>
</mount>
<fileserve>1</fileserve>
<paths>
<basedir>/usr/share/icecast2</basedir>
<logdir>/var/log/icecast2</logdir>
<webroot>/usr/share/icecast2/web</webroot>
<adminroot>/usr/share/icecast2/admin</adminroot>
<alias source="/" destination="/status.xsl"/>
</paths>
<logging>
<accesslog>access.log</accesslog>
<errorlog>error.log</errorlog>
<loglevel>3</loglevel>
<logsize>10000</logsize>
</logging>
</icecast>
#+END_SRC
** Liquidsoap Container Setup
*** Liquidsoap Configuration (asteroid-radio-docker.liq)
#+BEGIN_SRC liquidsoap
#!/usr/bin/liquidsoap
# Asteroid Radio - Docker streaming script
# Streams music library continuously to Icecast2 running in Docker
# Allow running as root in Docker
set("init.allow_root", true)
# Set log level for debugging
log.level.set(4)
# Enable telnet server for remote control
settings.server.telnet.set(true)
settings.server.telnet.port.set(1234)
settings.server.telnet.bind_addr.set("0.0.0.0")
# Create playlist source from mounted music directory
radio = playlist(
mode="randomize",
reload=3600,
reload_mode="watch",
"/app/music/"
)
# Add some audio processing
radio = amplify(1.0, radio)
radio = normalize(radio)
# Add crossfade between tracks
radio = crossfade(radio)
# Create a fallback with emergency content
emergency = sine(440.0)
emergency = amplify(0.1, emergency)
# Make source safe with fallback
radio = fallback(track_sensitive=false, [radio, emergency])
# Add metadata
radio = map_metadata(fun(m) ->
[("title", m["title"] ?? "Unknown Track"),
("artist", m["artist"] ?? "Unknown Artist"),
("album", m["album"] ?? "Unknown Album")], radio)
# High Quality MP3 Stream (128kbps)
output.icecast(
%mp3(bitrate=128),
host="icecast", # Docker service name
port=8000,
password="H1tn31EhsyLrfRmo",
mount="asteroid.mp3",
name="Asteroid Radio",
description="Music for Hackers - Streaming from the Asteroid",
genre="Electronic/Alternative",
url="http://localhost:8080/asteroid/",
public=true,
radio
)
# AAC High Quality Stream (96kbps - better quality than 128kbps MP3)
output.icecast(
%fdkaac(bitrate=96),
host="icecast",
port=8000,
password="H1tn31EhsyLrfRmo",
mount="asteroid.aac",
name="Asteroid Radio (AAC)",
description="Music for Hackers - High efficiency AAC stream",
genre="Electronic/Alternative",
url="http://localhost:8080/asteroid/",
public=true,
radio
)
# Low Quality MP3 Stream (for compatibility)
output.icecast(
%mp3(bitrate=64),
host="icecast",
port=8000,
password="H1tn31EhsyLrfRmo",
mount="asteroid-low.mp3",
name="Asteroid Radio (Low Quality)",
description="Music for Hackers - Low bandwidth stream",
genre="Electronic/Alternative",
url="http://localhost:8080/asteroid/",
public=true,
radio
)
print("🎵 Asteroid Radio Docker streaming started!")
print("High Quality MP3: http://localhost:8000/asteroid.mp3")
print("High Quality AAC: http://localhost:8000/asteroid.aac")
print("Low Quality MP3: http://localhost:8000/asteroid-low.mp3")
print("Icecast Admin: http://localhost:8000/admin/")
print("Telnet control: telnet localhost 1234")
#+END_SRC
* Management Scripts
** Start Script (start-streaming.sh)
#+BEGIN_SRC bash
#!/bin/bash
# Asteroid Radio Docker Streaming Startup Script
set -e
echo "🚀 Starting Asteroid Radio Docker Streaming..."
# Check if Docker is running
if ! docker info > /dev/null 2>&1; then
echo "❌ Docker is not running. Please start Docker first."
exit 1
fi
# Create required directories
mkdir -p music/incoming music/library logs
# Set permissions
chmod 755 music/incoming music/library
chmod 777 logs
# Pull latest images
echo "📦 Pulling latest Docker images..."
docker compose pull
# Start services
echo "🎵 Starting streaming services..."
docker compose up -d
# Wait for services to be ready
echo "⏳ Waiting for services to start..."
sleep 10
# Check service status
echo "📊 Checking service status..."
docker compose ps
# Test connectivity
echo "🔍 Testing streaming connectivity..."
if curl -s -I http://localhost:8000/asteroid.mp3 | grep -q "200 OK"; then
echo "✅ High quality stream is working"
else
echo "⚠️ High quality stream may not be ready yet"
fi
if curl -s -I http://localhost:8000/asteroid-low.mp3 | grep -q "200 OK"; then
echo "✅ Low quality MP3 stream is working"
else
echo "⚠️ Low quality MP3 stream may not be ready yet"
fi
if curl -s -I http://localhost:8000/asteroid.aac | grep -q "200 OK"; then
echo "✅ AAC stream is working"
else
echo "⚠️ AAC stream may not be ready yet"
fi
echo ""
echo "🎉 Asteroid Radio Docker setup complete!"
echo ""
echo "📻 Stream URLs:"
echo " High Quality MP3: http://localhost:8000/asteroid.mp3 (128kbps)"
echo " High Quality AAC: http://localhost:8000/asteroid.aac (96kbps)"
echo " Low Quality MP3: http://localhost:8000/asteroid-low.mp3 (64kbps)"
echo ""
echo "🔧 Admin Interfaces:"
echo " Icecast: http://localhost:8000/admin/ (admin/asteroid_admin_2024)"
echo " Telnet: telnet localhost 1234"
echo ""
echo "📁 Add music files to: ./music/"
echo " Files are automatically detected and streamed."
#+END_SRC
** Stop Script (stop-streaming.sh)
#+BEGIN_SRC bash
#!/bin/bash
# Asteroid Radio Docker Streaming Stop Script
echo "🛑 Stopping Asteroid Radio Docker Streaming..."
# Stop all services
docker compose down
# Optional: Remove volumes (uncomment to clean up completely)
# docker compose down -v
echo "✅ All services stopped."
#+END_SRC
** Test Script (test-streaming.sh)
#+BEGIN_SRC bash
#!/bin/bash
# Asteroid Radio Docker Streaming Test Script
echo "🧪 Testing Asteroid Radio Docker Setup..."
# Test container status
echo "📊 Container Status:"
docker compose ps
echo ""
echo "🔍 Testing Connectivity:"
# Test Icecast2
if curl -s -I http://localhost:8000/ | grep -q "200 OK"; then
echo "✅ Icecast2 server is responding"
else
echo "❌ Icecast2 server is not responding"
fi
# Test high quality stream
if curl -s -I http://localhost:8000/asteroid.mp3 | grep -q "200 OK"; then
echo "✅ High quality stream is available"
else
echo "❌ High quality stream is not available"
fi
# Test low quality stream
if curl -s -I http://localhost:8000/asteroid-low.mp3 | grep -q "200 OK"; then
echo "✅ Low quality MP3 stream is available"
else
echo "❌ Low quality MP3 stream is not available"
fi
# Test AAC stream
if curl -s -I http://localhost:8000/asteroid.aac | grep -q "200 OK"; then
echo "✅ AAC stream is available"
else
echo "❌ AAC stream is not available"
fi
echo ""
echo "📋 Service Logs (last 10 lines):"
echo "--- Icecast2 ---"
docker compose logs --tail=10 icecast
echo "--- Liquidsoap ---"
docker compose logs --tail=10 liquidsoap
#+END_SRC
* Volume Management
** Music Library Setup
#+BEGIN_SRC bash
# Music directory already exists in repository
# Copy sample music directly to the music directory
cp ~/path/to/music/*.mp3 docker/music/
# Set permissions
chmod 755 docker/music/
sudo chown -R $USER:$USER docker/music/
#+END_SRC
** Persistent Data
- *Music Library*: =./music/= - Mounted as volume
- *Logs*: =./logs/= - Container logs and streaming logs
- *Configuration*: =./liquidsoap/= and =./icecast.xml= - Read-only configs
* Networking
** Internal Container Network
- Containers communicate via =asteroid-network= bridge
- Liquidsoap connects to Icecast using hostname =icecast=
- Telnet control available on port 1234 for Liquidsoap management
** External Access
- *Port 8000*: Icecast2 streaming and admin interface
- *Port 1234*: Liquidsoap telnet control interface
- All services bind to =0.0.0.0= for external access
** WSL Compatibility
#+BEGIN_SRC bash
# Find WSL IP for external access
ip addr show eth0 | grep inet
# Access from Windows host
# http://[IP-ADDRESS]:8000/asteroid.mp3 # 128kbps MP3
# http://[IP-ADDRESS]:8000/asteroid.aac # 96kbps AAC
# http://[IP-ADDRESS]:8000/asteroid-low.mp3 # 64kbps MP3
#+END_SRC
* Production Deployment
** Docker Swarm Setup
#+BEGIN_SRC yaml
# docker compose.prod.yml
version: '3.8'
services:
icecast:
image: moul/icecast
deploy:
replicas: 1
restart_policy:
condition: on-failure
# ... rest of configuration
liquidsoap:
image: savonet/liquidsoap:v2.2.x
deploy:
replicas: 1
restart_policy:
condition: on-failure
# ... rest of configuration
#+END_SRC
** Environment Variables
#+BEGIN_SRC bash
# Production environment
export ASTEROID_ENV=production
export ASTEROID_STREAM_QUALITY=high
export ASTEROID_MAX_LISTENERS=200
export ICECAST_ADMIN_PASSWORD=secure_password_here
#+END_SRC
** SSL/TLS Setup
Use reverse proxy (nginx/traefik) for HTTPS termination:
#+BEGIN_SRC yaml
# Add to docker-compose.yml
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/ssl:ro
#+END_SRC
* Monitoring and Logging
** Container Health Checks
#+BEGIN_SRC bash
# Check container health
docker compose exec icecast curl -f http://localhost:8000/status.xsl
docker compose exec liquidsoap ps aux | grep liquidsoap
# Test telnet control interface
echo "help" | nc localhost 1234
#+END_SRC
** Log Management
#+BEGIN_SRC bash
# View real-time logs
docker compose logs -f
# View specific service logs
docker compose logs -f icecast
docker compose logs -f liquidsoap
# Log rotation setup
docker run --log-driver=json-file --log-opt max-size=10m --log-opt max-file=3
#+END_SRC
* Troubleshooting
** Common Docker Issues
*** Container Won't Start
#+BEGIN_SRC bash
# Check container logs
docker compose logs [service-name]
# Check resource usage
docker stats
# Verify configuration files
docker compose config
#+END_SRC
*** Streaming Issues
#+BEGIN_SRC bash
# Test internal connectivity
docker compose exec liquidsoap ping icecast
# Check Liquidsoap connection and logs
docker compose logs liquidsoap
# Test telnet interface
echo "request.queue" | nc localhost 1234
#+END_SRC
*** Permission Issues
#+BEGIN_SRC bash
# Fix music directory permissions
sudo chown -R $USER:$USER docker/music/
chmod 755 docker/music/
#+END_SRC
** Performance Tuning
*** Resource Limits
#+BEGIN_SRC yaml
# Add to services in docker-compose.yml
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
#+END_SRC
*** Network Optimization
#+BEGIN_SRC yaml
# Optimize network settings
networks:
asteroid-network:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: 1500
#+END_SRC
This Docker streaming setup provides a complete containerized solution for Asteroid Radio with professional streaming capabilities and easy deployment.

561
docs/INSTALLATION.org Normal file
View File

@ -0,0 +1,561 @@
#+TITLE: Asteroid Radio - Installation Guide
#+AUTHOR: Installation Team
#+DATE: 2025-10-03
* Installation Overview
This guide covers the complete installation and deployment of Asteroid Radio. The **recommended approach** is Docker-based installation for easy deployment and consistency. Native installation is also available for development or custom deployments.
* Quick Start (Docker - Recommended)
** Prerequisites Check
#+BEGIN_SRC bash
# Check if Docker is installed and running
docker --version
docker compose version
docker info
#+END_SRC
** One-Command Setup
#+BEGIN_SRC bash
# Clone and setup (replace with actual repository URL)
git clone <repository-url> asteroid-radio
cd asteroid-radio/docker
docker compose up -d
#+END_SRC
** Verify Installation
#+BEGIN_SRC bash
# Check all three streams are working
curl -I http://localhost:8000/asteroid.mp3 # 128kbps MP3
curl -I http://localhost:8000/asteroid.aac # 96kbps AAC
curl -I http://localhost:8000/asteroid-low.mp3 # 64kbps MP3
#+END_SRC
* Detailed Installation
** System Requirements
*** Docker Installation Requirements
- *OS*: Any OS with Docker support (Linux, macOS, Windows)
- *Docker*: Docker Engine 20.10+ and Docker Compose 2.0+
- *RAM*: 2GB minimum, 4GB recommended
- *Storage*: 20GB minimum, 500GB+ for music library
- *CPU*: 2 cores minimum, 4+ cores recommended
- *Network*: Stable internet connection for streaming
*** Native Installation Requirements (Advanced)
- *OS*: Ubuntu 20.04+ / Debian 11+ (for native installation)
- *RAM*: 1GB minimum, 2GB recommended
- *Storage*: 10GB minimum, 100GB+ for music library
- *CPU*: 1 core minimum, 2+ cores recommended
- *Dependencies*: SBCL, Icecast2, Liquidsoap, TagLib
** Docker Installation (Recommended)
*** Step 1: Install Docker
#+BEGIN_SRC bash
# Ubuntu/Debian
sudo apt update
sudo apt install -y docker.io docker compose
sudo usermod -a -G docker $USER
# Log out and back in for group changes
# CentOS/RHEL
sudo dnf install -y docker docker compose
sudo systemctl enable --now docker
sudo usermod -a -G docker $USER
# macOS
brew install docker docker compose
# Or install Docker Desktop
# Windows
# Install Docker Desktop from docker.com
#+END_SRC
*** Step 2: Clone and Setup
#+BEGIN_SRC bash
# Clone repository
git clone <repository-url> asteroid-radio
cd asteroid-radio/docker
# Start services
docker compose up -d
# Check status
docker compose ps
#+END_SRC
*** Step 3: Add Music
#+BEGIN_SRC bash
# Copy music files to the docker music directory
cp ~/path/to/music/*.mp3 music/
cp ~/path/to/music/*.flac music/
# Set proper permissions
sudo chown -R $USER:$USER music/
#+END_SRC
*** Step 4: Access Streams
- **High Quality MP3**: http://localhost:8000/asteroid.mp3 (128kbps)
- **High Quality AAC**: http://localhost:8000/asteroid.aac (96kbps)
- **Low Quality MP3**: http://localhost:8000/asteroid-low.mp3 (64kbps)
- **Icecast Admin**: http://localhost:8000/admin/ (admin/asteroid_admin_2024)
- **Telnet Control**: =telnet localhost 1234=
** Native Installation (Advanced Users)
*** Step 1: System Updates
#+BEGIN_SRC bash
sudo apt update && sudo apt upgrade -y
#+END_SRC
*** Step 2: Install System Dependencies
#+BEGIN_SRC bash
# Core dependencies
sudo apt install -y sbcl git curl wget build-essential
# Streaming dependencies
sudo apt install -y icecast2 liquidsoap
# Audio processing dependencies
sudo apt install -y libtag1-dev libtagc0-dev
# Optional: Development tools
sudo apt install -y emacs vim htop tree
#+END_SRC
*** Step 3: Configure Icecast2
#+BEGIN_SRC bash
# Configure Icecast2 during installation
sudo dpkg-reconfigure icecast2
# Or manually edit configuration
sudo nano /etc/icecast2/icecast.xml
#+END_SRC
*Icecast2 Configuration*:
#+BEGIN_SRC xml
<icecast>
<location>Asteroid Radio Station</location>
<admin>admin@asteroid-radio.local</admin>
<limits>
<clients>100</clients>
<sources>2</sources>
<queue-size>524288</queue-size>
<client-timeout>30</client-timeout>
<header-timeout>15</header-timeout>
<source-timeout>10</source-timeout>
</limits>
<authentication>
<source-password>b3l0wz3r0</source-password>
<relay-password>asteroid_relay_2024</relay-password>
<admin-user>admin</admin-user>
<admin-password>asteroid_admin_2024</admin-password>
</authentication>
<hostname>localhost</hostname>
<listen-socket>
<port>8000</port>
</listen-socket>
<mount type="normal">
<mount-name>/asteroid.mp3</mount-name>
<username>source</username>
<password>b3l0wz3r0</password>
<max-listeners>50</max-listeners>
<dump-file>/var/log/icecast2/asteroid.dump</dump-file>
<burst-on-connect>1</burst-on-connect>
<fallback-mount>/silence.mp3</fallback-mount>
<fallback-override>1</fallback-override>
</mount>
<fileserve>1</fileserve>
<paths>
<basedir>/usr/share/icecast2</basedir>
<logdir>/var/log/icecast2</logdir>
<webroot>/usr/share/icecast2/web</webroot>
<adminroot>/usr/share/icecast2/admin</adminroot>
<alias source="/" destination="/status.xsl"/>
</paths>
<logging>
<accesslog>access.log</accesslog>
<errorlog>error.log</errorlog>
<loglevel>3</loglevel>
<logsize>10000</logsize>
</logging>
</icecast>
#+END_SRC
*** Step 4: Install Quicklisp
#+BEGIN_SRC bash
# Download and install Quicklisp
cd /tmp
curl -O https://beta.quicklisp.org/quicklisp.lisp
sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --eval "(ql:add-to-init-file)" --quit
#+END_SRC
*** Step 5: Clone and Setup Project
#+BEGIN_SRC bash
# Clone repository (replace with actual URL)
git clone <repository-url> /opt/asteroid-radio
cd /opt/asteroid-radio
# Create required directories
sudo mkdir -p music/incoming music/library static template
sudo chown -R $USER:$USER music/
# Set permissions
chmod 755 music/incoming music/library
chmod +x *.sh
#+END_SRC
*** Step 6: Install Lisp Dependencies
#+BEGIN_SRC bash
# Start SBCL and install dependencies
sbcl --eval "(ql:quickload :asteroid)" --quit
#+END_SRC
** CentOS/RHEL Installation
*** Step 1: Enable EPEL Repository
#+BEGIN_SRC bash
sudo dnf install -y epel-release
sudo dnf update -y
#+END_SRC
*** Step 2: Install Dependencies
#+BEGIN_SRC bash
# Core dependencies
sudo dnf install -y sbcl git curl wget gcc make
# Streaming dependencies (may require additional repositories)
sudo dnf install -y icecast liquidsoap
# Audio processing
sudo dnf install -y taglib-devel
#+END_SRC
*** Step 3: Follow Ubuntu Steps 3-6
The remaining steps are similar to Ubuntu installation.
** macOS Installation (Development Only)
*** Step 1: Install Homebrew
#+BEGIN_SRC bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
#+END_SRC
*** Step 2: Install Dependencies
#+BEGIN_SRC bash
# Core dependencies
brew install sbcl git
# Streaming dependencies
brew install icecast2 liquidsoap
# Audio processing
brew install taglib
#+END_SRC
*** Step 3: Follow Similar Setup Steps
Adapt the Linux steps for macOS paths and conventions.
* Service Configuration
** Systemd Service Setup (Linux)
*** Icecast2 Service
#+BEGIN_SRC bash
# Enable and start Icecast2
sudo systemctl enable icecast2
sudo systemctl start icecast2
sudo systemctl status icecast2
#+END_SRC
*** Asteroid Radio Service
Create systemd service file:
#+BEGIN_SRC bash
sudo nano /etc/systemd/system/asteroid-radio.service
#+END_SRC
*Service Configuration*:
#+BEGIN_SRC ini
[Unit]
Description=Asteroid Radio Streaming Service
After=network.target icecast2.service
Requires=icecast2.service
[Service]
Type=forking
User=asteroid
Group=asteroid
WorkingDirectory=/opt/asteroid-radio
ExecStart=/opt/asteroid-radio/start-asteroid-radio.sh
ExecStop=/opt/asteroid-radio/stop-asteroid-radio.sh
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
#+END_SRC
*** Enable and Start Service
#+BEGIN_SRC bash
# Create service user
sudo useradd -r -s /bin/false asteroid
sudo chown -R asteroid:asteroid /opt/asteroid-radio
# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable asteroid-radio
sudo systemctl start asteroid-radio
sudo systemctl status asteroid-radio
#+END_SRC
* Network Configuration
** Firewall Setup
*** Ubuntu/Debian (ufw)
#+BEGIN_SRC bash
# Allow required ports
sudo ufw allow 8000/tcp # Icecast2 streaming and admin
sudo ufw allow 1234/tcp # Liquidsoap telnet control (optional)
sudo ufw enable
#+END_SRC
*** CentOS/RHEL (firewalld)
#+BEGIN_SRC bash
# Allow required ports
sudo firewall-cmd --permanent --add-port=8000/tcp # Icecast2
sudo firewall-cmd --permanent --add-port=1234/tcp # Liquidsoap telnet (optional)
sudo firewall-cmd --reload
#+END_SRC
** Reverse Proxy Setup (Optional)
*** Nginx Configuration
#+BEGIN_SRC bash
# Install Nginx
sudo apt install nginx
# Create configuration
sudo nano /etc/nginx/sites-available/asteroid-radio
#+END_SRC
*Nginx Configuration*:
#+BEGIN_SRC nginx
server {
listen 80;
server_name your-domain.com;
# Web interface
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Streaming endpoint
location /stream {
proxy_pass http://localhost:8000/asteroid.mp3;
proxy_set_header Host $host;
proxy_buffering off;
}
}
#+END_SRC
*** Enable Nginx Site
#+BEGIN_SRC bash
sudo ln -s /etc/nginx/sites-available/asteroid-radio /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
#+END_SRC
* Docker Management
** Container Management
#+BEGIN_SRC bash
# Start services
docker compose up -d
# Stop services
docker compose down
# View logs
docker compose logs -f
# Restart services
docker compose restart
#+END_SRC
** Docker Configuration
See =docker/docker-compose.yml= for complete Docker setup with Icecast2 and Liquidsoap containers. The setup includes:
- **Icecast2**: Streaming server with three output formats
- **Liquidsoap**: Audio processing and stream generation
- **Music Volume**: Mounted from =./music/= directory
* Initial Configuration
** First-Time Setup
*** Access Streaming Services
1. **Icecast Admin**: http://localhost:8000/admin/ (admin/asteroid_admin_2024)
2. **Stream URLs**:
- High Quality MP3: http://localhost:8000/asteroid.mp3 (128kbps)
- High Quality AAC: http://localhost:8000/asteroid.aac (96kbps)
- Low Quality MP3: http://localhost:8000/asteroid-low.mp3 (64kbps)
3. **Telnet Control**: =telnet localhost 1234= (for Liquidsoap management)
*** Add Music Library
#+BEGIN_SRC bash
# Copy music files to music directory
cp ~/path/to/music/*.mp3 ~/asteroid-radio/music/
# Files are automatically detected by Liquidsoap
# No additional processing needed - just add files to the music directory
#+END_SRC
*** Test Streaming
#+BEGIN_SRC bash
# Test all streams with curl
curl -I http://localhost:8000/asteroid.mp3 # 128kbps MP3
curl -I http://localhost:8000/asteroid.aac # 96kbps AAC
curl -I http://localhost:8000/asteroid-low.mp3 # 64kbps MP3
# Test with media player
vlc http://localhost:8000/asteroid.mp3 # High quality MP3
vlc http://localhost:8000/asteroid.aac # High quality AAC
#+END_SRC
** Configuration Files
*** Key Configuration Locations
*Docker Setup:*
- =docker/asteroid-radio-docker.liq= - Liquidsoap streaming configuration
- =docker/icecast.xml= - Icecast2 server settings
- =docker/docker-compose.yml= - Container orchestration
*Native Setup:*
- =asteroid-radio.liq= - Liquidsoap streaming configuration
- =/etc/icecast2/icecast.xml= - Icecast2 server settings
- =radiance-core.conf.lisp= - RADIANCE framework configuration
* Production Deployment
** Security Considerations
*** Change Default Passwords
- Update Icecast2 admin password
- Change streaming source password
- Secure database access if using external DB
*** File Permissions
#+BEGIN_SRC bash
# Secure file permissions
sudo chown -R asteroid:asteroid /opt/asteroid-radio
sudo chmod 750 /opt/asteroid-radio
sudo chmod 640 /opt/asteroid-radio/config/*
#+END_SRC
*** Network Security
- Use HTTPS with SSL certificates
- Implement rate limiting
- Configure fail2ban for brute force protection
** Performance Tuning
*** System Limits
#+BEGIN_SRC bash
# Increase file descriptor limits
echo "asteroid soft nofile 65536" | sudo tee -a /etc/security/limits.conf
echo "asteroid hard nofile 65536" | sudo tee -a /etc/security/limits.conf
#+END_SRC
*** Icecast2 Optimization
- Adjust client limits based on server capacity
- Configure appropriate buffer sizes
- Enable burst-on-connect for better user experience
** Monitoring Setup
*** Log Monitoring
#+BEGIN_SRC bash
# Docker setup - monitor container logs
docker compose logs -f icecast
docker compose logs -f liquidsoap
# Native setup - monitor system logs
sudo tail -f /var/log/icecast2/error.log
sudo tail -f /var/log/asteroid-radio/asteroid.log
#+END_SRC
*** Health Checks
#+BEGIN_SRC bash
# Create health check script
cat > ~/asteroid-radio/health-check.sh << 'EOF'
#!/bin/bash
# Check all three streams
curl -I http://localhost:8000/asteroid.mp3 | grep -q "200 OK" || exit 1
curl -I http://localhost:8000/asteroid.aac | grep -q "200 OK" || exit 1
curl -I http://localhost:8000/asteroid-low.mp3 | grep -q "200 OK" || exit 1
# Check Icecast admin interface
curl -f http://localhost:8000/admin/ || exit 1
EOF
chmod +x ~/asteroid-radio/health-check.sh
#+END_SRC
* Troubleshooting
** Common Installation Issues
*** Dependency Problems
- Ensure all system packages are installed
- Check Quicklisp installation
- Verify SBCL can load all required libraries
*** Permission Issues
- Check file ownership and permissions
- Verify service user has access to required directories
- Ensure music directories are writable
*** Network Issues
- Confirm firewall allows required ports
- Check service binding addresses
- Verify no port conflicts with other services
*** Streaming Issues
- Check Icecast2 configuration and logs
- Verify Liquidsoap can access music files
- Test stream connectivity from different networks
** Getting Support
- Check project documentation and FAQ
- Review system logs for error messages
- Submit issues with detailed system information
- Join our IRC chat room: **#asteroid.music** on **irc.libera.chat**
- Join community discussions for help
* Maintenance
** Regular Maintenance Tasks
- Update system packages monthly
- Monitor disk space for music library
- Review and rotate log files
- Backup configuration files
- Test streaming functionality
** Updates and Upgrades
- Follow project release notes
- Test updates in development environment first
- Backup before major upgrades
- Monitor service status after updates
This installation guide provides comprehensive setup instructions for Asteroid Radio. For development-specific setup, see the Development Guide.

110
docs/PROJECT-OVERVIEW.org Normal file
View File

@ -0,0 +1,110 @@
#+TITLE: Asteroid Radio - Project Overview
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade)
#+DATE: 2025-10-03
* 🎯 Mission
Asteroid Radio is a modern, web-based music streaming platform designed for hackers and music enthusiasts. Built with Common Lisp and the Radiance web framework, it combines the power of functional programming with contemporary web technologies.
* 🏗️ Architecture
** Core Components
#+BEGIN_EXAMPLE
┌─────────────────────────────────────────────────────────────┐
│ Asteroid Radio Platform │
├─────────────────────────────────────────────────────────────┤
│ Web Application Layer (Common Lisp + Radiance) │
│ ├── Authentication & User Management │
│ ├── Music Library Management │
│ ├── Web Player Interface │
│ └── API Endpoints │
├─────────────────────────────────────────────────────────────┤
│ Streaming Infrastructure (Docker) │
│ ├── Icecast2 (Streaming Server) │
│ ├── Liquidsoap (Audio Processing) │
│ └── Multiple Format Support (AAC, MP3) │
├─────────────────────────────────────────────────────────────┤
│ Data Layer │
│ ├── PostgreSQL Database (via Radiance) │
│ ├── User Accounts & Profiles │
│ └── Music Metadata │
└─────────────────────────────────────────────────────────────┘
```
### Technology Stack
**Backend:**
- **Common Lisp** (SBCL) - Core application language
- **Radiance Framework** - Web framework and module system
- **LASS** - CSS preprocessing in Lisp
- **PostgreSQL** - Database backend for user accounts and metadata
**Frontend:**
- **HTML5** with semantic templates
- **CSS3** with dark hacker theme
- **JavaScript** for interactive features
- **VT323 Font** for retro terminal aesthetic
**Streaming:**
- **Docker Compose** - Container orchestration
- **Icecast2** - HTTP streaming server
- **Liquidsoap** - Audio processing and encoding
- **Multiple Formats** - AAC 96kbps, MP3 128kbps/64kbps
**Development & Testing:**
- **Make** - Build system
- **Python** - Performance analysis tools
- **Bash** - Testing and deployment scripts
## 🎨 Design Philosophy
### Visual Theme
- **Dark terminal aesthetic** - Black background with colored text
- **Hacker-friendly** - Monospace fonts and terminal-inspired UI
- **Color scheme** - Black → Blue-grey → Cyan → Blue progression
- **Minimalist** - Clean, functional interface without clutter
### Technical Principles
- **Functional programming** - Leveraging Lisp's strengths
- **Modular architecture** - Radiance's interface system
- **Performance first** - Sub-1% CPU usage for web app
- **Self-contained** - Minimal external dependencies
- **Docker-ready** - Containerized streaming infrastructure
## 🚀 Features
### Current Features
- ✅ **User Authentication** - Registration, login, profiles
- ✅ **Music Streaming** - Multiple quality formats
- ✅ **Web Player** - Browser-based music player
- ✅ **Rate Limiting** - Anti-abuse protection
- ✅ **Docker Integration** - Icecast2/Liquidsoap streaming
- ✅ **Responsive Design** - Works on desktop and mobile
### Planned Features
- 🔄 **User Playlists** - Personal music collections
- 🔄 **Social Features** - Sharing and discovery
- 🔄 **Advanced Player** - Queue management, crossfade
- 🔄 **Admin Interface** - System management tools
- 🔄 **API Extensions** - Mobile app support
## 🔮 Vision
Asteroid Radio is the premier streaming platform for **Asteroid Music** - the perfect soundtrack for developers, hackers, and anyone who spends hours deep in code. Our mission is to curate and deliver music that enhances focus, creativity, and the flow state that every programmer knows.
**What is Asteroid Music?**
- **Focus-Enhancing** - Ambient, electronic, and instrumental tracks that don't distract
- **Coding-Optimized** - Rhythms and textures that complement the mental rhythm of programming
- **Hacker Culture** - Music that resonates with the developer mindset and aesthetic
- **Flow State** - Carefully selected tracks that help maintain deep concentration
**Platform Features:**
- **Multi-Format Streaming** - High-quality AAC, MP3 128k, and MP3 64k streams
- **User Community** - Accounts, playlists, and sharing among fellow developers
- **Developer-Friendly** - Built with Common Lisp, fully hackable and extensible
- **Professional Quality** - Crossfading, normalization, metadata, and telnet control
- **Always-On Broadcasting** - Continuous streams perfect for long coding sessions
Asteroid Radio isn't just another music platform - it's the soundtrack to the hacker lifestyle, designed by hackers for hackers who understand that the right music can make the difference between good code and great code.

116
docs/README.org Normal file
View File

@ -0,0 +1,116 @@
#+TITLE: Asteroid Radio - Documentation Index
#+AUTHOR: Documentation Team
#+DATE: 2025-10-03
* Welcome to Asteroid Radio Documentation
Asteroid Radio is a modern internet radio platform designed for developers and music enthusiasts who want to run their own radio stations streaming **Asteroid Music** - the perfect soundtrack for coding and hacking sessions.
* Quick Start
For immediate setup, see:
1. **[[file:INSTALLATION.org][Installation Guide]]** - Get Asteroid Radio running
2. **[[file:DOCKER-STREAMING.org][Docker Streaming Setup]]** - Docker-based streaming infrastructure
* Documentation Structure
** Core Documentation
*** [[file:PROJECT-OVERVIEW.org][Project Overview]]
Complete overview of Asteroid Radio's architecture, technology stack, and vision. Start here to understand what Asteroid Radio is and how it works.
*** [[file:INSTALLATION.org][Installation Guide]]
Comprehensive installation instructions for multiple operating systems, including system requirements, dependencies, and production deployment considerations.
*** [[file:DOCKER-STREAMING.org][Docker Streaming Setup]]
Complete guide to the Docker-based streaming infrastructure using Icecast2 and Liquidsoap. Includes container configuration, management scripts, and troubleshooting.
** Development & Integration
*** [[file:DEVELOPMENT.org][Development Guide]]
Development environment setup, contributing guidelines, coding standards, and debugging procedures for developers working on Asteroid Radio.
*** [[file:API-REFERENCE.org][Interface Reference]]
Documentation of all available interfaces including streaming endpoints, Icecast admin interface, Liquidsoap telnet control, and Docker management commands.
* Current System Status
** What's Working Now
- **Docker Streaming Infrastructure**: Icecast2 + Liquidsoap containers
- **Three Quality Streams**: 128kbps MP3, 96kbps AAC, 64kbps MP3
- **Admin Interface**: Icecast web admin at http://localhost:8000/admin/
- **Telnet Control**: Liquidsoap control via telnet localhost:1234
- **Professional Features**: Crossfading, normalization, metadata support
** Stream URLs (when running)
- **High Quality MP3**: http://localhost:8000/asteroid.mp3 (128kbps)
- **High Quality AAC**: http://localhost:8000/asteroid.aac (96kbps)
- **Low Quality MP3**: http://localhost:8000/asteroid-low.mp3 (64kbps)
* Getting Started
** New Users
1. Read the **[[file:PROJECT-OVERVIEW.org][Project Overview]]** to understand Asteroid Radio
2. Follow the **[[file:INSTALLATION.org][Installation Guide]]** for your operating system
3. Set up streaming with the **[[file:DOCKER-STREAMING.org][Docker Guide]]**
** Developers
1. Review the **[[file:DEVELOPMENT.org][Development Guide]]** for setup procedures
2. Check the **[[file:API-REFERENCE.org][Interface Reference]]** for available controls
3. Join our IRC channel: **#asteroid.music** on **irc.libera.chat**
** System Administrators
1. Follow the **[[file:INSTALLATION.org][Installation Guide]]** production deployment section
2. Review **[[file:DOCKER-STREAMING.org][Docker Streaming Setup]]** for container management
3. Monitor system resources and streaming performance
* Support & Community
** Getting Help
- **Documentation**: Start with the relevant guide above
- **IRC Chat**: Join **#asteroid.music** on **irc.libera.chat**
- **Issues**: Submit detailed bug reports with system information
- **Logs**: Check Docker container logs for troubleshooting
** Contributing
- Review the **[[file:DEVELOPMENT.org][Development Guide]]** for contribution guidelines
- Follow coding standards and testing procedures
- Submit pull requests with clear descriptions
* About Asteroid Music
Asteroid Radio streams **Asteroid Music** - a carefully curated genre designed for developers:
- **Focus-Enhancing**: Ambient, electronic, and instrumental tracks
- **Coding-Optimized**: Rhythms that complement programming flow
- **Hacker Culture**: Music that resonates with developer aesthetics
- **Flow State**: Tracks selected to maintain deep concentration
This isn't just background music - it's the soundtrack to the hacker lifestyle, designed by hackers for hackers who understand that the right music can elevate your code.
* Technical Architecture
Asteroid Radio uses a modern, containerized architecture:
#+BEGIN_EXAMPLE
┌─────────────────────────────────────────────────────────────┐
│ Asteroid Radio Platform │
├─────────────────────────────────────────────────────────────┤
│ Streaming Infrastructure (Docker) │
│ ├── Icecast2 (HTTP Streaming Server) │
│ ├── Liquidsoap (Audio Processing Pipeline) │
│ └── Multiple Format Support (AAC, MP3) │
├─────────────────────────────────────────────────────────────┤
│ Control Interfaces │
│ ├── Icecast Admin Web Interface │
│ ├── Liquidsoap Telnet Control │
│ └── Docker Container Management │
└─────────────────────────────────────────────────────────────┘
#+END_EXAMPLE
For detailed technical information, see the **[[file:PROJECT-OVERVIEW.org][Project Overview]]**.
---
*Last Updated: 2025-10-03*
*Documentation Version: 1.0*

57
run-all-tests.sh Executable file
View File

@ -0,0 +1,57 @@
#!/bin/bash
# Helper script to run all three stream format tests
# Usage: ./run-all-tests.sh
echo "=== Asteroid Comprehensive Performance Testing Suite ==="
echo ""
echo "This will run three 15-minute tests:"
echo "1. AAC 96kbps stream"
echo "2. MP3 128kbps stream"
echo "3. MP3 64kbps stream"
echo ""
echo "Each test will:"
echo "- Start Docker containers (Icecast2 + Liquidsoap)"
echo "- Start Asteroid web application"
echo "- Monitor performance for 15 minutes"
echo "- Generate light web traffic"
echo "- Save detailed logs and CSV data"
echo ""
read -p "Press Enter to start the test suite, or Ctrl+C to cancel..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo ""
echo "=== TEST 1/3: AAC 96kbps Stream ==="
echo "Starting AAC test..."
"$SCRIPT_DIR/comprehensive-performance-test.sh" aac
echo ""
echo "AAC test completed. Please switch to AAC stream format in Liquidsoap if needed."
read -p "Press Enter when ready for MP3 High Quality test..."
echo ""
echo "=== TEST 2/3: MP3 128kbps Stream ==="
echo "Starting MP3 High Quality test..."
"$SCRIPT_DIR/comprehensive-performance-test.sh" mp3-high
echo ""
echo "MP3 High test completed. Please switch to MP3 Low Quality stream format if needed."
read -p "Press Enter when ready for MP3 Low Quality test..."
echo ""
echo "=== TEST 3/3: MP3 64kbps Stream ==="
echo "Starting MP3 Low Quality test..."
"$SCRIPT_DIR/comprehensive-performance-test.sh" mp3-low
echo ""
echo "=== ALL TESTS COMPLETED ==="
echo ""
echo "Results saved in: $SCRIPT_DIR/performance-logs/"
echo ""
echo "Log files created:"
ls -la "$SCRIPT_DIR/performance-logs/" | grep "$(date +%Y%m%d)"
echo ""
echo "To analyze results, check the CSV files for detailed performance data."

37
setup-environment.lisp Normal file
View File

@ -0,0 +1,37 @@
;;;; Setup script for Asteroid Radio Radiance environment
;;;; This creates the necessary symbolic links for the custom environment
(defun setup-asteroid-environment ()
"Set up the asteroid Radiance environment with symbolic links to project config"
(let* ((project-root (asdf:system-source-directory :asteroid))
(config-dir (merge-pathnames "config/" project-root))
(radiance-env-dir (merge-pathnames ".config/radiance/asteroid/"
(user-homedir-pathname))))
;; Ensure the radiance environment directory exists
(ensure-directories-exist radiance-env-dir)
;; Create symbolic links for each config file
(dolist (config-file '("radiance-core.conf.lisp"
"i-lambdalite.conf.lisp"
"simple-auth.conf.lisp"
"simple-sessions.conf.lisp"
"i-hunchentoot.conf.lisp"))
(let ((source (merge-pathnames config-file config-dir))
(target (merge-pathnames config-file radiance-env-dir)))
(when (probe-file target)
(delete-file target))
(when (probe-file source)
#+unix
(sb-posix:symlink (namestring source) (namestring target))
#-unix
(progn
(format t "Warning: Symbolic links not supported on this platform~%")
(format t "Please manually copy ~a to ~a~%" source target)))))
(format t "Asteroid environment setup complete!~%")
(format t "Config directory: ~a~%" config-dir)
(format t "Radiance environment: ~a~%" radiance-env-dir)))
;; Auto-setup when loaded
(setup-asteroid-environment)

218
simple-analysis.py Normal file
View File

@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
Simple Asteroid Radio Performance Analysis
Uses only matplotlib (no seaborn dependency)
"""
import pandas as pd
import matplotlib.pyplot as plt
import glob
import os
from datetime import datetime
import numpy as np
def load_performance_data():
"""Load all CSV performance data files"""
csv_files = glob.glob('performance-logs/*_data_*.csv')
data_frames = {}
for file in csv_files:
# Extract test type from filename
filename = os.path.basename(file)
if 'aac' in filename:
test_type = 'AAC 96kbps'
elif 'mp3-high' in filename:
test_type = 'MP3 128kbps'
elif 'mp3-low' in filename:
test_type = 'MP3 64kbps'
else:
test_type = filename.split('_')[1]
try:
df = pd.read_csv(file)
df['test_type'] = test_type
data_frames[test_type] = df
print(f"✅ Loaded {len(df)} records from {test_type} test")
except Exception as e:
print(f"❌ Error loading {file}: {e}")
return data_frames
def create_simple_charts(data_frames):
"""Create simple performance charts"""
# Create figure with subplots
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
fig.suptitle('Asteroid Radio Performance Analysis', fontsize=14)
colors = ['#ff6b6b', '#4ecdc4', '#45b7d1']
# 1. CPU Usage Over Time
ax1 = axes[0, 0]
for i, (test_type, df) in enumerate(data_frames.items()):
if 'cpu_percent' in df.columns:
ax1.plot(range(len(df)), df['cpu_percent'],
label=test_type, color=colors[i % len(colors)], linewidth=2)
ax1.set_title('CPU Usage Over Time')
ax1.set_xlabel('Time (samples)')
ax1.set_ylabel('CPU %')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 2. Memory Usage Over Time
ax2 = axes[0, 1]
for i, (test_type, df) in enumerate(data_frames.items()):
if 'memory_mb' in df.columns:
ax2.plot(range(len(df)), df['memory_mb'],
label=test_type, color=colors[i % len(colors)], linewidth=2)
ax2.set_title('Memory Usage Over Time')
ax2.set_xlabel('Time (samples)')
ax2.set_ylabel('Memory (MB)')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 3. Average Performance Comparison
ax3 = axes[1, 0]
test_names = []
cpu_avgs = []
mem_avgs = []
for test_type, df in data_frames.items():
test_names.append(test_type.replace(' ', '\n'))
cpu_avgs.append(df['cpu_percent'].mean() if 'cpu_percent' in df.columns else 0)
mem_avgs.append(df['memory_mb'].mean() if 'memory_mb' in df.columns else 0)
x = np.arange(len(test_names))
width = 0.35
ax3.bar(x - width/2, cpu_avgs, width, label='CPU %', color=colors[0], alpha=0.8)
ax3_twin = ax3.twinx()
ax3_twin.bar(x + width/2, mem_avgs, width, label='Memory MB', color=colors[1], alpha=0.8)
ax3.set_title('Average Resource Usage')
ax3.set_xlabel('Stream Type')
ax3.set_ylabel('CPU %', color=colors[0])
ax3_twin.set_ylabel('Memory (MB)', color=colors[1])
ax3.set_xticks(x)
ax3.set_xticklabels(test_names)
# 4. Response Time Summary
ax4 = axes[1, 1]
response_summary = {}
for test_type, df in data_frames.items():
stream_resp = df['stream_response_ms'].mean() if 'stream_response_ms' in df.columns else 0
web_resp = df['web_response_ms'].mean() if 'web_response_ms' in df.columns else 0
response_summary[test_type] = {'Stream': stream_resp, 'Web': web_resp}
if response_summary:
test_types = list(response_summary.keys())
stream_times = [response_summary[t]['Stream'] for t in test_types]
web_times = [response_summary[t]['Web'] for t in test_types]
x = np.arange(len(test_types))
ax4.bar(x - width/2, stream_times, width, label='Stream Response', color=colors[0], alpha=0.8)
ax4.bar(x + width/2, web_times, width, label='Web Response', color=colors[1], alpha=0.8)
ax4.set_title('Average Response Times')
ax4.set_xlabel('Stream Type')
ax4.set_ylabel('Response Time (ms)')
ax4.set_xticks(x)
ax4.set_xticklabels([t.replace(' ', '\n') for t in test_types])
ax4.legend()
plt.tight_layout()
plt.savefig('performance-logs/asteroid_performance_charts.png', dpi=300, bbox_inches='tight')
print("📊 Charts saved as: performance-logs/asteroid_performance_charts.png")
return fig
def generate_text_report(data_frames):
"""Generate simple text report"""
report = []
report.append("🎵 ASTEROID RADIO PERFORMANCE REPORT")
report.append("=" * 45)
report.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
report.append("")
for test_type, df in data_frames.items():
report.append(f"📡 {test_type}:")
report.append("-" * 25)
if 'cpu_percent' in df.columns:
cpu_mean = df['cpu_percent'].mean()
cpu_max = df['cpu_percent'].max()
report.append(f" CPU: {cpu_mean:.1f}% avg, {cpu_max:.1f}% peak")
if 'memory_mb' in df.columns:
mem_mean = df['memory_mb'].mean()
mem_max = df['memory_mb'].max()
report.append(f" Memory: {mem_mean:.1f} MB avg, {mem_max:.1f} MB peak")
if 'stream_response_ms' in df.columns:
stream_resp = df['stream_response_ms'].dropna()
if len(stream_resp) > 0:
report.append(f" Stream Response: {stream_resp.mean():.1f} ms avg")
if 'web_response_ms' in df.columns:
web_resp = df['web_response_ms'].dropna()
if len(web_resp) > 0:
report.append(f" Web Response: {web_resp.mean():.1f} ms avg")
report.append(f" Test Duration: {len(df)} samples")
report.append("")
# Summary
report.append("📊 SUMMARY:")
report.append("-" * 15)
# Find most efficient stream
cpu_usage = {}
for test_type, df in data_frames.items():
if 'cpu_percent' in df.columns:
cpu_usage[test_type] = df['cpu_percent'].mean()
if cpu_usage:
best = min(cpu_usage, key=cpu_usage.get)
worst = max(cpu_usage, key=cpu_usage.get)
report.append(f" Most efficient: {best} ({cpu_usage[best]:.1f}% CPU)")
report.append(f" Most intensive: {worst} ({cpu_usage[worst]:.1f}% CPU)")
total_samples = sum(len(df) for df in data_frames.values())
report.append(f" Total test samples: {total_samples}")
report.append(f" Stream formats tested: {len(data_frames)}")
# Save report
with open('performance-logs/asteroid_simple_report.txt', 'w') as f:
f.write('\n'.join(report))
print("📄 Report saved as: performance-logs/asteroid_simple_report.txt")
return '\n'.join(report)
def main():
print("🎵 Asteroid Radio Performance Analyzer (Simple)")
print("=" * 45)
# Load data
data_frames = load_performance_data()
if not data_frames:
print("❌ No performance data found!")
return
# Create charts
print("\n📊 Creating performance charts...")
create_simple_charts(data_frames)
# Generate report
print("\n📄 Generating performance report...")
report = generate_text_report(data_frames)
print("\n✅ Analysis complete!")
print("\nFiles created:")
print(" 📊 performance-logs/asteroid_performance_charts.png")
print(" 📄 performance-logs/asteroid_simple_report.txt")
if __name__ == "__main__":
main()

View File

@ -234,8 +234,8 @@
const query = document.getElementById('track-search').value.toLowerCase();
const filtered = tracks.filter(track =>
(track.title || '').toLowerCase().includes(query) ||
(track.artist || '').toLowerCase().includes(query) ||
(track.album || '').toLowerCase().includes(query)
(track.artist || '').toLowerCase().includes(query) ||
(track.album || '').toLowerCase().includes(query)
);
displayTracks(filtered);
}
@ -244,11 +244,9 @@
function sortTracks() {
const sortBy = document.getElementById('sort-tracks').value;
const sorted = [...tracks].sort((a, b) => {
/* const aVal = a[sortBy] ? a[sortBy][0] : '';
* const bVal = b[sortBy] ? b[sortBy][0] : ''; */
const aVal = a[sortBy] ? a[sortBy] : '';
const bVal = b[sortBy] ? b[sortBy] : '';
return aVal.localeCompare(bVal);
const aVal = a[sortBy] || '';
const bVal = b[sortBy] || '';
return aVal.localeCompare(bVal);
});
displayTracks(sorted);
}