Compare commits

..

14 Commits

Author SHA1 Message Date
glenneth 90bb9a1650 refactor: Implement Lispy improvements - templates, strings, and error handling
This commit implements three major refactorings to make the codebase more
idiomatic and maintainable:

1. Template Path Centralization
   - Add *template-directory* parameter and helper functions
   - Replace 11+ instances of repetitive template loading boilerplate
   - New functions: template-path, load-template in template-utils.lisp

2. String Construction with FORMAT
   - Replace concatenate with format for external URLs (Icecast, static files)
   - Maintain Radiance URI handling for internal routes
   - Applied to stream URLs, status endpoints, and API responses

3. Error Handling with Custom Conditions
   - NEW FILE: conditions.lisp with comprehensive error hierarchy
   - Custom conditions: not-found-error, authentication-error,
     authorization-error, validation-error, database-error, asteroid-stream-error
   - Helper macros: with-error-handling, with-db-error-handling
   - Helper functions: signal-not-found, signal-validation-error, etc.
   - Refactored 19 API endpoints and page routes
   - Proper HTTP status codes: 404, 401, 403, 400, 500

Changes:
- conditions.lisp: NEW (180+ lines of error handling infrastructure)
- asteroid.asd: Add conditions.lisp to system components
- asteroid.lisp: Refactor 30+ endpoints, eliminate 200+ lines of boilerplate
- template-utils.lisp: Add centralized template loading helpers
- frontend-partials.lisp: Update template loading and string construction

Net result: -97 lines of code, significantly improved error handling,
more maintainable and idiomatic Common Lisp.

All changes tested and verified:
- Clean build
- All endpoints functional
- Error handling returns proper HTTP codes
- No regressions
2025-11-02 12:05:23 -05:00
Brian O'Reilly 0bb93c53a4 Update template paths in calling code. 2025-11-01 15:19:22 -04:00
Brian O'Reilly bc36a00322 Change the template extension to match clip documentation 2025-11-01 15:04:32 -04:00
Glenn Thompson 637650a5ef docs: Update PROJECT-HISTORY.org with Phase 8 and recent developments
Added Phase 8: Docker Deployment & Documentation (Oct 26 - Nov 1, 2025)
- easilok's Docker containerization work (user init, env vars, Dockerfile)
- Complete Docker deployment documentation
- Documentation updates and refinements
- Cross-distribution package manager support

Updated statistics:
- 213+ total commits (was 205+)
- 2.75 months active development (was 2.5)
- Luis Pereira (easilok) now at 23+ commits

Updated current state to November 2025:
- Complete Docker deployment for streams and application
- Comprehensive documentation overhaul
- Recent achievements section added
- Future work includes live chat and song requests per design.org

Last updated: 2025-11-01
2025-11-01 11:48:27 -04:00
Glenn Thompson fd02e4c1d1 chore: remove obsolete session notes file
SESSION-NOTES-2025-10-12.org is no longer needed
2025-11-01 11:48:27 -04:00
Glenn Thompson 1c85464a5f docs: Fix music directory location, remove Python examples, add package manager notes
- Updated DEVELOPMENT.org: music directory is now asteroid/music/ (not docker/music/)
- Clarified music/ can be a symlink to actual music collection
- Added multiple symlink options for music management
- Removed redundant Python integration examples from API-ENDPOINTS.org
- Removed duplicate Integration Examples section (curl already covered in Testing)
- Added package manager notes to INSTALLATION, DEVELOPMENT, DOCKER-STREAMING, and TESTING
- Notes clarify apt examples can be replaced with dnf, pacman, zypper, apk, etc.
- Maintains clean documentation without cluttering every command
2025-11-01 11:48:27 -04:00
Glenn Thompson f1eb43b325 docs: Comprehensive documentation update for October 2025
- Created PROJECT-HISTORY.org with complete development timeline
- Updated all documentation dates to 2025-10-26
- Added current features: multiple player modes, stream queue control, dynamic URLs
- Updated repository URLs from placeholders to actual GitHub links
- Refreshed feature lists across all docs to reflect current state
- Added PostgreSQL status (configured, ready for migration)
- Updated root README.org with comprehensive current information
- Improved quick start guides and access points
- Enhanced API documentation with complete endpoint list
- Updated all streaming documentation for Docker setup
- Standardized author attribution across all docs
- Incremented docs version to 3.0

All documentation now accurately reflects the current state of the project
with 205+ commits, 3 core contributors, and 2.5 months of active development.
2025-11-01 11:48:27 -04:00
Luis Pereira a458a85823 feat: added documentation on build and deploy docker based asteroid 2025-10-30 19:08:46 -04:00
Luis Pereira ab3acf1279 feat: add docker setup for asteroid app 2025-10-30 19:08:46 -04:00
Luis Pereira c4fd96289b feat: add custom env volume path for stream containers 2025-10-30 19:08:46 -04:00
Luis Pereira 0930fc2c1c fix: retry user initialization 2025-10-30 19:08:46 -04:00
Brian O'Reilly a2ae329d54 Merge branch 'glenneth1-fix/persistent-player-font'
conflict resolution
2025-10-25 12:36:30 -04:00
glenneth a795680e99 feat: Add hybrid player with frameset and pop-out options
- Add frameset mode with persistent audio player in bottom frame
- Add localStorage preference system for user choice
- Update all page navigation to work in both regular and frameset modes
- Add enable/disable buttons for frameset mode
- Fix redirect loops and template parameter issues
2025-10-22 18:01:48 -04:00
glenneth d8abd9661d feat: Add pop-out player and queue management improvements
- Add pop-out player window (400x300px) with auto-reconnect on stream errors
- Add queue reordering with up/down buttons in admin panel
- Add 'Load Queue from M3U' functionality
- Remove Play/Stream buttons from track management
- Fix Liquidsoap audio quality issues:
  - Remove ReplayGain and compression to prevent pulsing
  - Change reload_mode to 'seconds' to prevent playlist exhaustion
  - Reduce crossfade to 3 seconds
  - Add audio buffering settings for stability
- Add auto-reconnect logic for both front page and pop-out players
2025-10-22 18:01:48 -04:00
40 changed files with 1437 additions and 927 deletions

8
.dockerignore.asteroid Normal file
View File

@ -0,0 +1,8 @@
docker/
music/
data/
*.org
docker-compose.yml
Dockerfile*
Makefile
.git/

42
Dockerfile.asteroid Normal file
View File

@ -0,0 +1,42 @@
FROM debian:bookworm-slim AS builder
RUN apt-get update && \
apt-get install -y curl openssl ca-certificates \
git make sbcl rlwrap && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy asteroid source to container workdir
COPY . .
# Download Quicklisp installer
RUN curl -O https://beta.quicklisp.org/quicklisp.lisp
# Installs quicklisp and radiance
RUN sbcl --eval "(load \"quicklisp.lisp\")" \
--eval "(quicklisp-quickstart:install)" \
--eval "(ql-dist:install-dist \"http://dist.shirakumo.org/shirakumo.txt\" :prompt nil)" \
--eval "(ql:quickload :radiance)"
# Makes the project workdir known as a quicklisp project
RUN mkdir -p $HOME/.config/common-lisp/source-registry.conf.d
RUN echo '(:tree "/app/")' >> "$HOME/.config/common-lisp/source-registry.conf.d/projects.conf"
# Builds Asteroid binary
RUN make
# Links binary to path
ENV PATH="$PATH:/app"
# Adds radiance system configuration file
COPY docker/radiance-default.conf.lisp $HOME/.config/radiance/default/radiance-core/radiance-core.conf.lisp
# Application
EXPOSE 8080
# Slynk server
EXPOSE 4009
ENV ASTEROID_STREAM_URL=http://localhost:8000
CMD [ "asteroid" ]

View File

@ -1,35 +1,45 @@
#+TITLE: Asteroid Radio - Internet Streaming Implementation
#+AUTHOR: Database Implementation Branch
#+DATE: 2025-09-11
#+TITLE: Asteroid Radio - Internet Radio Streaming Platform
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-26
* Overview
This branch implements a complete internet radio streaming system for Asteroid Radio, transforming it from a simple web interface into a fully functional streaming radio station with live broadcasting capabilities.
Asteroid Radio is a complete internet radio streaming platform built with Common Lisp, featuring a hacker-themed terminal aesthetic. The project combines the Radiance web framework with Icecast/Liquidsoap streaming infrastructure to create a full-featured music streaming platform with live broadcasting capabilities.
** Project Links
- *Repository*: https://github.com/fade/asteroid
- *IRC*: #asteroid.music on irc.libera.chat
- *Documentation*: See =docs/= directory for comprehensive guides
* Key Features
** Live Internet Radio Streaming
- Continuous MP3 streaming at 128kbps stereo
- Professional audio processing with crossfading and normalization
- Multiple quality streams: 128kbps MP3, 96kbps AAC, 64kbps MP3
- Professional audio processing with crossfading and ReplayGain normalization
- Icecast2 streaming server integration
- Liquidsoap audio pipeline for reliable broadcasting
- Stream queue control for curated programming
** Music Library Management
- Database-backed track storage with metadata extraction
- Support for MP3, FLAC, OGG, and WAV formats
- Automatic metadata extraction using taglib
- Track search, filtering, and sorting capabilities
- Track search, filtering, sorting, and pagination
- Recursive directory scanning
** Web Interface
- RADIANCE framework with CLIP templating
- Admin dashboard for library management
- Web player with HTML5 audio controls
- Admin dashboard for library and user management
- Multiple player modes: inline, pop-out, and persistent frameset
- Live stream integration with embedded player
- Responsive design for desktop and mobile
- Role-based access control (Admin/DJ/Listener)
** Network Broadcasting
- WSL-compatible networking for internal network access
- Dynamic stream URL detection for multi-environment support
- Professional streaming URLs for media players
- Multi-listener support via Icecast2
- Docker-based deployment for easy setup
* Architecture Changes
@ -40,46 +50,82 @@ This branch implements a complete internet radio streaming system for Asteroid R
- Database abstraction layer for track storage
** Streaming Stack
- *Icecast2*: Streaming server (port 8000)
- *Liquidsoap*: Audio processing and streaming pipeline
- *Icecast2*: Streaming server (port 8000) - Docker containerized
- *Liquidsoap*: Audio processing and streaming pipeline - Docker containerized
- *RADIANCE*: Web server and API (port 8080)
- *Database*: Track metadata and playlist storage
- *PostgreSQL*: Database backend (configured, ready for migration)
- *Docker Compose*: Container orchestration
** File Structure
#+BEGIN_SRC
asteroid/
├── asteroid.lisp # Main server with RADIANCE routes
├── asteroid.asd # System definition with dependencies
├── asteroid-radio.liq # Liquidsoap streaming configuration
├── playlist.m3u # Generated playlist for streaming
├── start-asteroid-radio.sh # Launch script for all services
├── stop-asteroid-radio.sh # Stop script for all services
├── stream-control.lisp # Stream queue management
├── user-management.lisp # User administration
├── playlist-management.lisp # Playlist operations
├── test-server.sh # Automated test suite
├── docker/ # Docker infrastructure
│ ├── docker-compose.yml # Container orchestration
│ ├── asteroid-radio-docker.liq # Liquidsoap config
│ ├── icecast.xml # Icecast configuration
│ └── music/ # Music library mount
├── template/ # CLIP HTML templates
│ ├── front-page.chtml # Main page with live stream
│ ├── admin.chtml # Admin dashboard
│ └── player.chtml # Web player interface
│ ├── player.chtml # Web player interface
│ └── users.chtml # User management
├── static/ # CSS and assets
│ └── asteroid.lass # LASS stylesheet
└── music/ # Music library
├── incoming/ # Upload staging area
└── library/ # Processed music files
├── docs/ # Comprehensive documentation
│ ├── README.org # Documentation index
│ ├── PROJECT-OVERVIEW.org # Architecture overview
│ ├── PROJECT-HISTORY.org # Development timeline
│ ├── INSTALLATION.org # Setup guide
│ └── ... # Additional guides
└── music/ # Music library (local dev)
#+END_SRC
* Track Upload Workflow
* Quick Start
** Current Implementation (Manual Upload)
1. *Copy files to staging*: Place MP3/FLAC files in =music/incoming/=
2. *Access admin panel*: Navigate to =http://[IP]:8080/asteroid/admin=
3. *Process files*: Click "Copy Files from Incoming" button
4. *Database update*: Files are moved to =music/library/= and metadata extracted
5. *Automatic playlist*: =playlist.m3u= is regenerated for streaming
** Docker Installation (Recommended)
#+BEGIN_SRC bash
# Clone repository
git clone https://github.com/fade/asteroid
cd asteroid/docker
** File Processing Steps
1. Files copied from =music/incoming/= to =music/library/=
2. Metadata extracted using taglib (title, artist, album, duration, bitrate)
3. Database record created with file path and metadata
4. Playlist file updated for Liquidsoap streaming
5. Files immediately available for on-demand streaming
# Start all services
docker compose up -d
# Verify streams are working
curl -I http://localhost:8000/asteroid.mp3
curl -I http://localhost:8000/asteroid.aac
curl -I http://localhost:8000/asteroid-low.mp3
#+END_SRC
** Access Points
- *Web Interface*: http://localhost:8080/asteroid/
- *Admin Panel*: http://localhost:8080/asteroid/admin
- *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)
* Music Library Management
** Adding Music
1. *Copy files*: Place MP3/FLAC files in =docker/music/= directory
2. *Access admin panel*: Navigate to =http://localhost:8080/asteroid/admin=
3. *Scan library*: Click "Scan Library" to index new tracks
4. *Metadata extraction*: Track information automatically extracted
5. *Stream queue*: Optionally add tracks to broadcast queue
** Library Scanning
1. Recursive directory scanning of music folder
2. Metadata extracted using taglib (title, artist, album, duration)
3. Database records created with file paths and metadata
4. Tracks immediately available for playback and streaming
5. Supports nested folder structures
** Supported Formats
- *MP3*: Primary format, best compatibility
@ -90,216 +136,259 @@ asteroid/
* Icecast2 Integration
** Configuration
- *Server*: localhost:8000
- *Mount point*: =/asteroid.mp3=
- *Password*: =b3l0wz3r0= (configured in Liquidsoap)
- *Format*: MP3 128kbps stereo
- *Server*: localhost:8000 (Docker container)
- *Mount points*: =/asteroid.mp3=, =/asteroid.aac=, =/asteroid-low.mp3=
- *Password*: =H1tn31EhsyLrfRmo= (configured in Docker setup)
- *Formats*: MP3 128kbps, AAC 96kbps, MP3 64kbps
** Docker Setup
Icecast2 runs in a Docker container - no manual installation needed.
** Installation (Ubuntu/Debian)
#+BEGIN_SRC bash
sudo apt update
sudo apt install icecast2
sudo systemctl enable icecast2
sudo systemctl start icecast2
# Managed via docker-compose
cd docker
docker compose up -d icecast
#+END_SRC
** Stream Access
- *Direct URL*: =http://[IP]:8000/asteroid.mp3=
- *Admin interface*: =http://[IP]:8000/admin/=
- *Statistics*: =http://[IP]:8000/status.xsl=
- *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)
- *Admin interface*: =http://localhost:8000/admin/= (admin/asteroid_admin_2024)
- *Statistics*: =http://localhost:8000/status.xsl=
* Liquidsoap Integration
** Configuration File: =asteroid-radio.liq=
#+BEGIN_SRC liquidsoap
#!/usr/bin/liquidsoap
** Docker Configuration
Liquidsoap runs in a Docker container with configuration in =docker/asteroid-radio-docker.liq=
# Set log level for debugging
settings.log.level := 4
** Key Features
- *Multiple outputs*: Generates 3 simultaneous streams (MP3 128k, AAC 96k, MP3 64k)
- *Audio processing*: Crossfading, normalization, ReplayGain
- *Stream queue*: Reads from M3U playlist for curated programming
- *Telnet control*: Remote control interface on port 1234
- *Metadata*: Broadcasts track information to listeners
# Create playlist from directory
radio = playlist(mode="randomize", reload=3600, "/path/to/music/library/")
# Add audio processing
radio = amplify(1.0, radio)
# Fallback with sine wave for debugging
radio = fallback(track_sensitive=false, [radio, sine(440.0)])
# Output to Icecast2
output.icecast(
%mp3(bitrate=128),
host="localhost",
port=8000,
password="b3l0wz3r0",
mount="asteroid.mp3",
name="Asteroid Radio",
description="Music for Hackers - Streaming from the Asteroid",
genre="Electronic/Alternative",
url="http://localhost:8080/asteroid/",
radio
)
#+END_SRC
** Installation (Ubuntu/Debian)
** Management
#+BEGIN_SRC bash
sudo apt update
sudo apt install liquidsoap
# Start Liquidsoap container
cd docker
docker compose up -d liquidsoap
# View logs
docker compose logs -f liquidsoap
# Restart streaming
docker compose restart liquidsoap
#+END_SRC
** Features
- *Random playlist*: Shuffles music library continuously
- *Auto-reload*: Playlist refreshes every hour
- *Audio processing*: Amplification and normalization
- *Fallback*: Sine tone if no music available (debugging)
- *Metadata*: Station info broadcast to listeners
* Network Access
** Local Development
- *Web Interface*: =http://localhost:8080/asteroid/=
- *Live Stream*: =http://localhost:8000/asteroid.mp3=
- *Admin Panel*: =http://localhost:8080/asteroid/admin=
** WSL Network Access
- *WSL IP*: Check with =ip addr show eth0=
- *Web Interface*: =http://[WSL-IP]:8080/asteroid/=
- *Live Stream*: =http://[WSL-IP]:8000/asteroid.mp3=
** Internal Network Broadcasting
- Services bind to all interfaces (0.0.0.0)
- Accessible from any device on local network
- Compatible with media players (VLC, iTunes, etc.)
* Usage Instructions
** Starting the Radio Station
** Telnet Control
#+BEGIN_SRC bash
# Launch all services
./start-asteroid-radio.sh
# Connect to Liquidsoap
telnet localhost 1234
# Or use netcat for scripting
echo "request.queue" | nc localhost 1234
echo "request.skip" | nc localhost 1234
#+END_SRC
** Stopping the Radio Station
#+BEGIN_SRC bash
# Stop all services
./stop-asteroid-radio.sh
#+END_SRC
* User Management
** Adding Music
1. Copy MP3/FLAC files to =music/incoming/=
2. Visit admin panel: =http://[IP]:8080/asteroid/admin=
3. Click "Copy Files from Incoming"
4. Files are processed and added to streaming playlist
** Roles
- *Admin*: Full system access, user management, stream control
- *DJ*: Content management, playlist creation, library access
- *Listener*: Basic playback and personal playlists
** Listening to the Stream
- *Web Browser*: Visit main page for embedded player
- *Media Player*: Open =http://[IP]:8000/asteroid.mp3=
- *Mobile Apps*: Use internet radio apps with stream URL
** Default Credentials
- Username: =admin=
- Password: =asteroid123=
- ⚠️ Change default password after first login
** User Administration
- Create/manage users via admin panel
- Role-based access control
- User profiles and preferences
- Session management
* Player Modes
** Inline Player
- Embedded in web pages
- Standard HTML5 audio controls
- Queue management
** Pop-Out Player
- Standalone player window
- Independent from main browser window
- Persistent across page navigation
** Frameset Player
- Bottom-frame persistent player
- Audio continues during site navigation
- Seamless listening experience
* API Endpoints
Asteroid Radio provides a comprehensive REST API with 15+ endpoints.
** Status & Authentication
- =GET /api/asteroid/status= - Server status
- =GET /api/asteroid/auth-status= - Authentication status
- =GET /api/asteroid/icecast-status= - Streaming status
** Track Management
- =GET /api/tracks= - List all tracks with metadata
- =GET /tracks/{id}/stream= - Stream individual track
- =POST /api/scan-library= - Scan and update music library
- =POST /api/copy-files= - Process files from incoming directory
- =GET /api/asteroid/tracks= - List all tracks
- =GET /api/asteroid/admin/tracks= - Admin track listing
- =POST /api/asteroid/admin/scan-library= - Scan music library
** Player Control
- =POST /api/player/play= - Start playback
- =POST /api/player/pause= - Pause playback
- =POST /api/player/stop= - Stop playback
- =GET /api/status= - Get server status
- =GET /api/asteroid/player/status= - Player status
- =POST /api/asteroid/player/play= - Play track
- =POST /api/asteroid/player/pause= - Pause playback
- =POST /api/asteroid/player/stop= - Stop playback
- =POST /api/asteroid/player/resume= - Resume playback
** Search and Filter
- =GET /api/tracks?search={query}= - Search tracks
- =GET /api/tracks?sort={field}= - Sort by field
- =GET /api/tracks?artist={name}= - Filter by artist
** Playlist Management
- =GET /api/asteroid/playlists= - List user playlists
- =POST /api/asteroid/playlists/create= - Create playlist
- =GET /api/asteroid/playlists/get= - Get playlist details
- =POST /api/asteroid/playlists/add-track= - Add track to playlist
* Database Schema
** Stream Queue Control (Admin)
- =GET /api/asteroid/stream/queue= - Get broadcast queue
- =POST /api/asteroid/stream/queue/add= - Add track to queue
- =POST /api/asteroid/stream/queue/remove= - Remove from queue
- =POST /api/asteroid/stream/queue/clear= - Clear queue
** Tracks Collection
#+BEGIN_SRC lisp
(db:create "tracks" '((title :text)
(artist :text)
(album :text)
(duration :integer)
(file-path :text)
(format :text)
(bitrate :integer)
(added-date :integer)
(play-count :integer)))
#+END_SRC
See =docs/API-ENDPOINTS.org= for complete API documentation.
** Playlists Collection (Future)
#+BEGIN_SRC lisp
(db:create "playlists" '((name :text)
(description :text)
(created-date :integer)
(track-ids :text)))
#+END_SRC
* Database
** Current: Radiance DB
- File-based database abstraction
- Tracks, users, playlists, sessions
- Suitable for development and small deployments
** PostgreSQL (Configured)
- Docker container ready
- Full schema defined
- Migration pending
- See =docs/POSTGRESQL-SETUP.org= for details
* Documentation
Comprehensive documentation available in the =docs/= directory:
- *README.org* - Documentation index
- *PROJECT-OVERVIEW.org* - Architecture and features
- *PROJECT-HISTORY.org* - Development timeline and milestones
- *INSTALLATION.org* - Complete installation guide
- *DEVELOPMENT.org* - Developer setup and guidelines
- *DOCKER-STREAMING.org* - Docker streaming infrastructure
- *API-ENDPOINTS.org* - REST API reference
- *STREAM-CONTROL.org* - Stream queue management
- *USER-MANAGEMENT-SYSTEM.org* - User administration
- *PLAYLIST-SYSTEM.org* - Playlist functionality
- *TESTING.org* - Automated testing guide
- *POSTGRESQL-SETUP.org* - Database setup
* Dependencies
** Lisp Dependencies (asteroid.asd)
- =:radiance= - Web framework
- =:r-clip= - Templating system
- =:lass= - CSS generation
- =:cl-json= - JSON handling
- =:alexandria= - Utilities
- =:local-time= - Time handling
** Lisp Dependencies
- =radiance= - Web framework
- =r-clip= - CLIP templating
- =lass= - CSS preprocessing
- =cl-json= - JSON handling
- =alexandria= - Common Lisp utilities
- =local-time= - Time handling
- =taglib= - Audio metadata extraction
** System Dependencies
- =icecast2= - Streaming server
- =liquidsoap= - Audio processing
- =taglib= - Metadata extraction (via audio-streams)
** System Dependencies (Docker)
- Docker Engine 20.10+
- Docker Compose 2.0+
- All streaming components containerized
* Development Notes
* Testing
** RADIANCE Configuration
- Domain: "asteroid"
- Routes use =#@= syntax for URL patterns
- Database abstraction via =db:= functions
- CLIP templates with =data-text= attributes
** Automated Test Suite
#+BEGIN_SRC bash
# Run comprehensive tests
./test-server.sh
** Database Queries
- Use quoted symbols for field names: =(:= '_id id)=
- RADIANCE returns hash tables with string keys
- Primary key is "_id" internally, "id" in JSON responses
# Verbose mode
./test-server.sh -v
#+END_SRC
** Streaming Considerations
- MP3 files with spaces in names require playlist.m3u approach
- Liquidsoap fallback prevents stream silence
- Icecast2 mount points must match Liquidsoap configuration
** Test Coverage
- 25+ automated tests
- API endpoint validation
- HTML page rendering
- Static file serving
- JSON response format
- Authentication flows
* Future Enhancements
* Contributing
** Planned Features
- Playlist creation and management interface
- Now-playing status tracking and display
- Direct browser file uploads with progress
- Listener statistics and analytics
- Scheduled programming and automation
** Development Workflow
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Run test suite
5. Submit pull request
** Technical Improvements
- WebSocket integration for real-time updates
- Advanced audio processing options
- Multi-bitrate streaming support
- Mobile-responsive interface enhancements
** Community
- *IRC*: #asteroid.music on irc.libera.chat
- *Issues*: GitHub issue tracker
- *Discussions*: GitHub discussions
** Core Team
- Brian O'Reilly (Fade) - Project founder
- Glenn Thompson (glenneth) - Core developer
- Luis Pereira - UI/UX
* Troubleshooting
** Common Issues
- *No audio in stream*: Check Liquidsoap logs, verify MP3 files
- *Database errors*: Ensure proper field name quoting in queries
- *Network access*: Verify WSL IP and firewall settings
- *File upload issues*: Check permissions on music directories
** Docker Issues
#+BEGIN_SRC bash
# Check container status
docker compose ps
** Debugging
- Enable Liquidsoap debug logging: =settings.log.level := 4=
- Check Icecast admin interface for stream status
- Monitor RADIANCE logs for web server issues
- Verify database connectivity and collections
# View logs
docker compose logs icecast
docker compose logs liquidsoap
# Restart services
docker compose restart
#+END_SRC
** Stream Not Playing
- Verify containers are running
- Check music files exist in =docker/music/=
- Test stream URLs with curl
- Review Liquidsoap logs
** Database Issues
- Check Radiance DB file permissions
- Verify database collections exist
- Review application logs
For detailed troubleshooting, see documentation in =docs/= directory.
* License
This implementation maintains compatibility with the original Asteroid Radio project license while adding comprehensive streaming capabilities for internet radio broadcasting.
See LICENSE file for details.
* Acknowledgments
Built with:
- Common Lisp (SBCL)
- Radiance web framework
- Icecast2 streaming server
- Liquidsoap audio processing
- Docker containerization
Special thanks to all contributors and the Common Lisp community.
---
*Last Updated: 2025-10-26*

View File

@ -1,193 +0,0 @@
#+TITLE: Session Notes - Page Flow Feature Implementation
#+DATE: 2025-10-12
#+AUTHOR: Glenn
* Session Objective
Implement role-based page flow for Asteroid Radio application where:
- Admin users are redirected to ~/asteroid/admin~ upon login
- Regular users (listener, dj) are redirected to ~/asteroid/profile~ upon login
- User registration redirects to ~/asteroid/profile~ page
- Navigation links display conditionally based on authentication status and user role
* What Was Accomplished
** Core Feature: Role-Based Page Flow ✅
- Implemented login redirect logic based on user role
- Admin users → ~/asteroid/admin~ dashboard
- Regular users → ~/asteroid/profile~ page
- Registration flow → ~/asteroid/profile~ for new users
- Session persistence across page navigation
** User Management API Endpoints ✅
Converted user management endpoints to use Radiance's ~define-api~ standard:
- ~/api/asteroid/users~ - Get all users (admin only)
- ~/api/asteroid/user-stats~ - Get user statistics (admin only)
- ~/api/asteroid/users/create~ - Create new user (admin only)
** Profile Page API Endpoints ✅
Added new API endpoints for user profile functionality:
- ~/api/asteroid/user/profile~ - Get current user profile information
- ~/api/asteroid/user/listening-stats~ - Get user listening statistics (placeholder)
- ~/api/asteroid/user/recent-tracks~ - Get recently played tracks (placeholder)
- ~/api/asteroid/user/top-artists~ - Get top artists (placeholder)
** Authentication & Authorization Improvements ✅
- Fixed ~require-role~ function to properly handle API requests
- Added proper JSON error responses for authorization failures
- Improved password verification with debug logging
- Added ~reset-user-password~ function for admin use
** JavaScript API Response Handling ✅
Fixed all JavaScript files to properly handle Radiance's ~api-output~ wrapper format:
- Response structure: ~{status: 200, message: "Ok.", data: {...}}~
- Updated all fetch calls to extract ~result.data~ before processing
- Added fallback handling: ~const data = result.data || result~
* Files Modified
** Backend (Common Lisp)
*** ~asteroid.lisp~
- Added profile page API endpoints (~user/profile~, ~user/listening-stats~, ~user/recent-tracks~, ~user/top-artists~)
- All endpoints use ~define-api~ and ~api-output~ for proper JSON responses
- Added ~require-authentication~ checks for protected endpoints
*** ~auth-routes.lisp~
- Fixed user management API endpoints to properly use ~api-output~
- Updated ~/api/asteroid/users~ endpoint for proper JSON responses
- Updated ~/api/asteroid/user-stats~ endpoint for proper JSON responses
- Updated ~/api/asteroid/users/create~ endpoint for proper JSON responses
- Added proper error handling with HTTP status codes (400, 404, 500)
*** ~user-management.lisp~
- Modified ~require-role~ function to return ~nil~ for failed API authorization
- Removed problematic ~radiance:api-output~ calls
- Responsibility for JSON error responses moved to calling endpoints
- Added debug logging for authentication flow
** Frontend (JavaScript)
*** ~static/js/users.js~
- Fixed ~loadUserStats()~ to handle ~api-output~ wrapper
- Fixed ~loadUsers()~ to handle ~api-output~ wrapper
- Fixed ~createNewUser()~ to handle ~api-output~ wrapper
- Updated to properly extract ~result.data~ before processing
*** ~static/js/auth-ui.js~
- Fixed ~checkAuthStatus()~ to handle ~api-output~ wrapper
- Session persistence now working correctly across navigation
- Conditional nav links display properly based on auth status
*** ~static/js/profile.js~
- Fixed ~loadProfileData()~ to handle ~api-output~ wrapper
- Fixed ~loadListeningStats()~ to handle ~api-output~ wrapper
- Fixed ~loadRecentTracks()~ to handle ~api-output~ wrapper
- Fixed ~loadTopArtists()~ to handle ~api-output~ wrapper
- Added safe handling for empty arrays (no errors when no data)
- Used optional chaining (~?.~) for safer DOM queries
** Documentation
*** ~TODO.org~
- Marked "Page Flow" section as complete [2/2] ✅
- Updated notes to reflect working implementation
* Technical Details
** API Response Format
All API endpoints now return responses in this format:
#+BEGIN_SRC json
{
"status": 200,
"message": "Ok.",
"data": {
"status": "success",
"users": [...]
}
}
#+END_SRC
JavaScript must extract the ~data~ property before processing.
** Authentication Flow
1. User submits login form
2. ~authenticate-user~ validates credentials
3. Session field "user-id" is set
4. User role is checked
5. Redirect based on role:
- ~:admin~~/asteroid/admin~
- ~:listener~ or ~:dj~~/asteroid/profile~
** Authorization Pattern
#+BEGIN_SRC lisp
(define-api asteroid/endpoint () ()
"API endpoint description"
(require-role :admin) ; or (require-authentication)
(handler-case
(let ((data (get-some-data)))
(api-output `(("status" . "success")
("data" . ,data))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error: ~a" e)))
:status 500))))
#+END_SRC
* Testing Results
** Successful Tests
- ✅ Admin login redirects to ~/asteroid/admin~
- ✅ Regular user login redirects to ~/asteroid/profile~
- ✅ User registration redirects to ~/asteroid/profile~
- ✅ Session persists across page navigation
- ✅ Nav links display correctly based on role (Profile/Admin/Logout vs Login/Register)
- ✅ User statistics display correctly (3 users, 1 admin, 0 DJs)
- ✅ "View All Users" table displays all users
- ✅ "Create New User" functionality working
- ✅ Profile page loads without errors
- ✅ All API endpoints return proper JSON responses
** Test User Created
- Username: ~testuser~
- Email: ~test@asteroid123~
- Role: ~listener~
- Status: Active
* Git Commits
Three clean commits on ~feature/user-page-flow~ branch:
1. ~c6ac876~ - feat: Implement role-based page flow and user management APIs
2. ~0b5bde8~ - fix: Complete UI fixes for page flow feature
3. ~10bd8b4~ - docs: Mark Page Flow feature as complete in TODO
* Known Limitations
** Profile Page Data
- Listening statistics return placeholder data (all zeros)
- Recent tracks return empty array
- Top artists return empty array
- These are ready for future implementation when listening history tracking is added
** Future Enhancements
- Implement actual listening history tracking
- Add user profile editing functionality
- Add user avatar/photo support
- Implement password reset via email
* Notes for Integration
** For Fade (PostgreSQL Migration)
- User management API endpoints are now standardized with ~define-api~
- All endpoints use ~api-output~ for consistent JSON responses
- Session handling is working correctly
- Ready for database migration - just need to update ~find-user-by-id~, ~get-all-users~, etc.
** For easilokkx (UI Work)
- All JavaScript files now properly handle ~api-output~ wrapper format
- Pattern: ~const data = result.data || result;~
- Profile page has placeholder API endpoints ready for real data
- Auth UI system working correctly for conditional display
* Branch Status
- Branch: ~feature/user-page-flow~
- Status: Complete and tested
- Ready for: Pull Request to upstream/main
- Conflicts: None expected (isolated feature work)

View File

@ -33,6 +33,7 @@
:pathname "./"
:components ((:file "app-utils")
(:file "module")
(:file "conditions")
(:file "database")
(:file "template-utils")
(:file "stream-media")

View File

@ -37,21 +37,18 @@
(define-api asteroid/admin/scan-library () ()
"API endpoint to scan music library"
(require-role :admin)
(handler-case
(with-error-handling
(let ((tracks-added (scan-music-library)))
(api-output `(("status" . "success")
("message" . "Library scan completed")
("tracks-added" . ,tracks-added))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Scan failed: ~a" e)))
:status 500))))
("tracks-added" . ,tracks-added))))))
(define-api asteroid/admin/tracks () ()
"API endpoint to view all tracks in database"
(require-authentication)
(handler-case
(let ((tracks (db:select "tracks" (db:query :all))))
(with-error-handling
(let ((tracks (with-db-error-handling "select"
(db:select "tracks" (db:query :all)))))
(api-output `(("status" . "success")
("tracks" . ,(mapcar (lambda (track)
`(("id" . ,(gethash "_id" track))
@ -61,17 +58,13 @@
("duration" . ,(first (gethash "duration" track)))
("format" . ,(first (gethash "format" track)))
("bitrate" . ,(first (gethash "bitrate" track)))))
tracks)))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error retrieving tracks: ~a" e)))
:status 500))))
tracks)))))))
;; Playlist API endpoints
(define-api asteroid/playlists () ()
"Get all playlists for current user"
(require-authentication)
(handler-case
(with-error-handling
(let* ((user (get-current-user))
(user-id-raw (gethash "_id" user))
(user-id (if (listp user-id-raw) (first user-id-raw) user-id-raw))
@ -98,16 +91,12 @@
("description" . ,(if (listp desc-val) (first desc-val) desc-val))
("track-count" . ,track-count)
("created-date" . ,(if (listp created-val) (first created-val) created-val))))))
playlists)))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error retrieving playlists: ~a" e)))
:status 500))))
playlists)))))))
(define-api asteroid/playlists/create (name &optional description) ()
"Create a new playlist"
(require-authentication)
(handler-case
(with-error-handling
(let* ((user (get-current-user))
(user-id-raw (gethash "_id" user))
(user-id (if (listp user-id-raw) (first user-id-raw) user-id-raw)))
@ -115,32 +104,22 @@
(if (string= "true" (post/get "browser"))
(redirect "/asteroid/")
(api-output `(("status" . "success")
("message" . "Playlist created successfully")))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error creating playlist: ~a" e)))
:status 500))))
("message" . "Playlist created successfully")))))))
(define-api asteroid/playlists/add-track (playlist-id track-id) ()
"Add a track to a playlist"
(require-authentication)
(handler-case
(with-error-handling
(let ((pl-id (parse-integer playlist-id :junk-allowed t))
(tr-id (parse-integer track-id :junk-allowed t)))
(add-track-to-playlist pl-id tr-id)
(if (string= "true" (post/get "browser"))
(redirect "/asteroid/")
(api-output `(("status" . "success")
("message" . "Track added to playlist")))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error adding track: ~a" e)))
:status 500))))
("message" . "Track added to playlist"))))))
(define-api asteroid/playlists/get (playlist-id) ()
"Get playlist details with tracks"
(require-authentication)
(handler-case
(with-error-handling
(let* ((id (parse-integer playlist-id :junk-allowed t))
(playlist (get-playlist-by-id id)))
(if playlist
@ -164,18 +143,15 @@
valid-tracks)))))))
(api-output `(("status" . "error")
("message" . "Playlist not found"))
:status 404)))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error retrieving playlist: ~a" e)))
:status 500))))
:status 404)))))
;; API endpoint to get all tracks (for web player)
(define-api asteroid/tracks () ()
"Get all tracks for web player"
(require-authentication)
(handler-case
(let ((tracks (db:select "tracks" (db:query :all))))
(with-error-handling
(let ((tracks (with-db-error-handling "select"
(db:select "tracks" (db:query :all)))))
(api-output `(("status" . "success")
("tracks" . ,(mapcar (lambda (track)
`(("id" . ,(gethash "_id" track))
@ -184,111 +160,77 @@
("album" . ,(gethash "album" track))
("duration" . ,(gethash "duration" track))
("format" . ,(gethash "format" track))))
tracks)))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error retrieving tracks: ~a" e)))
:status 500))))
tracks)))))))
;; Stream Control API Endpoints
(define-api asteroid/stream/queue () ()
"Get the current stream queue"
(require-role :admin)
(handler-case
(with-error-handling
(let ((queue (get-stream-queue)))
(api-output `(("status" . "success")
("queue" . ,(mapcar (lambda (track-id)
(let ((track (get-track-by-id track-id)))
(when track
`(("id" . ,track-id)
("title" . ,(gethash "title" track))
("artist" . ,(gethash "artist" track))
("album" . ,(gethash "album" track))))))
queue)))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error getting queue: ~a" e)))
:status 500))))
("album" . ,(gethash "album" track)))))
queue)))))))
(define-api asteroid/stream/queue/add (track-id &optional (position "end")) ()
"Add a track to the stream queue"
(require-role :admin)
(handler-case
(with-error-handling
(let ((tr-id (parse-integer track-id :junk-allowed t))
(pos (if (string= position "next") :next :end)))
(add-to-stream-queue tr-id pos)
(api-output `(("status" . "success")
("message" . "Track added to stream queue"))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error adding to queue: ~a" e)))
:status 500))))
("message" . "Track added to stream queue"))))))
(define-api asteroid/stream/queue/remove (track-id) ()
"Remove a track from the stream queue"
(require-role :admin)
(handler-case
(with-error-handling
(let ((tr-id (parse-integer track-id :junk-allowed t)))
(remove-from-stream-queue tr-id)
(api-output `(("status" . "success")
("message" . "Track removed from stream queue"))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error removing from queue: ~a" e)))
:status 500))))
("message" . "Track removed from stream queue"))))))
(define-api asteroid/stream/queue/clear () ()
"Clear the entire stream queue"
(require-role :admin)
(handler-case
(progn
(with-error-handling
(clear-stream-queue)
(api-output `(("status" . "success")
("message" . "Stream queue cleared"))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error clearing queue: ~a" e)))
:status 500))))
("message" . "Stream queue cleared")))))
(define-api asteroid/stream/queue/add-playlist (playlist-id) ()
"Add all tracks from a playlist to the stream queue"
(require-role :admin)
(handler-case
(with-error-handling
(let ((pl-id (parse-integer playlist-id :junk-allowed t)))
(add-playlist-to-stream-queue pl-id)
(api-output `(("status" . "success")
("message" . "Playlist added to stream queue"))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error adding playlist to queue: ~a" e)))
:status 500))))
("message" . "Playlist added to stream queue"))))))
(define-api asteroid/stream/queue/reorder (track-ids) ()
"Reorder the stream queue (expects comma-separated track IDs)"
(require-role :admin)
(handler-case
(with-error-handling
(let ((ids (mapcar (lambda (id-str) (parse-integer id-str :junk-allowed t))
(cl-ppcre:split "," track-ids))))
(reorder-stream-queue ids)
(api-output `(("status" . "success")
("message" . "Stream queue reordered"))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error reordering queue: ~a" e)))
:status 500))))
("message" . "Stream queue reordered"))))))
(define-api asteroid/stream/queue/load-m3u () ()
"Load stream queue from stream-queue.m3u file"
(require-role :admin)
(handler-case
(with-error-handling
(let ((count (load-queue-from-m3u-file)))
(api-output `(("status" . "success")
("message" . "Queue loaded from M3U file")
("count" . ,count))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error loading from M3U: ~a" e)))
:status 500))))
("count" . ,count))))))
(defun get-track-by-id (track-id)
"Get a track by its ID - handles type mismatches"
@ -315,15 +257,19 @@
(define-page stream-track #@"/tracks/(.*)/stream" (:uri-groups (track-id))
"Stream audio file by track ID"
(handler-case
(with-error-handling
(let* ((id (parse-integer track-id))
(track (get-track-by-id id)))
(if track
(unless track
(signal-not-found "track" id))
(let* ((file-path (first (gethash "file-path" track)))
(format (first (gethash "format" track)))
(file (probe-file file-path)))
(if file
(progn
(unless file
(error 'not-found-error
:message "Audio file not found on disk"
:resource-type "file"
:resource-id file-path))
;; Set appropriate headers for audio streaming
(setf (radiance:header "Content-Type") (get-mime-type-for-format format))
(setf (radiance:header "Accept-Ranges") "bytes")
@ -332,22 +278,7 @@
(db:update "tracks" (db:query (:= '_id id))
`(("play-count" ,(1+ (first (gethash "play-count" track))))))
;; Return file contents
(alexandria:read-file-into-byte-vector file))
(progn
(setf (radiance:header "Content-Type") "application/json")
(cl-json:encode-json-to-string
`(("status" . "error")
("message" . "Audio file not found on disk"))))))
(progn
(setf (radiance:header "Content-Type") "application/json")
(cl-json:encode-json-to-string
`(("status" . "error")
("message" . "Track not found"))))))
(error (e)
(setf (radiance:header "Content-Type") "application/json")
(cl-json:encode-json-to-string
`(("status" . "error")
("message" . ,(format nil "Streaming error: ~a" e)))))))
(alexandria:read-file-into-byte-vector file)))))
;; Player state management
(defvar *current-track* nil "Currently playing track")
@ -390,11 +321,11 @@
;; Player control API endpoints
(define-api asteroid/player/play (track-id) ()
"Start playing a track by ID"
(handler-case
(with-error-handling
(let* ((id (parse-integer track-id))
(track (get-track-by-id id)))
(if track
(progn
(unless track
(signal-not-found "track" id))
(setf *current-track* id)
(setf *player-state* :playing)
(setf *current-position* 0)
@ -403,14 +334,7 @@
("track" . (("id" . ,id)
("title" . ,(first (gethash "title" track)))
("artist" . ,(first (gethash "artist" track)))))
("player" . ,(get-player-status)))))
(api-output `(("status" . "error")
("message" . "Track not found"))
:status 404)))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Play error: ~a" e)))
:status 500))))
("player" . ,(get-player-status)))))))
(define-api asteroid/player/pause () ()
"Pause current playback"
@ -519,40 +443,34 @@
;; Front page - regular view by default
(define-page front-page #@"/" ()
"Main front page"
(let ((template-path (merge-pathnames "template/front-page.chtml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "front-page")
:title "🎵 ASTEROID RADIO 🎵"
:station-name "🎵 ASTEROID RADIO 🎵"
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
:listeners "0"
:stream-quality "128kbps MP3"
:stream-base-url *stream-base-url*
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
:default-stream-encoding "audio/aac"
:default-stream-encoding-desc "AAC 96kbps Stereo"
:now-playing-artist "The Void"
:now-playing-track "Silence"
:now-playing-album "Startup Sounds"
:now-playing-duration "∞")))
:now-playing-duration "∞"))
;; Frameset wrapper for persistent player mode
(define-page frameset-wrapper #@"/frameset" ()
"Frameset wrapper with persistent audio player"
(let ((template-path (merge-pathnames "template/frameset-wrapper.chtml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
:title "🎵 ASTEROID RADIO 🎵")))
(load-template "frameset-wrapper")
:title "🎵 ASTEROID RADIO 🎵"))
;; Content frame - front page content without player
(define-page front-page-content #@"/content" ()
"Front page content (displayed in content frame)"
(let ((template-path (merge-pathnames "template/front-page-content.chtml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "front-page-content")
:title "🎵 ASTEROID RADIO 🎵"
:station-name "🎵 ASTEROID RADIO 🎵"
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
@ -562,29 +480,27 @@
:now-playing-artist "The Void"
:now-playing-track "Silence"
:now-playing-album "Startup Sounds"
:now-playing-duration "∞")))
:now-playing-duration "∞"))
;; Persistent audio player frame (bottom frame)
(define-page audio-player-frame #@"/audio-player-frame" ()
"Persistent audio player frame (bottom of page)"
(let ((template-path (merge-pathnames "template/audio-player-frame.chtml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "audio-player-frame")
:stream-base-url *stream-base-url*
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
:default-stream-encoding "audio/aac")))
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
:default-stream-encoding "audio/aac"))
;; Configure static file serving for other files
(define-page static #@"/static/(.*)" (:uri-groups (path))
(serve-file (merge-pathnames (concatenate 'string "static/" path)
(serve-file (merge-pathnames (format nil "static/~a" 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 (concatenate 'string *stream-base-url* "/status-json.xsl")
(let ((response (drakma:http-request (format nil "~a/status-json.xsl" *stream-base-url*)
:want-stream nil
:connection-timeout 2)))
(if response "🟢 Running" "🔴 Not Running"))
@ -606,13 +522,11 @@
(define-page admin #@"/admin" ()
"Admin dashboard"
(require-authentication)
(let ((template-path (merge-pathnames "template/admin.chtml"
(asdf:system-source-directory :asteroid)))
(track-count (handler-case
(let ((track-count (handler-case
(length (db:select "tracks" (db:query :all)))
(error () 0))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "admin")
:title "🎵 ASTEROID RADIO - Admin Dashboard"
:server-status "🟢 Running"
:database-status (handler-case
@ -623,26 +537,22 @@
:track-count (format nil "~d" track-count)
:library-path "/home/glenn/Projects/Code/asteroid/music/library/"
:stream-base-url *stream-base-url*
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac"))))
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*))))
;; User Management page (requires authentication)
(define-page users-management #@"/admin/user" ()
"User Management dashboard"
(require-authentication)
(let ((template-path (merge-pathnames "template/users.chtml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
:title "🎵 ASTEROID RADIO - User Management")))
(load-template "users")
:title "🎵 ASTEROID RADIO - User Management"))
;; User Profile page (requires authentication)
(define-page user-profile #@"/profile" ()
"User profile page"
(require-authentication)
(let ((template-path (merge-pathnames "template/profile.chtml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "profile")
:title "🎧 admin - Profile | Asteroid Radio"
:username "admin"
:user-role "admin"
@ -673,7 +583,7 @@
:top-artist-4 ""
:top-artist-4-plays ""
:top-artist-5 ""
:top-artist-5-plays "")))
:top-artist-5-plays ""))
;; Helper functions for profile page - TEMPORARILY COMMENTED OUT
#|
@ -710,7 +620,7 @@
(require-authentication)
(let* ((current-user (auth:current-user))
(username (gethash "username" current-user))
(template-path (merge-pathnames "template/profile.chtml"
(template-path (merge-pathnames "template/profile.ctml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
@ -750,7 +660,7 @@
;; Auth status API endpoint
(define-api asteroid/auth-status () ()
"Check if user is logged in and their role"
(handler-case
(with-error-handling
(let* ((user-id (session:field "user-id"))
(user (when user-id (find-user-by-id user-id))))
(api-output `(("loggedIn" . ,(if user t nil))
@ -758,18 +668,13 @@
("username" . ,(if user
(let ((username (gethash "username" user)))
(if (listp username) (first username) username))
nil)))))
(error (e)
(api-output `(("loggedIn" . nil)
("isAdmin" . nil)
("error" . ,(format nil "~a" e)))
:status 500))))
nil)))))))
;; User profile API endpoints
(define-api asteroid/user/profile () ()
"Get current user profile information"
(require-authentication)
(handler-case
(with-error-handling
(let* ((user-id (session:field "user-id"))
(user (find-user-by-id user-id)))
(if user
@ -779,13 +684,7 @@
("role" . ,(first (gethash "role" user)))
("created_at" . ,(first (gethash "created-date" user)))
("last_active" . ,(first (gethash "last-login" user)))))))
(api-output `(("status" . "error")
("message" . "User not found"))
:status 404)))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error loading profile: ~a" e)))
:status 500))))
(signal-not-found "user" user-id)))))
(define-api asteroid/user/listening-stats () ()
"Get user listening statistics"
@ -862,40 +761,34 @@
:success-message ""))))
(define-page player #@"/player" ()
(let ((template-path (merge-pathnames "template/player.chtml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "player")
:title "Asteroid Radio - Web Player"
:stream-base-url *stream-base-url*
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
:bitrate "128kbps MP3"
:now-playing-artist "The Void"
:now-playing-track "Silence"
:now-playing-album "Startup Sounds"
:player-status "Stopped")))
:player-status "Stopped"))
;; Player content frame (for frameset mode)
(define-page player-content #@"/player-content" ()
"Player page content (displayed in content frame)"
(let ((template-path (merge-pathnames "template/player-content.chtml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "player-content")
:title "Asteroid Radio - Web Player"
:stream-base-url *stream-base-url*
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
:default-stream-encoding "audio/aac")))
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
:default-stream-encoding "audio/aac"))
(define-page popout-player #@"/popout-player" ()
"Pop-out player window"
(let ((template-path (merge-pathnames "template/popout-player.chtml"
(asdf:system-source-directory :asteroid))))
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "popout-player")
:stream-base-url *stream-base-url*
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
:default-stream-encoding "audio/aac")))
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
:default-stream-encoding "audio/aac"))
(define-api asteroid/status () ()
"Get server status"
@ -907,14 +800,14 @@
("artist" . "The Void")
("album" . "Startup Sounds")))
("listeners" . 0)
("stream-url" . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
("stream-url" . ,(format nil "~a/asteroid.mp3" *stream-base-url*))
("stream-status" . "live"))))
;; Live stream status from Icecast
(define-api asteroid/icecast-status () ()
"Get live status from Icecast server"
(handler-case
(let* ((icecast-url (concatenate 'string *stream-base-url* "/admin/stats.xml"))
(with-error-handling
(let* ((icecast-url (format nil "~a/admin/stats.xml" *stream-base-url*))
(response (drakma:http-request icecast-url
:want-stream nil
:basic-authorization '("admin" "asteroid_admin_2024"))))
@ -936,7 +829,7 @@
(listeners (if listenersp (cl-ppcre:regex-replace-all ".*<listeners>(.*?)</listeners>.*" source-section "\\1") "0")))
;; Return JSON in format expected by frontend
(api-output
`(("icestats" . (("source" . (("listenurl" . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
`(("icestats" . (("source" . (("listenurl" . ,(format nil "~a/asteroid.mp3" *stream-base-url*))
("title" . ,title)
("listeners" . ,(parse-integer listeners :junk-allowed t)))))))))
;; No source found, return empty
@ -944,11 +837,7 @@
`(("icestats" . (("source" . nil))))))))
(api-output
`(("error" . "Could not connect to Icecast server"))
:status 503)))
(error (e)
(api-output
`(("error" . ,(format nil "Icecast connection failed: ~a" e)))
:status 500))))
:status 503)))))
;; RADIANCE server management functions

185
conditions.lisp Normal file
View File

@ -0,0 +1,185 @@
;;;; conditions.lisp - Custom error conditions for Asteroid Radio
;;;; Provides a hierarchy of error conditions for better error handling and debugging
(in-package :asteroid)
;;; Base Condition Hierarchy
(define-condition asteroid-error (error)
((message
:initarg :message
:reader error-message
:documentation "Human-readable error message"))
(:documentation "Base condition for all Asteroid-specific errors")
(:report (lambda (condition stream)
(format stream "Asteroid Error: ~a" (error-message condition)))))
;;; Specific Error Types
(define-condition database-error (asteroid-error)
((operation
:initarg :operation
:reader error-operation
:initform nil
:documentation "Database operation that failed (e.g., 'select', 'insert')"))
(:documentation "Signaled when a database operation fails")
(:report (lambda (condition stream)
(format stream "Database Error~@[ during ~a~]: ~a"
(error-operation condition)
(error-message condition)))))
(define-condition authentication-error (asteroid-error)
((user
:initarg :user
:reader error-user
:initform nil
:documentation "Username or user ID that failed authentication"))
(:documentation "Signaled when authentication fails")
(:report (lambda (condition stream)
(format stream "Authentication Error~@[ for user ~a~]: ~a"
(error-user condition)
(error-message condition)))))
(define-condition authorization-error (asteroid-error)
((required-role
:initarg :required-role
:reader error-required-role
:initform nil
:documentation "Role required for the operation"))
(:documentation "Signaled when user lacks required permissions")
(:report (lambda (condition stream)
(format stream "Authorization Error~@[ (requires ~a)~]: ~a"
(error-required-role condition)
(error-message condition)))))
(define-condition not-found-error (asteroid-error)
((resource-type
:initarg :resource-type
:reader error-resource-type
:initform nil
:documentation "Type of resource that wasn't found (e.g., 'track', 'user')")
(resource-id
:initarg :resource-id
:reader error-resource-id
:initform nil
:documentation "ID of the resource that wasn't found"))
(:documentation "Signaled when a requested resource doesn't exist")
(:report (lambda (condition stream)
(format stream "Not Found~@[ (~a~@[ ~a~])~]: ~a"
(error-resource-type condition)
(error-resource-id condition)
(error-message condition)))))
(define-condition validation-error (asteroid-error)
((field
:initarg :field
:reader error-field
:initform nil
:documentation "Field that failed validation"))
(:documentation "Signaled when input validation fails")
(:report (lambda (condition stream)
(format stream "Validation Error~@[ in field ~a~]: ~a"
(error-field condition)
(error-message condition)))))
(define-condition asteroid-stream-error (asteroid-error)
((stream-type
:initarg :stream-type
:reader error-stream-type
:initform nil
:documentation "Type of stream (e.g., 'icecast', 'liquidsoap')"))
(:documentation "Signaled when stream operations fail")
(:report (lambda (condition stream)
(format stream "Stream Error~@[ (~a)~]: ~a"
(error-stream-type condition)
(error-message condition)))))
;;; Error Handling Macros
(defmacro with-error-handling (&body body)
"Wrap API endpoint code with standard error handling.
Catches specific Asteroid errors and returns appropriate HTTP status codes.
Usage:
(define-api my-endpoint () ()
(with-error-handling
(do-something-that-might-fail)))"
`(handler-case
(progn ,@body)
(not-found-error (e)
(api-output `(("status" . "error")
("message" . ,(error-message e)))
:status 404))
(authentication-error (e)
(api-output `(("status" . "error")
("message" . ,(error-message e)))
:status 401))
(authorization-error (e)
(api-output `(("status" . "error")
("message" . ,(error-message e)))
:status 403))
(validation-error (e)
(api-output `(("status" . "error")
("message" . ,(error-message e)))
:status 400))
(database-error (e)
(format t "Database error: ~a~%" e)
(api-output `(("status" . "error")
("message" . "Database operation failed"))
:status 500))
(asteroid-stream-error (e)
(format t "Stream error: ~a~%" e)
(api-output `(("status" . "error")
("message" . "Stream operation failed"))
:status 500))
(asteroid-error (e)
(format t "Asteroid error: ~a~%" e)
(api-output `(("status" . "error")
("message" . ,(error-message e)))
:status 500))
(error (e)
(format t "Unexpected error: ~a~%" e)
(api-output `(("status" . "error")
("message" . "An unexpected error occurred"))
:status 500))))
(defmacro with-db-error-handling (operation &body body)
"Wrap database operations with error handling.
Automatically converts database errors to database-error conditions.
Usage:
(with-db-error-handling \"select\"
(db:select 'tracks (db:query :all)))"
`(handler-case
(progn ,@body)
(error (e)
(error 'database-error
:message (format nil "~a" e)
:operation ,operation))))
;;; Helper Functions
(defun signal-not-found (resource-type resource-id)
"Signal a not-found-error with the given resource information."
(error 'not-found-error
:message (format nil "~a not found" resource-type)
:resource-type resource-type
:resource-id resource-id))
(defun signal-validation-error (field message)
"Signal a validation-error for the given field."
(error 'validation-error
:message message
:field field))
(defun signal-auth-error (user message)
"Signal an authentication-error for the given user."
(error 'authentication-error
:message message
:user user))
(defun signal-authz-error (required-role message)
"Signal an authorization-error with the required role."
(error 'authorization-error
:message message
:required-role required-role))

View File

@ -0,0 +1,14 @@
services:
asteroid:
build:
context: ../
dockerfile: Dockerfile.asteroid
image: asteroid/app
container_name: asteroid
environment:
- ASTEROID_STREAM_URL=${ASTEROID_STREAM_URL:-http://localhost:8000}
volumes:
- ${MUSIC_LIBRARY:-../music/library}:/app/music/library:ro
- ${QUEUE_PLAYLIST:-../stream-queue.m3u}:/app/stream-queue.m3u
network_mode: host
restart: unless-stopped

View File

@ -24,9 +24,9 @@ services:
depends_on:
- icecast
volumes:
- ../music/library:/app/music:ro
- ${MUSIC_LIBRARY:-../music/library}:/app/music:ro
- ./asteroid-radio-docker.liq:/app/asteroid-radio.liq:ro
- ../stream-queue.m3u:/app/stream-queue.m3u:ro
- ${QUEUE_PLAYLIST:-../stream-queue.m3u}:/app/stream-queue.m3u:ro
restart: unless-stopped
networks:
- asteroid-network

View File

@ -0,0 +1,20 @@
; meta (:version 1.0 :package "RADIANCE-CORE")
((:interfaces (:admin . "r-simple-admin") (:auth . "r-simple-auth")
(:ban . "r-simple-ban") (:cache . "r-simple-cache")
(:data-model . "r-simple-model") (:database . "i-lambdalite")
(:relational-database . "i-sqlite") (:logger . "i-verbose")
(:mail . "i-smtp") (:profile . "r-simple-profile") (:rate . "r-simple-rate")
(:server . "i-hunchentoot") (:session . "r-simple-sessions")
(:user . "r-simple-users"))
(:versions
. [hash-table equal ("radiance-core" :|2.2.0|) ("i-hunchentoot" :|1.1.0|)
("asteroid" :|0.0.0|) ("i-log4cl" :|1.0.0|) ("r-clip" :|1.0.0|)
("r-data-model" :|1.0.1|) ("i-lambdalite" :|1.0.0|)
("r-simple-users" :|1.0.1|)
("r-simple-errors" :|1.0.0|) ("i-verbose" :|1.0.0|)
("r-simple-auth" :|1.0.0|) ("r-simple-sessions" :|1.0.1|)
("r-ratify" :|1.0.0|) ("r-simple-rate" :|1.0.0|)
("r-simple-profile" :|1.0.0|)])
(:domains "radiance" "localhost")
(:startup :r-simple-errors :r-simple-sessions) (:routes)
(:debugger . :if-swank-connected))

View File

@ -1,6 +1,6 @@
#+TITLE: Asteroid Radio - API Endpoints Reference
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-10
#+DATE: 2025-10-26
* Overview
@ -423,51 +423,6 @@ curl -X POST http://localhost:8080/api/asteroid/playlists/create \
When =browser=true= is passed, endpoints will redirect to appropriate pages instead of returning JSON.
* Integration Examples
** JavaScript/Fetch API
#+BEGIN_SRC javascript
// Get tracks
fetch('/api/asteroid/tracks')
.then(response => response.json())
.then(data => {
console.log('Tracks:', data.tracks);
});
// Play a track
fetch('/api/asteroid/player/play', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'track-id=123'
})
.then(response => response.json())
.then(data => {
console.log('Now playing:', data.player.currentTrack);
});
#+END_SRC
** Python
#+BEGIN_SRC python
import requests
# Get server status
response = requests.get('http://localhost:8080/api/asteroid/status')
print(response.json())
# Create playlist (with session)
session = requests.Session()
# ... login first ...
response = session.post(
'http://localhost:8080/api/asteroid/playlists/create',
data={'name': 'My Playlist', 'description': 'Test'}
)
print(response.json())
#+END_SRC
* Rate Limiting
API endpoints implement rate limiting to prevent abuse. Excessive requests may result in temporary blocking.

View File

@ -1,6 +1,6 @@
#+TITLE: Asteroid Radio - Interface Reference
#+TITLE: Asteroid Radio - API Reference
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-10
#+DATE: 2025-10-26
* Current Interfaces

View File

@ -1,9 +1,13 @@
#+TITLE: Asteroid Radio - Development Guide
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-10
#+DATE: 2025-10-26
* Development Setup
#+BEGIN_QUOTE
*Note on Package Managers*: Examples use =apt= (Debian/Ubuntu). Replace with your distribution's package manager (=dnf=, =pacman=, =zypper=, =apk=, etc.).
#+END_QUOTE
** Prerequisites
*** System Dependencies
@ -68,7 +72,7 @@ sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --quit
*** Clone Repository
#+BEGIN_SRC bash
git clone <repository-url>
git clone https://github.com/fade/asteroid.git
cd asteroid
#+END_SRC
@ -125,9 +129,9 @@ sbcl --eval "(ql:quickload :asteroid)" --eval "(asteroid:start-server)"
** Music Library Management
*** Directory Structure
The music directory structure is:
The music directory is located directly under the asteroid root directory:
#+BEGIN_SRC
asteroid/docker/music/ # Host directory (mounted to containers)
asteroid/music/ # Music directory (can be symlink)
├── artist1/
│ ├── album1/
│ │ ├── track1.mp3
@ -138,6 +142,11 @@ asteroid/docker/music/ # Host directory (mounted to containers)
└── single.wav
#+END_SRC
The =music/= directory can be:
- A regular directory with music files
- A symlink to your actual music collection
- Multiple subdirectories or symlinks within it
*** Recursive Scanning Capabilities
The Asteroid application includes built-in recursive directory scanning:
- *Function*: =scan-music-library= in =stream-media.lisp=
@ -149,16 +158,21 @@ The Asteroid application includes built-in recursive directory scanning:
*** Adding Music to Development Environment
#+BEGIN_SRC bash
# Option 1: Copy music files directly
cp -r /path/to/your/music/* docker/music/
cp -r /path/to/your/music/* music/
# Option 2: Mount remote directory (for large collections)
# Option 2: Symlink entire music directory
ln -s /path/to/existing/music music
# Option 3: Symlink subdirectories within music/
mkdir -p music
ln -s /path/to/collection1 music/collection1
ln -s /path/to/collection2 music/collection2
# Option 4: 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/api/asteroid/admin/scan-library
#+END_SRC

View File

@ -1,11 +1,15 @@
#+TITLE: Asteroid Radio - Docker Streaming Setup
#+AUTHOR: Docker Team
#+DATE: 2025-10-03
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-26
* 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.
#+BEGIN_QUOTE
*Note on Package Managers*: Examples use =apt= (Debian/Ubuntu). Replace with your distribution's package manager (=dnf=, =pacman=, =zypper=, =apk=, etc.).
#+END_QUOTE
* Architecture
** Container Stack
@ -37,7 +41,7 @@ sudo usermod -a -G docker $USER
** One-Command Setup
#+BEGIN_SRC bash
# Clone and start
git clone <repository-url> asteroid-radio
git clone https://github.com/fade/asteroid asteroid-radio
cd asteroid-radio/docker
docker compose up -d
#+END_SRC

View File

@ -1,11 +1,19 @@
#+TITLE: Asteroid Radio - Installation Guide
#+AUTHOR: Installation Team
#+DATE: 2025-10-03
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-26
* 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.
#+BEGIN_QUOTE
*Note on Package Managers*: Examples in this guide use =apt= (Debian/Ubuntu). Replace with your distribution's package manager:
- Fedora/RHEL: =dnf= or =yum=
- Arch Linux: =pacman=
- openSUSE: =zypper=
- Alpine: =apk=
#+END_QUOTE
* Quick Start (Docker - Recommended)
** Prerequisites Check
@ -18,8 +26,8 @@ docker info
** One-Command Setup
#+BEGIN_SRC bash
# Clone and setup (replace with actual repository URL)
git clone <repository-url> asteroid-radio
# Clone and setup
git clone https://github.com/fade/asteroid.git asteroid-radio
cd asteroid-radio/docker
docker compose up -d
#+END_SRC
@ -201,8 +209,8 @@ sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --eval "(ql:a
*** Step 5: Clone and Setup Project
#+BEGIN_SRC bash
# Clone repository (replace with actual URL)
git clone <repository-url> /opt/asteroid-radio
# Clone repository
git clone https://github.com/fade/asteroid /opt/asteroid-radio
cd /opt/asteroid-radio
# Create required directories
@ -380,7 +388,11 @@ sudo systemctl reload nginx
* Docker Management
** Container Management
** Stream Services
The stream services can be managed using docker from inside the =docker= folder on this repository.
*** Container Management
#+BEGIN_SRC bash
# Start services
docker compose up -d
@ -395,11 +407,42 @@ docker compose logs -f
docker compose restart
#+END_SRC
** Docker Configuration
*** 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
- **Music Volume**: Mounted to the =./music/library= directory (can also be set with the =MUSIC_LIBRARY= environment variable)
- *Queue Playlist*: Mounted to the =./stream-queue.m3u= file (can also be set with the =QUEUE_PLAYLIST= environment variable)
** Asteroid Radio Application
The asteroid radio application can also be served and managed using docker from inside the =docker= folder on this repository.
*** Container Management
#+BEGIN_SRC bash
# Build service
docker compose -f docker-compose.asteroid.yml build
# Start service
docker compose -f docker-compose.asteroid.yml up -d
# Stop service
docker compose -f docker-compose.asteroid.yml down
# View logs
docker compose -f docker-compose.asteroid.yml logs -f
# Restart service
docker compose -f docker-compose.asteroid.yml restart
#+END_SRC
*** Docker Configuration
See =docker/docker-compose.asteroid.yml= for complete Docker setup, which includes:
- Buils the application using the current cloned branch for the repository
- Uses the host network for easy access to the stream endpoint
- *Stream endpoint* mapped to =http://localhost:8000= (can also be set with the =ASTEROID_STREAM_URL= environment variable)
- **Music Volume**: Mounted to the =./music/library= directory (can also be set with the =MUSIC_LIBRARY= environment variable)
- *Queue Playlist*: Mounted to the =./stream-queue.m3u= file (can also be set with the =QUEUE_PLAYLIST= environment variable)
* Initial Configuration
@ -537,7 +580,7 @@ chmod +x ~/asteroid-radio/health-check.sh
- Test stream connectivity from different networks
** Getting Support
- Check project documentation and FAQ
- Check project documentation
- Review system logs for error messages
- Submit issues with detailed system information
- Join our IRC chat room: **#asteroid.music** on **irc.libera.chat**

View File

@ -1,6 +1,6 @@
#+TITLE: Playlist System - Complete (MVP)
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-04
#+DATE: 2025-10-26
* Overview

View File

@ -1,6 +1,6 @@
#+TITLE: PostgreSQL Setup for Asteroid Radio
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-04
#+DATE: 2025-10-26
* Overview

406
docs/PROJECT-HISTORY.org Normal file
View File

@ -0,0 +1,406 @@
#+TITLE: Asteroid Radio - Project Development History
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-26
#+DESCRIPTION: Comprehensive history of the Asteroid Radio project from inception to present
* Project Overview
Asteroid Radio is a web-based internet radio station built with Common Lisp, featuring a hacker-themed terminal aesthetic. The project combines the Radiance web framework with Icecast/Liquidsoap streaming infrastructure to create a full-featured music streaming platform.
** Technology Stack
- *Backend*: Common Lisp (SBCL), Radiance web framework
- *Streaming*: Icecast2, Liquidsoap
- *Database*: PostgreSQL (configured, ready for migration)
- *Frontend*: HTML5, JavaScript, CLIP templating, LASS (CSS in Lisp)
- *Infrastructure*: Docker, Docker Compose
* Project Timeline
** Phase 1: Project Inception (August 2025)
*** 2025-08-12: Initial Commit
- *Author*: Brian O'Reilly (Fade)
- Project founded and initial repository created
- Basic project structure established
- Core Radiance framework integration begun
** Phase 2: Foundation Building (September - Early October 2025)
*** Core Features Established
- Basic web server setup with Radiance
- Initial music library scanning functionality
- Database integration for track metadata
- Basic authentication system
- Front-end page structure
*** Key Contributors Join
- Glenn Thompson (glenneth) begins major contributions
- Luis Pereira joins for UI/UX improvements
- Collaborative development model established
** Phase 3: Template System & UI Overhaul (October 2025)
*** 2025-10-04 to 2025-10-06: CLIP Template Migration
- *Lead*: Luis Pereira, Glenn Thompson
- Migrated from inline HTML to CLIP templating system
- Established consistent site-wide styling
- Implemented VT323 retro terminal font
- Created reusable template components
*** 2025-10-04 to 2025-10-07: User Management System
- *Lead*: Glenn Thompson
- User profile pages with edit functionality
- Registration and authentication UI
- Role-based access control (admin, DJ, listener)
- User profile management interface
*** 2025-10-05: Navigation Improvements
- *Lead*: Luis Pereira
- Unified navigation bar across all pages
- Improved responsive design
- Better mobile experience
** Phase 4: API Refactoring & Testing (October 8-10, 2025)
*** 2025-10-08: Major API Overhaul
- *Lead*: Glenn Thompson
- Refactored all endpoints to use Radiance's define-api macro
- Standardized JSON API responses
- API-aware authentication (auto-detects API vs web requests)
- Comprehensive automated test suite added
*** 2025-10-08 to 2025-10-09: Frontend JavaScript Updates
- Fixed all frontend code to work with new API endpoints
- Improved error handling
- Better async/await patterns
*** 2025-10-10: Documentation Sprint
- *Lead*: Glenn Thompson
- Major documentation cleanup
- Added comprehensive API documentation
- Created testing guides
- Updated all core documentation files
** Phase 5: Streaming Infrastructure (October 8-14, 2025)
*** 2025-10-08: Liquidsoap DJ Controls
- *Lead*: Glenn Thompson
- Telnet integration with Liquidsoap
- Real-time stream control
- Skip track functionality
- Queue management via telnet commands
*** 2025-10-10: Dynamic Stream URL Support
- *Lead*: Glenn Thompson
- Stream base URL as template variable
- Support for multiple deployment environments
- Preparation for multi-network access
*** 2025-10-14: Stream Queue System
- *Lead*: Brian O'Reilly, Glenn Thompson
- M3U playlist queue management
- Admin UI for queue control
- Add/remove tracks from stream queue
- Real-time queue updates
*** 2025-10-14: Audio Quality Improvements
- ReplayGain volume normalization
- Reduced buffering
- Improved player UI
- Better streaming performance
** Phase 6: Advanced Features (October 12-17, 2025)
*** 2025-10-12: Role-Based Page Flow
- *Lead*: Glenn Thompson
- Intelligent page routing based on user role
- Admin-specific workflows
- DJ control interfaces
- Enhanced user experience
*** 2025-10-13: HTML Partial Hydration
- *Lead*: Luis Pereira
- Now-playing partial component
- Server-side rendering with client updates
- Reduced JavaScript complexity
- Better performance
*** 2025-10-15 to 2025-10-16: Configuration System
- *Lead*: Brian O'Reilly
- Dedicated configuration namespace exploration
- Environment-based configuration
- Improved deployment flexibility
*** 2025-10-16: Comprehensive Documentation Update
- *Lead*: Glenn Thompson
- PROJECT-OVERVIEW updated with all features
- Stream queue and ReplayGain documentation
- Complete feature documentation
*** 2025-10-17: Code Quality Improvements
- *Lead*: Glenn Thompson
- Code consistency refactoring
- Bug fixes (track search query variable)
- Maintainability improvements
- Better code organization
** Phase 7: Player Evolution (October 19-25, 2025)
*** 2025-10-19: Pop-Out Player
- *Lead*: Glenn Thompson
- Standalone pop-out player window
- Independent audio playback
- Queue management improvements
- Multi-window support
*** 2025-10-19: Persistent Audio Player (Frameset)
- *Lead*: Glenn Thompson
- Frameset-based persistent player
- Audio continues during navigation
- Bottom-frame player bar
- Seamless listening experience
*** 2025-10-21: Hybrid Player System
- *Lead*: Glenn Thompson
- Combined frameset and pop-out options
- User preference storage (localStorage)
- Flexible playback modes
- Enhanced user choice
*** 2025-10-24: Dynamic Stream URL Detection
- *Lead*: Glenn Thompson
- Automatic host detection from HTTP headers
- Multi-environment support (localhost, Tailscale, LAN)
- Fixed remote access issues
- No configuration needed for different networks
*** 2025-10-25: Typography Consistency Fix
- *Lead*: Glenn Thompson
- Replaced Courier New with VT323 in persistent player
- Consistent font usage site-wide
- Addressed styling feedback
- Improved visual coherence
** Phase 8: Docker Deployment & Documentation (October 26 - November 1, 2025)
*** 2025-10-19: User Initialization Retry Logic
- *Lead*: Luis Pereira (easilok)
- Fixed user initialization retry mechanism
- Improved reliability on startup
- Better error handling
*** 2025-10-26: Custom Environment Variables for Streams
- *Lead*: Luis Pereira (easilok)
- Added MUSIC_LIBRARY environment variable
- Added QUEUE_PLAYLIST environment variable
- Flexible path configuration for Docker deployments
*** 2025-10-26: Docker Setup for Asteroid Application
- *Lead*: Luis Pereira (easilok)
- Created Dockerfile.asteroid for app containerization
- Added docker-compose.asteroid.yml
- Radiance configuration for containerized deployment
- Complete Docker-based deployment solution
*** 2025-10-26: Docker Deployment Documentation
- *Lead*: Luis Pereira (easilok)
- Comprehensive Docker deployment guide in INSTALLATION.org
- Separate sections for stream services and application
- Environment variable documentation
- Build and deployment instructions
*** 2025-10-26: Comprehensive Documentation Update
- *Lead*: Glenn Thompson
- Created PROJECT-HISTORY.org with complete timeline
- Updated all documentation dates to 2025-10-26
- Added current features across all docs
- Updated repository URLs to GitHub
- Documentation version 3.0
*** 2025-10-28: Documentation Refinements
- *Lead*: Glenn Thompson
- Fixed music directory location (asteroid/music/ not docker/music/)
- Removed redundant Python/JavaScript examples from API docs
- Added package manager notes for cross-distribution compatibility
- Clarified symlink support for music directories
*** 2025-11-01: Documentation Merge and Cleanup
- *Lead*: Glenn Thompson
- Merged upstream Docker deployment documentation
- Removed obsolete session notes
- Synchronized with upstream/main
- Prepared comprehensive documentation PR
* Development Statistics
** Contributors (by commit count)
1. Glenn Thompson (glenneth/Glenneth) - 135+ commits
2. Brian O'Reilly (Fade) - 55+ commits
3. Luis Pereira (easilok) - 23+ commits
** Total Commits: 213+ commits
** Active Development Period
- Start: August 12, 2025
- Current: November 1, 2025
- Duration: ~2.75 months of active development
* Major Features Implemented
** Core Functionality
- ✅ Music library scanning and metadata extraction
- ✅ PostgreSQL database integration (configured, ready for migration)
- ✅ Track search and filtering
- ✅ Playlist management
- ✅ Stream queue control
- ✅ Live streaming via Icecast/Liquidsoap
** User Management
- ✅ User registration and authentication
- ✅ Role-based access control (Admin, DJ, Listener)
- ✅ User profiles with edit functionality
- ✅ Session management
- ✅ Role-based page flow
** Streaming Features
- ✅ Multiple quality options (AAC 96k, MP3 128k, MP3 64k)
- ✅ ReplayGain volume normalization
- ✅ Live now-playing information
- ✅ Icecast integration
- ✅ Liquidsoap DJ controls
- ✅ Stream queue management
** Player Options
- ✅ Inline web player
- ✅ Pop-out player window
- ✅ Persistent frameset player
- ✅ Hybrid player system
- ✅ Quality selector
- ✅ Auto-reconnect on errors
** API & Integration
- ✅ RESTful JSON API
- ✅ API-aware authentication
- ✅ Comprehensive test suite
- ✅ Telnet integration with Liquidsoap
- ✅ Real-time status updates
** UI/UX
- ✅ Retro terminal aesthetic (VT323 font)
- ✅ Responsive design
- ✅ CLIP templating system
- ✅ LASS CSS preprocessing
- ✅ Consistent navigation
- ✅ HTML partial hydration
** Infrastructure
- ✅ Docker containerization (streams and application)
- ✅ Docker Compose orchestration
- ✅ Dockerfile for Asteroid application
- ✅ Environment variable configuration
- ✅ PostgreSQL database (configured)
- ✅ Multi-environment support
- ✅ Dynamic URL detection
* Technical Milestones
** Architecture Evolution
1. *Initial*: Monolithic HTML generation
2. *Template Migration*: CLIP templating system
3. *API Standardization*: Radiance define-api macros
4. *Component Architecture*: HTML partials and hydration
5. *Multi-Mode Player*: Hybrid player system
** Code Quality Improvements
- Comprehensive test suite
- API refactoring for consistency
- Code organization and maintainability
- Documentation standards
- Consistent error handling
** Performance Optimizations
- ReplayGain normalization
- Reduced buffering
- Efficient database queries
- Parallel music scanning
- Client-side caching
* Current State (November 2025)
** Production Ready Features
- Full music streaming platform
- User management system
- Admin control panel
- DJ controls
- Multiple player modes
- Complete Docker deployment (streams + application)
- Multi-environment support with dynamic URLs
- Comprehensive documentation
** Active Development Areas
- PostgreSQL migration (configured, ready for data migration)
- JavaScript code cleanup and refactoring
- Additional UI improvements
- Performance optimization
- Feature expansion based on user feedback
** Recent Achievements
- ✅ Complete Docker containerization
- ✅ Environment variable configuration
- ✅ Comprehensive documentation overhaul
- ✅ Cross-distribution package manager support
- ✅ Streamlined deployment process
** Known Issues & Future Work
- PostgreSQL migration (configured, pending data migration)
- Continued UI/UX refinement
- Additional streaming features (per design.org)
- Enhanced playlist functionality
- Live chat and song requests
- Mobile app considerations
- Scalability improvements
* Project Philosophy
** Design Principles
- *Hacker Aesthetic*: Terminal-inspired retro design
- *User Choice*: Multiple player modes and options
- *Simplicity*: Clean, focused interface
- *Performance*: Fast, responsive experience
- *Flexibility*: Multi-environment support
** Development Approach
- Collaborative development
- Iterative improvements
- Comprehensive testing
- Documentation-first
- User feedback driven
* Acknowledgments
** Core Team
- *Brian O'Reilly (Fade)*: Project founder, architecture, streaming infrastructure
- *Glenn Thompson (glenneth)*: Major features, API, player systems, documentation
- *Luis Pereira*: UI/UX, templating, frontend improvements
** Technologies
- Radiance web framework
- Icecast streaming server
- Liquidsoap audio processing
- PostgreSQL database
- Common Lisp ecosystem
* Conclusion
Asteroid Radio has evolved from a simple concept into a full-featured internet radio platform in just 2.75 months of active development. The project demonstrates the power of Common Lisp for web development and the collaborative nature of open-source development.
With complete Docker deployment, comprehensive documentation, and a growing feature set, Asteroid Radio is ready for production use while continuing to evolve with regular improvements, bug fixes, and new features based on user needs and technical requirements.
** Project Links
- Repository: https://github.com/fade/asteroid
- Contributors: https://github.com/fade/asteroid/graphs/contributors
- IRC: #asteroid.music on irc.libera.chat
---
*Last Updated: 2025-11-01*

View File

@ -1,6 +1,6 @@
#+TITLE: Asteroid Radio - Project Overview
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade)
#+DATE: 2025-10-03
#+DATE: 2025-10-26
* 🎯 Mission
@ -38,7 +38,8 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack
- **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
- **PostgreSQL** - Database backend (configured, ready for migration)
- **Radiance DB** - Current database abstraction layer
**Frontend:**
- **HTML5** with semantic templates
@ -75,26 +76,32 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack
## 🚀 Features
### Current Features
- ✅ **User Authentication** - Registration, login, profiles, role-based access
- ✅ **User Authentication** - Registration, login, profiles, role-based access (Admin/DJ/Listener)
- ✅ **User Management** - Admin interface for user administration
- ✅ **Music Library** - Track management with pagination and search
- ✅ **Music Library** - Track management with pagination, search, and filtering
- ✅ **User Playlists** - Create, manage, and play personal music collections
- ✅ **Web Player** - Browser-based player with queue management
- ✅ **Multiple Player Modes** - Inline, pop-out, and persistent frameset players
- ✅ **Stream Queue Control** - Admin control over broadcast stream queue (M3U-based)
- ✅ **REST API** - Comprehensive JSON API with 15+ endpoints
- ✅ **Music Streaming** - Multiple quality formats (MP3, AAC)
- ✅ **Music Streaming** - Multiple quality formats (128k MP3, 96k AAC, 64k MP3)
- ✅ **Rate Limiting** - Anti-abuse protection
- ✅ **Docker Integration** - Icecast2/Liquidsoap streaming infrastructure
- ✅ **PostgreSQL Database** - Persistent data storage
- ✅ **PostgreSQL Database** - Configured and ready for migration
- ✅ **Liquidsoap DJ Controls** - Telnet interface for live control
- ✅ **Dynamic Stream URLs** - Automatic host detection for multi-environment support
- ✅ **ReplayGain Normalization** - Consistent audio volume across tracks
- ✅ **Responsive Design** - Works on desktop and mobile
- ✅ **Automated Testing** - Comprehensive test suite
### Planned Features
- 🔄 **PostgreSQL Migration** - Full migration from Radiance DB to PostgreSQL
- 🔄 **Enhanced Playlist Management** - Full CRUD operations with PostgreSQL
- 🔄 **Social Features** - Playlist sharing and discovery
- 🔄 **Advanced Search** - Full-text search and filtering
- 🔄 **Mobile App** - Native mobile applications
- 🔄 **WebSocket Support** - Real-time updates
- 🔄 **Analytics** - Listening statistics and insights
- 🔄 **Scheduled Programming** - Time-based queue switching
## 🔮 Vision

View File

@ -1,6 +1,6 @@
#+TITLE: Asteroid Radio - Documentation Index
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-10
#+DATE: 2025-10-26
* Welcome to Asteroid Radio Documentation
@ -19,6 +19,9 @@ For immediate setup, see:
*** [[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:PROJECT-HISTORY.org][Project History]]
Comprehensive development history from inception to present, including timeline, milestones, and contributor information.
*** [[file:INSTALLATION.org][Installation Guide]]
Comprehensive installation instructions for multiple operating systems, including system requirements, dependencies, and production deployment considerations.
@ -58,16 +61,19 @@ Pagination system for efficient browsing of large music libraries.
** What's Working Now
- **Web Application**: Full-featured web interface with authentication
- **REST API**: JSON API with 15+ endpoints for programmatic access
- **User Management**: Registration, login, roles, and profiles
- **Music Library**: Track management with pagination and search
- **User Management**: Registration, login, roles (Admin/DJ/Listener), and profiles
- **Music Library**: Track management with pagination, search, and filtering
- **Playlists**: User playlists with creation and playback
- **Web Player**: Browser-based audio player with queue management
- **Multiple Player Modes**: Inline, pop-out, and persistent frameset players
- **Stream Queue Control**: Admin control over broadcast stream queue
- **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/
- **Liquidsoap DJ Controls**: Telnet control via localhost:1234
- **Professional Features**: Crossfading, normalization, metadata support
- **PostgreSQL Database**: Persistent data storage with full CRUD operations
- **Professional Features**: Crossfading, ReplayGain normalization, metadata support
- **PostgreSQL Database**: Configured and ready for migration
- **Dynamic Stream URLs**: Automatic host detection for multi-environment support
- **Responsive Design**: Works on desktop and mobile devices
** Stream URLs (when running)
- **High Quality MP3**: http://localhost:8000/asteroid.mp3 (128kbps)
@ -141,5 +147,5 @@ For detailed technical information, see the **[[file:PROJECT-OVERVIEW.org][Proje
---
*Last Updated: 2025-10-10*
*Documentation Version: 2.0*
*Last Updated: 2025-10-26*
*Documentation Version: 3.0*

View File

@ -1,6 +1,6 @@
#+TITLE: Stream Queue Control System
#+AUTHOR: Asteroid Radio Team
#+DATE: 2025-10-14
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-26
* Overview
@ -140,7 +140,7 @@ If you're working directly in the Lisp REPL:
* File Locations
- *Stream Queue File*: =/home/glenn/Projects/Code/asteroid/stream-queue.m3u=
- *Stream Queue File*: =stream-queue.m3u= (in project root)
- *Docker Mount*: =/app/stream-queue.m3u= (inside Liquidsoap container)
- *Liquidsoap Config*: =docker/asteroid-radio-docker.liq=

View File

@ -1,11 +1,15 @@
#+TITLE: Asteroid Radio Testing Guide
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-08
#+DATE: 2025-10-26
* Overview
This document describes the automated testing system for Asteroid Radio.
#+BEGIN_QUOTE
*Note on Package Managers*: Examples use =apt= (Debian/Ubuntu). Replace with your distribution's package manager (=dnf=, =pacman=, =zypper=, =apk=, etc.).
#+END_QUOTE
* Test Script
The =test-server.sh= script provides comprehensive testing of all server endpoints and functionality.

View File

@ -1,6 +1,6 @@
#+TITLE: Track Pagination System - Complete
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-04
#+DATE: 2025-10-26
* Overview

View File

@ -1,6 +1,6 @@
#+TITLE: User Management System - Complete
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-04
#+DATE: 2025-10-26
* Overview

View File

@ -1,7 +1,7 @@
(in-package :asteroid)
(defun icecast-now-playing (icecast-base-url)
(let* ((icecast-url (concatenate 'string icecast-base-url "/admin/stats.xml"))
(let* ((icecast-url (format nil "~a/admin/stats.xml" icecast-base-url))
(response (drakma:http-request icecast-url
:want-stream nil
:basic-authorization '("admin" "asteroid_admin_2024"))))
@ -22,31 +22,29 @@
(listenersp (cl-ppcre:all-matches "<listeners>" source-section))
(title (if titlep (cl-ppcre:regex-replace-all ".*<title>(.*?)</title>.*" source-section "\\1") "Unknown"))
(listeners (if listenersp (cl-ppcre:regex-replace-all ".*<listeners>(.*?)</listeners>.*" source-section "\\1") "0")))
`((:listenurl . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
`((:listenurl . ,(format nil "~a/asteroid.mp3" *stream-base-url*))
(:title . ,title)
(:listeners . ,(parse-integer listeners :junk-allowed t))))
`((:listenurl . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
`((:listenurl . ,(format nil "~a/asteroid.mp3" *stream-base-url*))
(:title . "Unknown")
(:listeners . "Unknown"))))))))
(define-api asteroid/partial/now-playing () ()
"Get Partial HTML with live status from Icecast server"
(handler-case
(let ((now-playing-stats (icecast-now-playing *stream-base-url*))
(template-path (merge-pathnames "template/partial/now-playing.chtml"
(asdf:system-source-directory :asteroid))))
(let ((now-playing-stats (icecast-now-playing *stream-base-url*)))
(if now-playing-stats
(progn
;; TODO: it should be able to define a custom api-output for this
;; (api-output <clip-parser> :format "html"))
(setf (header "Content-Type") "text/html")
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "partial/now-playing")
:stats now-playing-stats))
(progn
(setf (header "Content-Type") "text/html")
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
(load-template "partial/now-playing")
:connection-error t
:stats nil))))
(error (e)

View File

@ -3,17 +3,32 @@
(in-package :asteroid)
;; Template directory configuration
(defparameter *template-directory*
(merge-pathnames "template/" (asdf:system-source-directory :asteroid))
"Base directory for all CLIP templates")
;; Template cache for parsed templates
(defvar *template-cache* (make-hash-table :test 'equal)
"Cache for parsed template DOMs")
(defun template-path (name)
"Build full path to template file.
NAME can be either:
- Simple name: 'front-page' -> 'template/front-page.ctml'
- Path with subdirs: 'partial/now-playing' -> 'template/partial/now-playing.ctml'"
(merge-pathnames (format nil "~a.ctml" name) *template-directory*))
(defun load-template (name)
"Load and parse a template by name without caching.
Use this for templates that change frequently during development."
(plump:parse (alexandria:read-file-into-string (template-path name))))
(defun get-template (template-name)
"Load and cache a template file"
"Load and cache a template file.
Use this for production - templates are cached after first load."
(or (gethash template-name *template-cache*)
(let* ((template-path (merge-pathnames
(format nil "template/~a.chtml" template-name)
(asdf:system-source-directory :asteroid)))
(parsed (plump:parse (alexandria:read-file-into-string template-path))))
(let ((parsed (load-template template-name)))
(setf (gethash template-name *template-cache*) parsed)
parsed)))

View File

@ -282,12 +282,15 @@
;; Fallback to delayed initialization
(bt:make-thread
(lambda ()
(sleep 3) ; Give database more time to initialize
(dotimes (a 5)
(unless (db:connected-p)
(sleep 3)) ; Give database more time to initialize
(handler-case
(progn
(format t "Retrying user management setup...~%")
(create-default-admin)
(format t "User management initialization complete.~%"))
(format t "User management initialization complete.~%")
(return))
(error (e)
(format t "Error initializing user system: ~a~%" e))))
(format t "Error initializing user system: ~a~%" e)))))
:name "user-init"))))