Compare commits

..

19 Commits

Author SHA1 Message Date
Glenn Thompson 775bc16261 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 05:33:56 +03:00
Glenn Thompson 1c1cc995bd chore: remove obsolete session notes file
SESSION-NOTES-2025-10-12.org is no longer needed
2025-11-01 05:28:24 +03:00
Glenn Thompson fba21cb5c4 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 05:28:24 +03:00
Glenn Thompson c743e25e3d 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 05:28:24 +03: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
Glenn Thompson 66e97aaf37 fix: Replace Courier New with VT323 in persistent audio player frame
- Changed font-family from 'Courier New' to 'VT323' in body style
- Updated quality selector dropdown font to VT323
- Updated disable button inline style to use VT323
- Ensures consistent typography across the entire site
- Popout player already inherits VT323 from main stylesheet
2025-10-25 18:32:46 +03: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
glenneth 01f5806959 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-21 21:50:39 +03:00
glenneth 74cd3625f3 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-19 13:52:59 +03:00
glenneth 9721fbbc8a fix: track search missing query variable
- Add missing query variable in filterTracks() function
- Reads value from track-search input field
- Bug was pre-existing, not introduced by refactoring
2025-10-17 21:38:18 -04:00
glenneth b3fd00cb4d refactor: improve code consistency and maintainability
- Add /api/asteroid/partial/now-playing-inline endpoint for inline text
- Refactor admin.js to use server-side partial (removes 23 lines of JSON/XML parsing)
- Fix hardcoded path in convert-to-docker-path to use *music-library-path* variable
- Consistent with front-page and player refactoring from upstream
- Improves portability and reduces client-side JavaScript complexity
2025-10-17 21:38:18 -04:00
Luis Pereira 4d0b54f7d6 feat: move player to use now-playing partial 2025-10-16 19:02:00 -04:00
Luis Pereira f3d012cbc6 feat: move front-page to use now-playing partial 2025-10-16 19:02:00 -04:00
Luis Pereira d0efc89e33 feat: add HTML partial hidration for now-playing 2025-10-16 19:02:00 -04:00
45 changed files with 2004 additions and 1662 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,59 +1,45 @@
#+TITLE: Asteroid Radio - Internet Streaming Platform #+TITLE: Asteroid Radio - Internet Radio Streaming Platform
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade) #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-16 #+DATE: 2025-10-26
* Overview * Overview
Asteroid Radio is a modern, web-based music streaming platform built with Common Lisp and the Radiance framework. It provides a complete internet radio streaming system with live broadcasting, user management, playlist control, and professional audio processing. 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 * Key Features
** Live Internet Radio Streaming ** Live Internet Radio Streaming
- Multiple streaming formats: AAC 96kbps (high efficiency), MP3 128kbps (standard), MP3 64kbps (low bandwidth) - Multiple quality streams: 128kbps MP3, 96kbps AAC, 64kbps MP3
- Professional audio processing with ReplayGain for consistent volume without pumping - Professional audio processing with crossfading and ReplayGain normalization
- Smooth crossfading between tracks (5 second transitions)
- Dynamic compression to prevent clipping
- Icecast2 streaming server integration - Icecast2 streaming server integration
- Liquidsoap audio pipeline with watch-based playlist reloading - Liquidsoap audio pipeline for reliable broadcasting
- Stream queue control for curated programming
** Stream Queue Control System
- Curated stream queue management (play tracks in specific order)
- Add tracks to end of queue or as "next to play"
- Add entire playlists to stream queue
- Real-time queue reordering
- Automatic fallback to random playback when queue is empty
- Stream history tracking
- Admin-only queue control via REST API
** Music Library Management ** Music Library Management
- PostgreSQL-backed track storage with metadata extraction - Database-backed track storage with metadata extraction
- Support for MP3, FLAC, OGG, and WAV formats - Support for MP3, FLAC, OGG, and WAV formats
- Automatic metadata extraction using taglib - Automatic metadata extraction using taglib
- Track search, filtering, and sorting capabilities - Track search, filtering, sorting, and pagination
- Pagination support for large libraries - Recursive directory scanning
- Individual track streaming on-demand
** User Management System
- User registration and authentication
- Role-based access control (admin/user)
- User profiles and session management
- Personal playlist creation and management
- Admin interface for user administration
** Web Interface ** Web Interface
- RADIANCE framework with CLIP templating - RADIANCE framework with CLIP templating
- Modern admin dashboard with JavaScript controls - Admin dashboard for library and user management
- Web player with HTML5 audio controls - Multiple player modes: inline, pop-out, and persistent frameset
- Live stream integration with embedded player - Live stream integration with embedded player
- Dark hacker-themed aesthetic with VT323 font
- Responsive design for desktop and mobile - Responsive design for desktop and mobile
- Role-based access control (Admin/DJ/Listener)
** Network Broadcasting ** Network Broadcasting
- Docker-based streaming infrastructure - Dynamic stream URL detection for multi-environment support
- WSL-compatible networking for internal network access
- Professional streaming URLs for media players - Professional streaming URLs for media players
- Multi-listener support via Icecast2 - Multi-listener support via Icecast2
- Telnet control interface for live DJ operations - Docker-based deployment for easy setup
* Architecture Changes * Architecture Changes
@ -64,62 +50,82 @@ Asteroid Radio is a modern, web-based music streaming platform built with Common
- Database abstraction layer for track storage - Database abstraction layer for track storage
** Streaming Stack ** Streaming Stack
- *Icecast2*: Streaming server (port 8000) - *Icecast2*: Streaming server (port 8000) - Docker containerized
- *Liquidsoap*: Audio processing and streaming pipeline with telnet control (port 1234) - *Liquidsoap*: Audio processing and streaming pipeline - Docker containerized
- *RADIANCE*: Web server and API (port 8080) - *RADIANCE*: Web server and API (port 8080)
- *PostgreSQL*: User accounts, track metadata, and playlist storage - *PostgreSQL*: Database backend (configured, ready for migration)
- *Docker Compose*: Container orchestration for streaming services - *Docker Compose*: Container orchestration
** File Structure ** File Structure
#+BEGIN_SRC #+BEGIN_SRC
asteroid/ asteroid/
├── asteroid.lisp # Main server with RADIANCE routes ├── asteroid.lisp # Main server with RADIANCE routes
├── asteroid.asd # System definition with dependencies ├── asteroid.asd # System definition with dependencies
├── stream-control.lisp # Stream queue management system ├── stream-control.lisp # Stream queue management
├── stream-media.lisp # Media streaming and track management ├── user-management.lisp # User administration
├── user-management.lisp # User authentication and profiles ├── playlist-management.lisp # Playlist operations
├── playlist-management.lisp # User playlist operations ├── test-server.sh # Automated test suite
├── auth-routes.lisp # Authentication endpoints ├── docker/ # Docker infrastructure
├── stream-queue.m3u # Generated stream queue playlist
├── docker/ # Docker streaming infrastructure
│ ├── docker-compose.yml # Container orchestration │ ├── docker-compose.yml # Container orchestration
│ ├── asteroid-radio-docker.liq # Liquidsoap configuration │ ├── asteroid-radio-docker.liq # Liquidsoap config
│ └── start.sh # Container startup script │ ├── icecast.xml # Icecast configuration
│ └── music/ # Music library mount
├── template/ # CLIP HTML templates ├── template/ # CLIP HTML templates
│ ├── front-page.chtml # Main page with live stream │ ├── front-page.chtml # Main page with live stream
│ ├── admin.chtml # Admin dashboard with queue controls │ ├── admin.chtml # Admin dashboard
│ ├── player.chtml # Web player interface │ ├── player.chtml # Web player interface
│ └── login.chtml # User authentication │ └── users.chtml # User management
├── static/ # Frontend assets ├── static/ # CSS and assets
│ ├── asteroid.lass # LASS stylesheet source │ └── asteroid.lass # LASS stylesheet
│ ├── asteroid.css # Compiled CSS ├── docs/ # Comprehensive documentation
│ └── js/ │ ├── README.org # Documentation index
│ ├── admin.js # Admin interface controls │ ├── PROJECT-OVERVIEW.org # Architecture overview
│ └── player.js # Web player functionality │ ├── PROJECT-HISTORY.org # Development timeline
├── docs/ # Documentation │ ├── INSTALLATION.org # Setup guide
│ ├── STREAM-CONTROL.org # Queue management guide │ └── ... # Additional guides
│ ├── API-REFERENCE.org # Complete API documentation └── music/ # Music library (local dev)
│ ├── USER-MANAGEMENT-SYSTEM.org # User system guide
│ └── INSTALLATION.org # Setup instructions
└── music/ # Music library
└── library/ # Music files
#+END_SRC #+END_SRC
* Track Upload Workflow * Quick Start
** Current Implementation (Manual Upload) ** Docker Installation (Recommended)
1. *Copy files to staging*: Place MP3/FLAC files in =music/incoming/= #+BEGIN_SRC bash
2. *Access admin panel*: Navigate to =http://[IP]:8080/asteroid/admin= # Clone repository
3. *Process files*: Click "Copy Files from Incoming" button git clone https://github.com/fade/asteroid
4. *Database update*: Files are moved to =music/library/= and metadata extracted cd asteroid/docker
5. *Automatic playlist*: =playlist.m3u= is regenerated for streaming
** File Processing Steps # Start all services
1. Files copied from =music/incoming/= to =music/library/= docker compose up -d
2. Metadata extracted using taglib (title, artist, album, duration, bitrate)
3. Database record created with file path and metadata # Verify streams are working
4. Playlist file updated for Liquidsoap streaming curl -I http://localhost:8000/asteroid.mp3
5. Files immediately available for on-demand streaming 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 ** Supported Formats
- *MP3*: Primary format, best compatibility - *MP3*: Primary format, best compatibility
@ -130,288 +136,259 @@ asteroid/
* Icecast2 Integration * Icecast2 Integration
** Configuration ** Configuration
- *Server*: localhost:8000 - *Server*: localhost:8000 (Docker container)
- *Mount point*: =/asteroid.mp3= - *Mount points*: =/asteroid.mp3=, =/asteroid.aac=, =/asteroid-low.mp3=
- *Password*: =b3l0wz3r0= (configured in Liquidsoap) - *Password*: =H1tn31EhsyLrfRmo= (configured in Docker setup)
- *Format*: MP3 128kbps stereo - *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 #+BEGIN_SRC bash
sudo apt update # Managed via docker-compose
sudo apt install icecast2 cd docker
sudo systemctl enable icecast2 docker compose up -d icecast
sudo systemctl start icecast2
#+END_SRC #+END_SRC
** Stream Access ** Stream Access
- *Direct URL*: =http://[IP]:8000/asteroid.mp3= - *High Quality MP3*: =http://localhost:8000/asteroid.mp3= (128kbps)
- *Admin interface*: =http://[IP]:8000/admin/= - *High Quality AAC*: =http://localhost:8000/asteroid.aac= (96kbps)
- *Statistics*: =http://[IP]:8000/status.xsl= - *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 * Liquidsoap Integration
** Configuration File: =docker/asteroid-radio-docker.liq= ** Docker Configuration
#+BEGIN_SRC liquidsoap Liquidsoap runs in a Docker container with configuration in =docker/asteroid-radio-docker.liq=
#!/usr/bin/liquidsoap
# Allow running as root in Docker ** Key Features
set("init.allow_root", true) - *Multiple outputs*: Generates 3 simultaneous streams (MP3 128k, AAC 96k, MP3 64k)
log.level.set(4) - *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
# Enable telnet server for remote control ** Management
settings.server.telnet.set(true)
settings.server.telnet.port.set(1234)
settings.server.telnet.bind_addr.set("0.0.0.0")
# Create playlist source from managed stream queue
radio = playlist(
mode="normal", # Play in order (not randomized)
reload=5, # Check for playlist updates every 5 seconds
reload_mode="watch", # Watch file for changes
"/app/stream-queue.m3u"
)
# Fallback to directory scan if queue is empty
radio_fallback = playlist.safe(
mode="randomize",
reload=3600,
"/app/music/"
)
radio = fallback(track_sensitive=false, [radio, radio_fallback])
# Use ReplayGain for consistent volume without pumping
radio = amplify(1.0, override="replaygain", radio)
# Add smooth crossfade between tracks (5 seconds)
radio = crossfade(
duration=5.0,
fade_in=3.0,
fade_out=3.0,
radio
)
# Add compressor to prevent clipping
radio = compress(
ratio=3.0,
threshold=-15.0,
attack=50.0,
release=400.0,
radio
)
# Output to Icecast2 in multiple formats
output.icecast(%mp3(bitrate=128), host="icecast", port=8000,
password="H1tn31EhsyLrfRmo", mount="asteroid.mp3",
name="Asteroid Radio", radio)
output.icecast(%fdkaac(bitrate=96), host="icecast", port=8000,
password="H1tn31EhsyLrfRmo", mount="asteroid.aac",
name="Asteroid Radio (AAC)", radio)
output.icecast(%mp3(bitrate=64), host="icecast", port=8000,
password="H1tn31EhsyLrfRmo", mount="asteroid-low.mp3",
name="Asteroid Radio (Low Quality)", radio)
#+END_SRC
** Installation (Ubuntu/Debian)
#+BEGIN_SRC bash #+BEGIN_SRC bash
sudo apt update # Start Liquidsoap container
sudo apt install liquidsoap
#+END_SRC
** Features
- *Stream queue control*: Curated playlist with watch-based reloading (5 second updates)
- *Smart fallback*: Random playback when queue is empty
- *ReplayGain processing*: Consistent volume without pumping artifacts
- *Crossfading*: Smooth 5-second transitions between tracks
- *Dynamic compression*: Prevents clipping and maintains audio quality
- *Multi-format output*: AAC 96kbps, MP3 128kbps, MP3 64kbps
- *Telnet control*: Live DJ operations via port 1234
- *Metadata broadcasting*: Track info sent 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
#+BEGIN_SRC bash
# Start Docker streaming services
cd docker cd docker
docker-compose up -d docker compose up -d liquidsoap
# Start Asteroid web application # View logs
sbcl --load asteroid.lisp docker compose logs -f liquidsoap
# Restart streaming
docker compose restart liquidsoap
#+END_SRC #+END_SRC
** Stopping the Radio Station ** Telnet Control
#+BEGIN_SRC bash #+BEGIN_SRC bash
# Stop Docker services # Connect to Liquidsoap
cd docker telnet localhost 1234
docker-compose down
# Or use netcat for scripting
echo "request.queue" | nc localhost 1234
echo "request.skip" | nc localhost 1234
#+END_SRC #+END_SRC
** Adding Music * User Management
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
** Listening to the Stream ** Roles
- *Web Browser*: Visit main page for embedded player - *Admin*: Full system access, user management, stream control
- *Media Player*: Open =http://[IP]:8000/asteroid.mp3= - *DJ*: Content management, playlist creation, library access
- *Mobile Apps*: Use internet radio apps with stream URL - *Listener*: Basic playback and personal playlists
** 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 * 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 ** Track Management
- =GET /api/asteroid/tracks= - List all tracks with pagination - =GET /api/asteroid/tracks= - List all tracks
- =GET /api/asteroid/tracks/:id/stream= - Stream individual track - =GET /api/asteroid/admin/tracks= - Admin track listing
- =POST /api/asteroid/scan-library= - Scan and update music library - =POST /api/asteroid/admin/scan-library= - Scan music library
- =GET /api/asteroid/tracks?search={query}= - Search tracks
** Stream Queue Control (Admin Only) ** Player Control
- =GET /api/asteroid/stream/queue= - Get current stream queue - =GET /api/asteroid/player/status= - Player status
- =POST /api/asteroid/stream/queue/add= - Add track to queue (position: end/next) - =POST /api/asteroid/player/play= - Play track
- =POST /api/asteroid/stream/queue/remove= - Remove track from queue - =POST /api/asteroid/player/pause= - Pause playback
- =POST /api/asteroid/stream/queue/clear= - Clear entire queue - =POST /api/asteroid/player/stop= - Stop playback
- =POST /api/asteroid/stream/queue/add-playlist= - Add playlist to queue - =POST /api/asteroid/player/resume= - Resume playback
- =POST /api/asteroid/stream/queue/reorder= - Reorder queue tracks
- =GET /api/asteroid/stream/history= - Get stream history
** User Management
- =POST /api/asteroid/auth/register= - Register new user
- =POST /api/asteroid/auth/login= - User login
- =POST /api/asteroid/auth/logout= - User logout
- =GET /api/asteroid/auth/profile= - Get user profile
- =GET /api/asteroid/users= - List users (admin only)
- =POST /api/asteroid/users/:id/role= - Update user role (admin only)
** Playlist Management ** Playlist Management
- =GET /api/asteroid/playlists= - List user's playlists - =GET /api/asteroid/playlists= - List user playlists
- =POST /api/asteroid/playlists= - Create new playlist - =POST /api/asteroid/playlists/create= - Create playlist
- =GET /api/asteroid/playlists/:id= - Get playlist details - =GET /api/asteroid/playlists/get= - Get playlist details
- =DELETE /api/asteroid/playlists/:id= - Delete playlist - =POST /api/asteroid/playlists/add-track= - Add track to playlist
- =POST /api/asteroid/playlists/:id/tracks= - Add track to playlist
- =DELETE /api/asteroid/playlists/:id/tracks/:track-id= - Remove track from playlist
See =docs/API-REFERENCE.org= for complete API documentation. ** 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
* Database Schema See =docs/API-ENDPOINTS.org= for complete API documentation.
** Tracks Table * Database
#+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
** Users Table ** Current: Radiance DB
#+BEGIN_SRC lisp - File-based database abstraction
(db:create "users" '((username :text) - Tracks, users, playlists, sessions
(email :text) - Suitable for development and small deployments
(password-hash :text)
(role :text) ; "admin" or "user"
(created-at :integer)
(last-login :integer)))
#+END_SRC
** Playlists Table ** PostgreSQL (Configured)
#+BEGIN_SRC lisp - Docker container ready
(db:create "playlists" '((name :text) - Full schema defined
(description :text) - Migration pending
(user-id :integer) - See =docs/POSTGRESQL-SETUP.org= for details
(created-at :integer)
(track-ids :text))) ; JSON array of track IDs * Documentation
#+END_SRC
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 * Dependencies
** Lisp Dependencies (asteroid.asd) ** Lisp Dependencies
- =:radiance= - Web framework - =radiance= - Web framework
- =:r-clip= - Templating system - =r-clip= - CLIP templating
- =:lass= - CSS generation - =lass= - CSS preprocessing
- =:cl-json= - JSON handling - =cl-json= - JSON handling
- =:alexandria= - Utilities - =alexandria= - Common Lisp utilities
- =:local-time= - Time handling - =local-time= - Time handling
- =taglib= - Audio metadata extraction
** System Dependencies ** System Dependencies (Docker)
- =icecast2= - Streaming server - Docker Engine 20.10+
- =liquidsoap= - Audio processing - Docker Compose 2.0+
- =taglib= - Metadata extraction (via audio-streams) - All streaming components containerized
* Development Notes * Testing
** RADIANCE Configuration ** Automated Test Suite
- Domain: "asteroid" #+BEGIN_SRC bash
- Routes use =#@= syntax for URL patterns # Run comprehensive tests
- Database abstraction via =db:= functions ./test-server.sh
- CLIP templates with =data-text= attributes
** Database Queries # Verbose mode
- Use quoted symbols for field names: =(:= '_id id)= ./test-server.sh -v
- RADIANCE returns hash tables with string keys #+END_SRC
- Primary key is "_id" internally, "id" in JSON responses
** Streaming Considerations ** Test Coverage
- MP3 files with spaces in names require playlist.m3u approach - 25+ automated tests
- Liquidsoap fallback prevents stream silence - API endpoint validation
- Icecast2 mount points must match Liquidsoap configuration - HTML page rendering
- Static file serving
- JSON response format
- Authentication flows
* Future Enhancements * Contributing
** Planned Features ** Development Workflow
- Web UI for drag-and-drop queue management 1. Fork the repository
- Real-time now-playing display with WebSocket updates 2. Create a feature branch
- Direct browser file uploads with progress bars 3. Make your changes
- Listener statistics and analytics dashboard 4. Run test suite
- Scheduled programming and automation 5. Submit pull request
- Social features (playlist sharing, discovery)
- Mobile native applications
** Technical Improvements ** Community
- WebSocket integration for real-time updates - *IRC*: #asteroid.music on irc.libera.chat
- Telnet integration for skip/next commands from web UI - *Issues*: GitHub issue tracker
- Auto-queue filling (add tracks when queue runs low) - *Discussions*: GitHub discussions
- Genre-based smart queues
- Listener request system ** Core Team
- Full-text search capabilities - Brian O'Reilly (Fade) - Project founder
- Glenn Thompson (glenneth) - Core developer
- Luis Pereira - UI/UX
* Troubleshooting * Troubleshooting
** Common Issues ** Docker Issues
- *No audio in stream*: Check Liquidsoap logs, verify MP3 files #+BEGIN_SRC bash
- *Database errors*: Ensure proper field name quoting in queries # Check container status
- *Network access*: Verify WSL IP and firewall settings docker compose ps
- *File upload issues*: Check permissions on music directories
** Debugging # View logs
- Enable Liquidsoap debug logging: =settings.log.level := 4= docker compose logs icecast
- Check Icecast admin interface for stream status docker compose logs liquidsoap
- Monitor RADIANCE logs for web server issues
- Verify database connectivity and collections # 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 * 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

@ -40,4 +40,5 @@
(:file "playlist-management") (:file "playlist-management")
(:file "stream-control") (:file "stream-control")
(:file "auth-routes") (:file "auth-routes")
(:file "frontend-partials")
(:file "asteroid"))) (:file "asteroid")))

View File

@ -277,6 +277,19 @@
("message" . ,(format nil "Error reordering queue: ~a" e))) ("message" . ,(format nil "Error reordering queue: ~a" e)))
:status 500)))) :status 500))))
(define-api asteroid/stream/queue/load-m3u () ()
"Load stream queue from stream-queue.m3u file"
(require-role :admin)
(handler-case
(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))))
(defun get-track-by-id (track-id) (defun get-track-by-id (track-id)
"Get a track by its ID - handles type mismatches" "Get a track by its ID - handles type mismatches"
;; Try direct query first ;; Try direct query first
@ -503,7 +516,7 @@
("message" . "Listening history cleared successfully")))) ("message" . "Listening history cleared successfully"))))
|# |#
;; Front page ;; Front page - regular view by default
(define-page front-page #@"/" () (define-page front-page #@"/" ()
"Main front page" "Main front page"
(let ((template-path (merge-pathnames "template/front-page.chtml" (let ((template-path (merge-pathnames "template/front-page.chtml"
@ -524,6 +537,44 @@
:now-playing-album "Startup Sounds" :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 🎵")))
;; 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))
: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*
:now-playing-artist "The Void"
:now-playing-track "Silence"
:now-playing-album "Startup Sounds"
: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))
:stream-base-url *stream-base-url*
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
:default-stream-encoding "audio/aac")))
;; Configure static file serving for other files ;; Configure static file serving for other files
(define-page static #@"/static/(.*)" (:uri-groups (path)) (define-page static #@"/static/(.*)" (:uri-groups (path))
(serve-file (merge-pathnames (concatenate 'string "static/" path) (serve-file (merge-pathnames (concatenate 'string "static/" path)
@ -824,6 +875,28 @@
:now-playing-album "Startup Sounds" :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))
: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")))
(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))
:stream-base-url *stream-base-url*
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
:default-stream-encoding "audio/aac")))
(define-api asteroid/status () () (define-api asteroid/status () ()
"Get server status" "Get server status"
(api-output `(("status" . "running") (api-output `(("status" . "running")

View File

@ -9,6 +9,11 @@ set("init.allow_root", true)
# Set log level for debugging # Set log level for debugging
log.level.set(4) log.level.set(4)
# Audio buffering settings to prevent choppiness
settings.frame.audio.samplerate.set(44100)
settings.frame.audio.channels.set(2)
settings.audio.converter.samplerate.libsamplerate.quality.set("best")
# Enable telnet server for remote control # Enable telnet server for remote control
settings.server.telnet.set(true) settings.server.telnet.set(true)
settings.server.telnet.port.set(1234) settings.server.telnet.port.set(1234)
@ -19,8 +24,8 @@ settings.server.telnet.bind_addr.set("0.0.0.0")
# Falls back to directory scan if playlist file doesn't exist # Falls back to directory scan if playlist file doesn't exist
radio = playlist( radio = playlist(
mode="normal", # Play in order (not randomized) mode="normal", # Play in order (not randomized)
reload=5, # Check for playlist updates every 5 seconds reload=30, # Check for playlist updates every 30 seconds
reload_mode="watch", # Watch file for changes reload_mode="seconds", # Reload every N seconds (prevents running out of tracks)
"/app/stream-queue.m3u" "/app/stream-queue.m3u"
) )
@ -34,24 +39,11 @@ radio_fallback = playlist.safe(
# Use main playlist, fall back to directory scan # Use main playlist, fall back to directory scan
radio = fallback(track_sensitive=false, [radio, radio_fallback]) radio = fallback(track_sensitive=false, [radio, radio_fallback])
# Add some audio processing # Simple crossfade for smooth transitions
# Use ReplayGain for consistent volume without pumping
radio = amplify(1.0, override="replaygain", radio)
# Add smooth crossfade between tracks (5 seconds)
radio = crossfade( radio = crossfade(
duration=5.0, # 5 second crossfade duration=3.0, # 3 second crossfade
fade_in=3.0, # 3 second fade in fade_in=2.0, # 2 second fade in
fade_out=3.0, # 3 second fade out fade_out=2.0, # 2 second fade out
radio
)
# Add a compressor to prevent clipping
radio = compress(
ratio=3.0, # Compression ratio
threshold=-15.0, # Threshold in dB
attack=50.0, # Attack time in ms
release=400.0, # Release time in ms
radio radio
) )

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: depends_on:
- icecast - icecast
volumes: volumes:
- ../music/library:/app/music:ro - ${MUSIC_LIBRARY:-../music/library}:/app/music:ro
- ./asteroid-radio-docker.liq:/app/asteroid-radio.liq: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 restart: unless-stopped
networks: networks:
- asteroid-network - 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 #+TITLE: Asteroid Radio - API Endpoints Reference
#+AUTHOR: Asteroid Radio Development Team #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-10 #+DATE: 2025-10-26
* Overview * Overview
@ -340,186 +340,6 @@ curl -X POST http://localhost:8080/api/asteroid/playlists/add-track \
} }
#+END_SRC #+END_SRC
* Stream Queue Control Endpoints (Admin Only)
** GET /api/asteroid/stream/queue
Get the current stream queue.
*** Authentication
Required (Admin role)
*** Response
#+BEGIN_SRC json
{
"status": "success",
"queue": [
{
"id": "track-id-123",
"title": "Track Name",
"artist": "Artist Name",
"album": "Album Name",
"duration": 245
}
],
"queueLength": 10
}
#+END_SRC
** POST /api/asteroid/stream/queue/add
Add a track to the stream queue.
*** Authentication
Required (Admin role)
*** Parameters
- =track-id= (required) - ID of the track to add
- =position= (optional) - "end" (default) or "next"
*** Example Request
#+BEGIN_SRC bash
# Add to end of queue
curl -X POST http://localhost:8080/api/asteroid/stream/queue/add \
-d "track-id=123" \
-b cookies.txt
# Add as next track
curl -X POST http://localhost:8080/api/asteroid/stream/queue/add \
-d "track-id=123&position=next" \
-b cookies.txt
#+END_SRC
*** Response
#+BEGIN_SRC json
{
"status": "success",
"message": "Track added to stream queue"
}
#+END_SRC
** POST /api/asteroid/stream/queue/remove
Remove a track from the stream queue.
*** Authentication
Required (Admin role)
*** Parameters
- =track-id= (required) - ID of the track to remove
*** Example Request
#+BEGIN_SRC bash
curl -X POST http://localhost:8080/api/asteroid/stream/queue/remove \
-d "track-id=123" \
-b cookies.txt
#+END_SRC
*** Response
#+BEGIN_SRC json
{
"status": "success",
"message": "Track removed from stream queue"
}
#+END_SRC
** POST /api/asteroid/stream/queue/clear
Clear the entire stream queue.
*** Authentication
Required (Admin role)
*** Response
#+BEGIN_SRC json
{
"status": "success",
"message": "Stream queue cleared"
}
#+END_SRC
** POST /api/asteroid/stream/queue/add-playlist
Add all tracks from a playlist to the stream queue.
*** Authentication
Required (Admin role)
*** Parameters
- =playlist-id= (required) - ID of the playlist to add
*** Example Request
#+BEGIN_SRC bash
curl -X POST http://localhost:8080/api/asteroid/stream/queue/add-playlist \
-d "playlist-id=5" \
-b cookies.txt
#+END_SRC
*** Response
#+BEGIN_SRC json
{
"status": "success",
"message": "Playlist added to stream queue",
"tracksAdded": 15
}
#+END_SRC
** POST /api/asteroid/stream/queue/reorder
Reorder the stream queue.
*** Authentication
Required (Admin role)
*** Parameters
- =track-ids= (required) - Comma-separated list of track IDs in desired order
*** Example Request
#+BEGIN_SRC bash
curl -X POST http://localhost:8080/api/asteroid/stream/queue/reorder \
-d "track-ids=123,456,789" \
-b cookies.txt
#+END_SRC
*** Response
#+BEGIN_SRC json
{
"status": "success",
"message": "Stream queue reordered"
}
#+END_SRC
** GET /api/asteroid/stream/history
Get recently played tracks from the stream.
*** Authentication
Required (Admin role)
*** Parameters
- =limit= (optional) - Number of tracks to return (default: 10)
*** Example Request
#+BEGIN_SRC bash
curl "http://localhost:8080/api/asteroid/stream/history?limit=20" \
-b cookies.txt
#+END_SRC
*** Response
#+BEGIN_SRC json
{
"status": "success",
"history": [
{
"id": "track-id-123",
"title": "Track Name",
"artist": "Artist Name",
"playedAt": "2025-10-16T10:30:00Z"
}
]
}
#+END_SRC
* Admin Endpoints * Admin Endpoints
** POST /api/asteroid/admin/scan-library ** POST /api/asteroid/admin/scan-library
@ -603,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. 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 * Rate Limiting
API endpoints implement rate limiting to prevent abuse. Excessive requests may result in temporary blocking. 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 #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-10 #+DATE: 2025-10-26
* Current Interfaces * Current Interfaces

View File

@ -1,638 +0,0 @@
#+TITLE: Asteroid Radio - Development Log
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade)
#+DATE: 2024-09-11 to 2025-10-16
* Introduction
This document chronicles the complete development history of Asteroid Radio, from initial concept to a fully-featured internet radio streaming platform. The project evolved from a simple design document into a sophisticated Common Lisp web application with professional streaming infrastructure.
** Project Vision
Asteroid Radio is designed to be the premier streaming platform for "Asteroid Music" - the perfect soundtrack for developers, hackers, and anyone who spends hours deep in code. The mission is to curate and deliver music that enhances focus, creativity, and the flow state.
** Key Contributors
- *Glenn Thompson (glenneth)* - Core development, API architecture, user management, testing
- *Brian O'Reilly (Fade)* - Project lead, infrastructure, CLIP templating, system architecture
- *Luis Pereira (easilok)* - Frontend development, UI/UX improvements, styling
* Phase 1: Project Foundation (September 2024)
** 2024-09-11: Initial Commit
- Project skeleton created
- Basic repository structure established
- Initial .gitignore configuration
** 2024-09-11: Design Document
- Added comprehensive design document (design.org)
- Defined project goals and architecture
- Outlined feature roadmap for internet radio station
- Established technical requirements
** 2024-09-11: Hunchentoot Implementation
- Implemented initial web server using Hunchentoot
- Basic HTTP server functionality
- Foundation for web interface
* Phase 2: RADIANCE Migration (September 2024)
** 2024-09-11: Framework Migration
- Migrated from Hunchentoot to RADIANCE web framework
- Reorganized system around Radiance build conventions
- Established proper module structure
- Set up template directory structure
** 2024-09-11: LASS CSS System
- Implemented LASS for dynamic CSS generation
- Fixed LASS compilation issues
- Enabled programmatic stylesheet generation
- Established dark hacker theme aesthetic
** 2024-09-11: Build System
- Created build-executable.lisp for deployment
- Configured custom SBCL paths
- Set up Makefile for build automation
- Removed build artifacts from version control
* Phase 3: CLIP Templating (September 2024)
** 2024-09-11: Template System
- Began CLIP templating implementation
- Created initial template structure
- Established template conventions
- Set up dynamic content rendering
** 2024-09-11: Complete Template Migration
- Completed CLIP template refactoring
- Migrated all pages to CLIP system
- Implemented template inheritance
- Established consistent page structure
* Phase 4: Database Integration (September-October 2024)
** 2024-09-11: RADIANCE Database
- Implemented RADIANCE database integration
- Set up database abstraction layer
- Created track storage schema
- Established query patterns
** 2024-09-11: Metadata Extraction
- Implemented complete metadata extraction
- Added taglib integration for audio files
- Extracted title, artist, album, duration, bitrate
- Automated metadata processing pipeline
** 2024-09-11: Internet Radio System
- Implemented complete internet radio streaming system
- Integrated Icecast2 streaming server
- Set up Liquidsoap audio pipeline
- Established continuous broadcasting
** 2024-10-01: Database Refactoring
- Refactored database features into discrete files
- Improved code organization
- Separated concerns for maintainability
- Created modular file structure
* Phase 5: User Management System (October 2024)
** 2024-10-01: User System Foundation
- Added user management dependencies
- Created users.lisp module
- Implemented user profile system
- Set up authentication framework
** 2024-10-01: Authentication System
- Fixed Asteroid Radio authentication system
- Implemented secure password hashing
- Created session management
- Added login/logout functionality
** 2024-10-01: User Management API
- Fixed user management API authentication
- Implemented proper data formatting
- Created user administration endpoints
- Added role-based access control
** 2024-10-01: Recursive Music Scanning
- Implemented recursive directory scanning
- Added implicit depth-2 music discovery
- Improved library management
- Automated music file detection
* Phase 6: Docker Infrastructure (October 2024)
** 2024-10-02: Docker Streaming
- Added Docker streaming infrastructure
- Created Liquidsoap container configuration
- Set up Icecast2 container
- Established container networking
** 2024-10-02: Docker Integration
- Completed Docker streaming infrastructure
- Fixed user management integration
- Created docker-compose.yml
- Established volume mounts
** 2024-10-02: Web Interface Integration
- Completed Docker streaming integration with web interface
- Connected frontend to streaming backend
- Implemented status monitoring
- Added stream metadata display
** 2024-10-02: Docker Compose V2
- Fixed Docker Compose V2 compatibility
- Updated start/stop scripts
- Modernized container orchestration
- Improved deployment process
* Phase 7: AAC Streaming (October 2024)
** 2024-10-02: AAC Support
- Added AAC streaming support
- Implemented quality selector
- Created multiple stream formats
- Improved audio efficiency
** 2024-10-02: Icecast Configuration
- Added Icecast mount configurations for all streams
- Configured MP3 128kbps stream
- Configured AAC 96kbps stream
- Configured MP3 64kbps low-bandwidth stream
** 2024-10-02: Stream Metadata
- Restored live stream metadata display
- Fixed metadata extraction from Icecast
- Implemented real-time track info
- Added "now playing" functionality
** 2024-10-02: Documentation
- Converted AAC-STREAMING.md to org-mode format
- Updated streaming documentation
- Added multi-format stream guides
- Documented quality options
* Phase 8: Docker Refinement (October 2024)
** 2024-10-02: Docker Configuration
- Updated Docker configuration for improved streaming
- Optimized container settings
- Improved volume management
- Enhanced networking setup
** 2024-10-02: Utility Scripts
- Restored Docker utility scripts per Fade's request
- Created start-streaming.sh
- Created stop-streaming.sh
- Added test-streaming.sh
** 2024-10-02: Gitignore Updates
- Added shell script exclusion to gitignore
- Cleaned up version control
- Removed build artifacts
- Improved repository hygiene
* Phase 9: UI Improvements (October 2024)
** 2024-10-03: Color Scheme
- Updated color scheme from green to blue theme
- Implemented consistent color palette
- Improved visual hierarchy
- Enhanced dark theme aesthetics
** 2024-10-03: Template Features
- Completed Templates section
- Implemented CLIP refactoring
- Added user management templates
- Created pagination templates
- Built playlist templates
- Fixed UI issues
- Documented PostgreSQL setup
** 2024-10-03: Status Monitoring
- Added auto-scan on startup
- Implemented live Icecast status checks
- Added live Liquidsoap status checks
- Created admin dashboard monitoring
* Phase 10: JavaScript Modularization (October 2024)
** 2024-10-04: Code Organization
- Moved admin JavaScript code to own file (admin.js)
- Moved frontpage JavaScript to own file
- Moved player JavaScript to own file (player.js)
- Moved users JavaScript to own file
- Improved code maintainability
** 2024-10-04: User Management Page
- Added user management page
- Created admin interface for users
- Implemented user listing
- Added role management UI
* Phase 11: Styling Improvements (October 2024)
** 2024-10-05: LASS Fixes
- Fixed: Move font import to LASS file
- Fixed: LASS rules moved up one level
- Fixed: Pseudo selectors now working in LASS
- Improved CSS compilation
** 2024-10-05: Live Player Styling
- Fixed sizing of live player
- Improved player responsiveness
- Enhanced player controls
- Better mobile layout
** 2024-10-05: Navigation Styling
- Used nav styles on front page
- Implemented consistent navigation
- Improved menu appearance
- Enhanced user experience
* Phase 12: Playlist System (October 2024)
** 2024-10-04: Schema Fix
- Fixed playlist schema mismatch
- Used track-ids field consistently
- Resolved data structure issues
- Improved playlist reliability
* Phase 13: User Profiles (October 2024)
** 2024-10-06: Profile Pages
- Added user profile page with CLIP template styling
- Implemented profile edit functionality
- Updated profile page to match site-wide layout
- Created consistent styling
** 2024-10-06: Authentication UI
- Added user registration UI improvements
- Enhanced authentication interface
- Fixed auth form styling (wider 600px forms)
- Hidden message boxes for cleaner UI
* Phase 14: API Architecture (October 2024)
** 2024-10-07: API-Aware Authentication
- WIP: Added API-aware authentication
- Implemented detection for API routes
- Fixed execution flow issues
- Completed API-aware authentication returning JSON
** 2024-10-07: API Output Refactoring
- Fixed api-output usage
- Passed structured data with :status and :message
- Standardized API responses
- Improved error handling
* Phase 15: API Refactoring (October 2024)
** 2024-10-08: Define-API Migration
- Refactored API endpoints to use Radiance's define-api macro
- Modernized API architecture
- Improved endpoint consistency
- Better integration with Radiance framework
** 2024-10-08: Frontend Integration
- Fixed frontend JavaScript to work with define-api endpoints
- Updated AJAX calls
- Improved error handling
- Enhanced user feedback
** 2024-10-08: Automated Testing
- Added comprehensive automated test suite
- Created test-server.sh
- Implemented API endpoint testing
- Added integration tests
* Phase 16: Telnet Controls (October 2024)
** 2024-10-08: Liquidsoap DJ Controls
- Added Liquidsoap DJ controls via telnet integration
- Implemented remote control interface
- Created telnet command system
- Enabled live stream manipulation
* Phase 17: Merge Conflicts and Integration (October 2024)
** 2024-10-09: Track-IDs Integration
- Resolved merge conflict
- Integrated track-ids fix with api-output refactoring
- Maintained data consistency
- Preserved both feature sets
* Phase 18: Navigation Improvements (October 2024)
** 2024-10-09: Navbar Enhancement
- Improved navbar in all pages
- Enhanced nav styling
- Created consistent navigation experience
- Better responsive behavior
* Phase 19: Documentation Updates (October 2024)
** 2024-10-10: API Documentation
- Updated documentation authors to Asteroid Radio Development Team
- Documentation cleanup: removed outdated files
- Added API docs
- Updated core documentation
- Created API-REFERENCE.org
- Created API-ENDPOINTS.org
** 2024-10-10: Testing Documentation
- Updated TESTING.org
- Documented test suite
- Added testing examples
- Improved test coverage documentation
** 2024-10-10: Template Variables
- Made stream base URL variable in templates
- Improved configuration flexibility
- Better environment handling
* Phase 20: System Configuration (October 2024)
** 2024-10-11: Parallel Processing
- Added dependency to run music scan in parallel
- Improved performance
- Faster library scanning
- Better resource utilization
** 2024-10-11: TODO Updates
- Updated TODO with UI items
- Tracked remaining work
- Prioritized features
** 2024-10-12: Configuration Documentation
- Added file for notes on application configuration
- Documented IRC log of Shinmera chat
- Preserved configuration discussions
- Improved setup documentation
** 2024-10-12: Code Documentation
- Added documentation string in scan-directory-for-music-recursively
- Improved code readability
- Better function documentation
* Phase 21: Page Flow System (October 2024)
** 2024-10-12: Role-Based Flow
- Implemented role-based page flow
- Created user management APIs
- Added admin/user routing
- Improved user experience
** 2024-10-12: UI Fixes
- Completed UI fixes for page flow feature
- Fixed navigation issues
- Improved page transitions
- Enhanced user feedback
** 2024-10-12: Documentation
- Marked Page Flow feature as complete in TODO
- Added session notes for page flow implementation
- Documented implementation details
** 2024-10-12: README Update
- Updated README.org file structure
- Reflected current project state
- Improved documentation accuracy
* Phase 22: Stream Queue Control (October 2025)
** 2025-10-14: Queue System Foundation
- Added stream queue control system
- Implemented in-memory queue management
- Created stream-queue.m3u generation
- Integrated with Liquidsoap
** 2025-10-14: Admin UI
- Added admin UI for stream queue management
- Created queue control interface
- Implemented drag-and-drop (planned)
- Added queue visualization
** 2025-10-14: Audio Quality Improvements
- Improved audio quality and streaming performance
- Replaced normalize() with ReplayGain
- Added crossfading (5 seconds)
- Implemented dynamic compression
- Eliminated volume pumping issues
** 2025-10-14: Player Improvements
- Improved player UI
- Reduced buffering
- Enhanced playback controls
- Better user experience
* Phase 23: UI Polish (October 2025)
** 2025-10-14: Browser Compatibility
- Fixed scrollbars only visible when required on Chrome browsers
- Improved cross-browser compatibility
- Better CSS handling
** 2025-10-14: Responsive Design
- Fixed playlist create button wrap on small screens
- Improved mobile experience
- Enhanced responsive layout
** 2025-10-14: Icecast Integration
- Fixed: avoid Icecast XML shown on frontend when there is no artist
- Improved error handling
- Better metadata display
* Phase 24: Documentation Overhaul (October 2025)
** 2025-10-16: Comprehensive Update
- Comprehensive documentation update for current features
- Updated README.org with all current capabilities
- Documented stream queue control system
- Added ReplayGain documentation
- Updated multi-format streaming docs
- Documented user management system
- Complete API endpoint reference
- Updated database schema documentation
** 2025-10-16: API Documentation
- Added complete stream queue control API section
- Documented all 7 stream queue endpoints
- Added authentication requirements
- Included request/response examples
** 2025-10-16: Docker Documentation
- Updated Liquidsoap config documentation
- Documented stream queue integration
- Added queue management examples
- Updated audio processing details
** 2025-10-16: Project Overview
- Updated PROJECT-OVERVIEW with stream queue
- Added ReplayGain mentions
- Updated feature list
- Refreshed project vision
* Technical Milestones
** Architecture Evolution
1. *Hunchentoot**RADIANCE* (Major framework migration)
2. *Simple HTML**CLIP Templates* (Dynamic templating)
3. *File-based**PostgreSQL Database* (Persistent storage)
4. *Single stream**Multi-format streaming* (AAC, MP3 high/low)
5. *Random playback**Queue control* (Curated broadcasting)
** Audio Processing Evolution
1. *Basic amplify**Normalize**ReplayGain* (Volume consistency)
2. *No crossfade**5-second crossfade* (Smooth transitions)
3. *No compression**Dynamic compression* (Clipping prevention)
** API Evolution
1. *Custom routes**define-api macro* (Standardization)
2. *Mixed responses**JSON API* (Consistent format)
3. *No auth**Session-based auth* (Security)
4. *Basic endpoints**20+ endpoints* (Comprehensive API)
** Infrastructure Evolution
1. *Native installation**Docker containers* (Easy deployment)
2. *Single Icecast**Icecast + Liquidsoap* (Professional streaming)
3. *No telnet**Telnet control* (Live DJ operations)
* Feature Summary
** Core Features Implemented
- ✅ User authentication and registration
- ✅ Role-based access control (admin/user)
- ✅ Music library management with metadata
- ✅ User playlists (create, edit, delete)
- ✅ Stream queue control (admin only)
- ✅ Multi-format streaming (AAC 96k, MP3 128k/64k)
- ✅ ReplayGain audio processing
- ✅ Crossfading and compression
- ✅ Web player with controls
- ✅ Live stream metadata display
- ✅ Admin dashboard
- ✅ REST API (20+ endpoints)
- ✅ Automated testing suite
- ✅ Docker deployment
- ✅ Telnet DJ controls
- ✅ PostgreSQL database
- ✅ Responsive design
- ✅ Dark hacker theme
** Planned Features
- 🔄 WebSocket real-time updates
- 🔄 Drag-and-drop queue management
- 🔄 Social features (playlist sharing)??
- 🔄 Advanced search and filtering
- 🔄 Listener statistics
- 🔄 Scheduled programming
- 🔄 Auto-queue filling
- 🔄 Genre-based smart queues
* Development Statistics
** Commit Count by Phase
- Phase 1-2 (Foundation): ~15 commits
- Phase 3-4 (Templates/DB): ~10 commits
- Phase 5-6 (Users/Docker): ~20 commits
- Phase 7-9 (Streaming): ~15 commits
- Phase 10-14 (UI/Profiles): ~25 commits
- Phase 15-17 (API): ~15 commits
- Phase 18-21 (Polish): ~20 commits
- Phase 22-24 (Queue/Docs): ~10 commits
** Key Contributors Statistics
- Glenn Thompson: ~70 commits (Core development, API, testing)
- Brian O'Reilly: ~40 commits (Infrastructure, architecture)
- Luis Pereira: ~20 commits (Frontend, UI/UX)
** Technology Stack
- *Language*: Common Lisp (SBCL)
- *Framework*: RADIANCE
- *Database*: PostgreSQL
- *Templating*: CLIP
- *CSS*: LASS
- *Streaming*: Icecast2 + Liquidsoap
- *Containers*: Docker + Docker Compose
- *Audio*: ReplayGain, crossfade, compression
* Lessons Learned
** Framework Migration
The migration from Hunchentoot to RADIANCE was challenging but worthwhile. RADIANCE's module system and database abstraction provided better structure for a growing application.
** Audio Processing
The evolution from normalize() to ReplayGain solved the volume pumping issue. ReplayGain uses track metadata for consistent volume without dynamic compression artifacts.
** API Design
Migrating to Radiance's define-api macro standardized our API and improved maintainability. Consistent JSON responses made frontend integration much easier.
** Docker Deployment
Containerizing the streaming infrastructure simplified deployment and made the system more portable. The separation of concerns between web app and streaming services improved reliability.
** Testing
Adding automated tests early would have caught integration issues faster. The comprehensive test suite added in Phase 15 significantly improved code quality.
* Future Directions
** Short Term (Next 3 Months?)
- WebSocket integration for real-time updates
- Drag-and-drop queue management UI
- Telnet integration from web interface
- Listener statistics dashboard
** Medium Term (6-12 Months)
- Social features (playlist sharing, discovery)
- Advanced search with full-text indexing
- Mobile native applications
- Scheduled programming system
** Long Term (12+ Months)
- Multi-station support
- Federation with other Asteroid Radio instances
- Machine learning for music recommendations
- Live DJ streaming capabilities
* Conclusion
Asteroid Radio has evolved from a simple design document into a sophisticated internet radio platform. The journey from Hunchentoot to RADIANCE, from file-based storage to PostgreSQL, from single-stream to multi-format broadcasting, and from random playback to curated queue control demonstrates the power of iterative development and continuous improvement.
The project successfully combines the elegance of Common Lisp with modern web technologies, creating a platform that's both technically impressive and user-friendly. The dark hacker aesthetic, professional audio processing, and comprehensive API make Asteroid Radio a unique contribution to the internet radio landscape.
Most importantly, Asteroid Radio achieves its core mission: providing the perfect soundtrack for developers, hackers, and anyone who spends hours deep in code.
* Appendix: Key Files
** Core Application
- =asteroid.lisp= - Main application with RADIANCE routes
- =asteroid.asd= - System definition
- =stream-control.lisp= - Queue management
- =stream-media.lisp= - Media streaming
- =user-management.lisp= - User authentication
- =playlist-management.lisp= - Playlist operations
- =auth-routes.lisp= - Authentication endpoints
** Configuration
- =docker/docker-compose.yml= - Container orchestration
- =docker/asteroid-radio-docker.liq= - Liquidsoap configuration
- =docker/icecast.xml= - Icecast server config
** Frontend
- =static/asteroid.lass= - Stylesheet source
- =static/js/admin.js= - Admin interface (289 lines)
- =static/js/player.js= - Web player
- =template/*.chtml= - CLIP templates
** Documentation
- =README.org= - Project overview
- =docs/STREAM-CONTROL.org= - Queue management guide
- =docs/API-ENDPOINTS.org= - Complete API reference
- =docs/DOCKER-STREAMING.org= - Streaming setup
- =docs/DEV-LOG.org= - This document
---
*End of Development Log*
Last Updated: 2025-10-16

View File

@ -1,9 +1,13 @@
#+TITLE: Asteroid Radio - Development Guide #+TITLE: Asteroid Radio - Development Guide
#+AUTHOR: Asteroid Radio Development Team #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-10 #+DATE: 2025-10-26
* Development Setup * 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 ** Prerequisites
*** System Dependencies *** System Dependencies
@ -68,7 +72,7 @@ sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --quit
*** Clone Repository *** Clone Repository
#+BEGIN_SRC bash #+BEGIN_SRC bash
git clone <repository-url> git clone https://github.com/fade/asteroid.git
cd asteroid cd asteroid
#+END_SRC #+END_SRC
@ -125,9 +129,9 @@ sbcl --eval "(ql:quickload :asteroid)" --eval "(asteroid:start-server)"
** Music Library Management ** Music Library Management
*** Directory Structure *** Directory Structure
The music directory structure is: The music directory is located directly under the asteroid root directory:
#+BEGIN_SRC #+BEGIN_SRC
asteroid/docker/music/ # Host directory (mounted to containers) asteroid/music/ # Music directory (can be symlink)
├── artist1/ ├── artist1/
│ ├── album1/ │ ├── album1/
│ │ ├── track1.mp3 │ │ ├── track1.mp3
@ -138,6 +142,11 @@ asteroid/docker/music/ # Host directory (mounted to containers)
└── single.wav └── single.wav
#+END_SRC #+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 *** Recursive Scanning Capabilities
The Asteroid application includes built-in recursive directory scanning: The Asteroid application includes built-in recursive directory scanning:
- *Function*: =scan-music-library= in =stream-media.lisp= - *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 *** Adding Music to Development Environment
#+BEGIN_SRC bash #+BEGIN_SRC bash
# Option 1: Copy music files directly # 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: # Edit docker-compose.yml to change volume mount:
# volumes: # volumes:
# - /mnt/remote-music:/app/music:ro # - /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 # Trigger library scan via API
curl -X POST http://localhost:8080/api/asteroid/admin/scan-library curl -X POST http://localhost:8080/api/asteroid/admin/scan-library
#+END_SRC #+END_SRC

View File

@ -1,17 +1,14 @@
#+TITLE: Asteroid Radio - Docker Streaming Setup #+TITLE: Asteroid Radio - Docker Streaming Setup
#+AUTHOR: Docker Team #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-03 #+DATE: 2025-10-26
* Docker Streaming Overview * 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. 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.
** Key Features #+BEGIN_QUOTE
- Stream queue control system for curated playlists *Note on Package Managers*: Examples use =apt= (Debian/Ubuntu). Replace with your distribution's package manager (=dnf=, =pacman=, =zypper=, =apk=, etc.).
- ReplayGain audio processing for consistent volume #+END_QUOTE
- Automatic fallback to random playback
- Multi-format streaming (AAC, MP3 high/low)
- Telnet control interface for live DJ operations
* Architecture * Architecture
@ -25,12 +22,6 @@ This guide covers the complete Docker-based streaming setup for Asteroid Radio u
- *High Quality AAC*: 96kbps AAC stream at /asteroid.aac (better efficiency than MP3) - *High Quality AAC*: 96kbps AAC stream at /asteroid.aac (better efficiency than MP3)
- *Low Quality MP3*: 64kbps MP3 stream at /asteroid-low.mp3 (compatibility) - *Low Quality MP3*: 64kbps MP3 stream at /asteroid-low.mp3 (compatibility)
** Audio Processing
- *ReplayGain*: Consistent volume without pumping artifacts
- *Crossfading*: Smooth 5-second transitions between tracks
- *Compression*: Dynamic compression to prevent clipping
- *Fallback*: Emergency sine wave if all sources fail
** Network Configuration ** Network Configuration
- *Icecast2*: Port 8000 (streaming and admin) - *Icecast2*: Port 8000 (streaming and admin)
- *Liquidsoap Telnet*: Port 1234 (remote control) - *Liquidsoap Telnet*: Port 1234 (remote control)
@ -50,7 +41,7 @@ sudo usermod -a -G docker $USER
** One-Command Setup ** One-Command Setup
#+BEGIN_SRC bash #+BEGIN_SRC bash
# Clone and start # Clone and start
git clone <repository-url> asteroid-radio git clone https://github.com/fade/asteroid asteroid-radio
cd asteroid-radio/docker cd asteroid-radio/docker
docker compose up -d docker compose up -d
#+END_SRC #+END_SRC
@ -100,7 +91,6 @@ services:
volumes: volumes:
- ./music:/app/music:ro - ./music:/app/music:ro
- ./asteroid-radio-docker.liq:/app/asteroid-radio.liq:ro - ./asteroid-radio-docker.liq:/app/asteroid-radio.liq:ro
- ../stream-queue.m3u:/app/stream-queue.m3u:ro # Stream queue control
restart: unless-stopped restart: unless-stopped
networks: networks:
- asteroid-network - asteroid-network
@ -222,42 +212,20 @@ settings.server.telnet.set(true)
settings.server.telnet.port.set(1234) settings.server.telnet.port.set(1234)
settings.server.telnet.bind_addr.set("0.0.0.0") settings.server.telnet.bind_addr.set("0.0.0.0")
# Create playlist source from managed stream queue # Create playlist source from mounted music directory
radio = playlist( radio = playlist(
mode="normal", # Play in order (not randomized) mode="randomize",
reload=5, # Check for playlist updates every 5 seconds reload=3600,
reload_mode="watch", # Watch file for changes reload_mode="watch",
"/app/stream-queue.m3u"
)
# Fallback to directory scan if queue is empty
radio_fallback = playlist.safe(
mode="randomize",
reload=3600,
"/app/music/" "/app/music/"
) )
radio = fallback(track_sensitive=false, [radio, radio_fallback]) # Add some audio processing
radio = amplify(1.0, radio)
radio = normalize(radio)
# Use ReplayGain for consistent volume without pumping # Add crossfade between tracks
radio = amplify(1.0, override="replaygain", radio) radio = crossfade(radio)
# Add smooth crossfade between tracks (5 seconds)
radio = crossfade(
duration=5.0,
fade_in=3.0,
fade_out=3.0,
radio
)
# Add compressor to prevent clipping
radio = compress(
ratio=3.0,
threshold=-15.0,
attack=50.0,
release=400.0,
radio
)
# Create a fallback with emergency content # Create a fallback with emergency content
emergency = sine(440.0) emergency = sine(440.0)
@ -473,36 +441,6 @@ docker compose logs --tail=10 liquidsoap
#+END_SRC #+END_SRC
* Stream Queue Control
** Overview
The Docker setup integrates with Asteroid's stream queue control system, allowing you to curate exactly what plays on the broadcast stream.
** How It Works
1. Asteroid web app manages =stream-queue.m3u= file in the project root
2. File is mounted into Liquidsoap container at =/app/stream-queue.m3u=
3. Liquidsoap watches the file and reloads every 5 seconds
4. When queue is empty, falls back to random playback from music directory
** Managing the Queue
Use the Asteroid web API or admin interface to control the stream queue:
#+BEGIN_SRC bash
# Add track to queue (requires admin authentication)
curl -X POST http://localhost:8080/api/asteroid/stream/queue/add \
-d "track-id=42" \
-b cookies.txt
# View current queue
curl http://localhost:8080/api/asteroid/stream/queue -b cookies.txt
# Clear queue (falls back to random)
curl -X POST http://localhost:8080/api/asteroid/stream/queue/clear \
-b cookies.txt
#+END_SRC
See =docs/STREAM-CONTROL.org= for complete queue management documentation.
* Volume Management * Volume Management
** Music Library Setup ** Music Library Setup

View File

@ -1,11 +1,19 @@
#+TITLE: Asteroid Radio - Installation Guide #+TITLE: Asteroid Radio - Installation Guide
#+AUTHOR: Installation Team #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-03 #+DATE: 2025-10-26
* Installation Overview * 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. 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) * Quick Start (Docker - Recommended)
** Prerequisites Check ** Prerequisites Check
@ -18,8 +26,8 @@ docker info
** One-Command Setup ** One-Command Setup
#+BEGIN_SRC bash #+BEGIN_SRC bash
# Clone and setup (replace with actual repository URL) # Clone and setup
git clone <repository-url> asteroid-radio git clone https://github.com/fade/asteroid.git asteroid-radio
cd asteroid-radio/docker cd asteroid-radio/docker
docker compose up -d docker compose up -d
#+END_SRC #+END_SRC
@ -201,8 +209,8 @@ sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --eval "(ql:a
*** Step 5: Clone and Setup Project *** Step 5: Clone and Setup Project
#+BEGIN_SRC bash #+BEGIN_SRC bash
# Clone repository (replace with actual URL) # Clone repository
git clone <repository-url> /opt/asteroid-radio git clone https://github.com/fade/asteroid /opt/asteroid-radio
cd /opt/asteroid-radio cd /opt/asteroid-radio
# Create required directories # Create required directories
@ -380,7 +388,11 @@ sudo systemctl reload nginx
* Docker Management * 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 #+BEGIN_SRC bash
# Start services # Start services
docker compose up -d docker compose up -d
@ -395,11 +407,42 @@ docker compose logs -f
docker compose restart docker compose restart
#+END_SRC #+END_SRC
** Docker Configuration *** Docker Configuration
See =docker/docker-compose.yml= for complete Docker setup with Icecast2 and Liquidsoap containers. The setup includes: See =docker/docker-compose.yml= for complete Docker setup with Icecast2 and Liquidsoap containers. The setup includes:
- **Icecast2**: Streaming server with three output formats - **Icecast2**: Streaming server with three output formats
- **Liquidsoap**: Audio processing and stream generation - **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 * Initial Configuration
@ -537,7 +580,7 @@ chmod +x ~/asteroid-radio/health-check.sh
- Test stream connectivity from different networks - Test stream connectivity from different networks
** Getting Support ** Getting Support
- Check project documentation and FAQ - Check project documentation
- Review system logs for error messages - Review system logs for error messages
- Submit issues with detailed system information - Submit issues with detailed system information
- Join our IRC chat room: **#asteroid.music** on **irc.libera.chat** - Join our IRC chat room: **#asteroid.music** on **irc.libera.chat**

View File

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

View File

@ -1,6 +1,6 @@
#+TITLE: PostgreSQL Setup for Asteroid Radio #+TITLE: PostgreSQL Setup for Asteroid Radio
#+AUTHOR: Asteroid Radio Development Team #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-04 #+DATE: 2025-10-26
* Overview * 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 #+TITLE: Asteroid Radio - Project Overview
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade) #+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade)
#+DATE: 2025-10-16 #+DATE: 2025-10-26
* 🎯 Mission * 🎯 Mission
@ -38,7 +38,8 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack
- **Common Lisp** (SBCL) - Core application language - **Common Lisp** (SBCL) - Core application language
- **Radiance Framework** - Web framework and module system - **Radiance Framework** - Web framework and module system
- **LASS** - CSS preprocessing in Lisp - **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:** **Frontend:**
- **HTML5** with semantic templates - **HTML5** with semantic templates
@ -75,28 +76,32 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack
## 🚀 Features ## 🚀 Features
### Current 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 - ✅ **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 - ✅ **User Playlists** - Create, manage, and play personal music collections
- ✅ **Stream Queue Control** - Curated broadcast queue with admin controls - ✅ **Multiple Player Modes** - Inline, pop-out, and persistent frameset players
- ✅ **Web Player** - Browser-based player with queue management - ✅ **Stream Queue Control** - Admin control over broadcast stream queue (M3U-based)
- ✅ **REST API** - Comprehensive JSON API with 20+ endpoints - ✅ **REST API** - Comprehensive JSON API with 15+ endpoints
- ✅ **Multi-Format Streaming** - AAC 96kbps, MP3 128kbps/64kbps - ✅ **Music Streaming** - Multiple quality formats (128k MP3, 96k AAC, 64k MP3)
- ✅ **ReplayGain Processing** - Consistent volume without pumping
- ✅ **Rate Limiting** - Anti-abuse protection - ✅ **Rate Limiting** - Anti-abuse protection
- ✅ **Docker Integration** - Icecast2/Liquidsoap streaming infrastructure - ✅ **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 - ✅ **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 - ✅ **Responsive Design** - Works on desktop and mobile
- ✅ **Automated Testing** - Comprehensive test suite - ✅ **Automated Testing** - Comprehensive test suite
### Planned Features ### 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 - 🔄 **Social Features** - Playlist sharing and discovery
- 🔄 **Advanced Search** - Full-text search and filtering - 🔄 **Advanced Search** - Full-text search and filtering
- 🔄 **Mobile App** - Native mobile applications - 🔄 **Mobile App** - Native mobile applications
- 🔄 **WebSocket Support** - Real-time updates - 🔄 **WebSocket Support** - Real-time updates
- 🔄 **Analytics** - Listening statistics and insights - 🔄 **Analytics** - Listening statistics and insights
- 🔄 **Scheduled Programming** - Time-based queue switching
## 🔮 Vision ## 🔮 Vision
@ -111,10 +116,9 @@ Asteroid Radio is the premier streaming platform for **Asteroid Music** - the pe
**Platform Features:** **Platform Features:**
- **Multi-Format Streaming** - High-quality AAC, MP3 128k, and MP3 64k streams - **Multi-Format Streaming** - High-quality AAC, MP3 128k, and MP3 64k streams
- **Stream Queue Control** - Curate exactly what plays on the broadcast
- **User Community** - Accounts, playlists, and sharing among fellow developers - **User Community** - Accounts, playlists, and sharing among fellow developers
- **Developer-Friendly** - Built with Common Lisp, fully hackable and extensible - **Developer-Friendly** - Built with Common Lisp, fully hackable and extensible
- **Professional Quality** - ReplayGain, crossfading, compression, metadata, and telnet control - **Professional Quality** - Crossfading, normalization, metadata, and telnet control
- **Always-On Broadcasting** - Continuous streams perfect for long coding sessions - **Always-On Broadcasting** - Continuous streams perfect for long coding sessions
Asteroid Radio isn't just another music platform - it's the soundtrack to the hacker lifestyle, designed by hackers for hackers who understand that the right music can make the difference between good code and great code. Asteroid Radio isn't just another music platform - it's the soundtrack to the hacker lifestyle, designed by hackers for hackers who understand that the right music can make the difference between good code and great code.

View File

@ -1,6 +1,6 @@
#+TITLE: Asteroid Radio - Documentation Index #+TITLE: Asteroid Radio - Documentation Index
#+AUTHOR: Asteroid Radio Development Team #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-10 #+DATE: 2025-10-26
* Welcome to Asteroid Radio Documentation * Welcome to Asteroid Radio Documentation
@ -19,6 +19,9 @@ For immediate setup, see:
*** [[file:PROJECT-OVERVIEW.org][Project Overview]] *** [[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. 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]] *** [[file:INSTALLATION.org][Installation Guide]]
Comprehensive installation instructions for multiple operating systems, including system requirements, dependencies, and production deployment considerations. 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 ** What's Working Now
- **Web Application**: Full-featured web interface with authentication - **Web Application**: Full-featured web interface with authentication
- **REST API**: JSON API with 15+ endpoints for programmatic access - **REST API**: JSON API with 15+ endpoints for programmatic access
- **User Management**: Registration, login, roles, and profiles - **User Management**: Registration, login, roles (Admin/DJ/Listener), and profiles
- **Music Library**: Track management with pagination and search - **Music Library**: Track management with pagination, search, and filtering
- **Playlists**: User playlists with creation and playback - **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 - **Docker Streaming Infrastructure**: Icecast2 + Liquidsoap containers
- **Three Quality Streams**: 128kbps MP3, 96kbps AAC, 64kbps MP3 - **Three Quality Streams**: 128kbps MP3, 96kbps AAC, 64kbps MP3
- **Admin Interface**: Icecast web admin at http://localhost:8000/admin/ - **Admin Interface**: Icecast web admin at http://localhost:8000/admin/
- **Liquidsoap DJ Controls**: Telnet control via localhost:1234 - **Liquidsoap DJ Controls**: Telnet control via localhost:1234
- **Professional Features**: Crossfading, normalization, metadata support - **Professional Features**: Crossfading, ReplayGain normalization, metadata support
- **PostgreSQL Database**: Persistent data storage with full CRUD operations - **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) ** Stream URLs (when running)
- **High Quality MP3**: http://localhost:8000/asteroid.mp3 (128kbps) - **High Quality MP3**: http://localhost:8000/asteroid.mp3 (128kbps)
@ -125,15 +131,15 @@ Asteroid Radio uses a modern, containerized architecture:
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
│ Asteroid Radio Platform │ │ Asteroid Radio Platform │
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ Streaming Infrastructure (Docker) │ │ Streaming Infrastructure (Docker)
│ ├── Icecast2 (HTTP Streaming Server) │ │ ├── Icecast2 (HTTP Streaming Server)
│ ├── Liquidsoap (Audio Processing Pipeline) │ │ ├── Liquidsoap (Audio Processing Pipeline)
│ └── Multiple Format Support (AAC, MP3) │ │ └── Multiple Format Support (AAC, MP3)
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ Control Interfaces │ │ Control Interfaces
│ ├── Icecast Admin Web Interface │ │ ├── Icecast Admin Web Interface
│ ├── Liquidsoap Telnet Control │ │ ├── Liquidsoap Telnet Control
│ └── Docker Container Management │ │ └── Docker Container Management
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
#+END_EXAMPLE #+END_EXAMPLE
@ -141,5 +147,5 @@ For detailed technical information, see the **[[file:PROJECT-OVERVIEW.org][Proje
--- ---
*Last Updated: 2025-10-10* *Last Updated: 2025-10-26*
*Documentation Version: 2.0* *Documentation Version: 3.0*

View File

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

View File

@ -1,11 +1,15 @@
#+TITLE: Asteroid Radio Testing Guide #+TITLE: Asteroid Radio Testing Guide
#+AUTHOR: Asteroid Radio Development Team #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-08 #+DATE: 2025-10-26
* Overview * Overview
This document describes the automated testing system for Asteroid Radio. 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 * Test Script
The =test-server.sh= script provides comprehensive testing of all server endpoints and functionality. 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 #+TITLE: Track Pagination System - Complete
#+AUTHOR: Asteroid Radio Development Team #+AUTHOR: Asteroid Radio Development Team
#+DATE: 2025-10-04 #+DATE: 2025-10-26
* Overview * Overview

View File

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

70
frontend-partials.lisp Normal file
View File

@ -0,0 +1,70 @@
(in-package :asteroid)
(defun icecast-now-playing (icecast-base-url)
(let* ((icecast-url (concatenate 'string icecast-base-url "/admin/stats.xml"))
(response (drakma:http-request icecast-url
:want-stream nil
:basic-authorization '("admin" "asteroid_admin_2024"))))
(when response
(let ((xml-string (if (stringp response)
response
(babel:octets-to-string response :encoding :utf-8))))
;; Simple XML parsing to extract source information
;; Look for <source mount="/asteroid.mp3"> sections and extract title, listeners, etc.
(multiple-value-bind (match-start match-end)
(cl-ppcre:scan "<source mount=\"/asteroid\\.mp3\">" xml-string)
(if match-start
(let* ((source-section (subseq xml-string match-start
(or (cl-ppcre:scan "</source>" xml-string :start match-start)
(length xml-string))))
(titlep (cl-ppcre:all-matches "<title>" source-section))
(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"))
(:title . ,title)
(:listeners . ,(parse-integer listeners :junk-allowed t))))
`((:listenurl . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
(: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))))
(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))
:stats now-playing-stats))
(progn
(setf (header "Content-Type") "text/html")
(clip:process-to-string
(plump:parse (alexandria:read-file-into-string template-path))
:connection-error t
:stats nil))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error loading profile: ~a" e)))
:status 500))))
(define-api asteroid/partial/now-playing-inline () ()
"Get inline text with now playing info (for admin dashboard and widgets)"
(handler-case
(let ((now-playing-stats (icecast-now-playing *stream-base-url*)))
(if now-playing-stats
(progn
(setf (header "Content-Type") "text/plain")
(cdr (assoc :title now-playing-stats)))
(progn
(setf (header "Content-Type") "text/plain")
"Stream Offline")))
(error (e)
(setf (header "Content-Type") "text/plain")
"Error loading stream info")))

View File

@ -28,11 +28,13 @@ document.addEventListener('DOMContentLoaded', function() {
// Queue controls // Queue controls
const refreshQueueBtn = document.getElementById('refresh-queue'); const refreshQueueBtn = document.getElementById('refresh-queue');
const loadFromM3uBtn = document.getElementById('load-from-m3u');
const clearQueueBtn = document.getElementById('clear-queue-btn'); const clearQueueBtn = document.getElementById('clear-queue-btn');
const addRandomBtn = document.getElementById('add-random-tracks'); const addRandomBtn = document.getElementById('add-random-tracks');
const queueSearchInput = document.getElementById('queue-track-search'); const queueSearchInput = document.getElementById('queue-track-search');
if (refreshQueueBtn) refreshQueueBtn.addEventListener('click', loadStreamQueue); if (refreshQueueBtn) refreshQueueBtn.addEventListener('click', loadStreamQueue);
if (loadFromM3uBtn) loadFromM3uBtn.addEventListener('click', loadQueueFromM3U);
if (clearQueueBtn) clearQueueBtn.addEventListener('click', clearStreamQueue); if (clearQueueBtn) clearQueueBtn.addEventListener('click', clearStreamQueue);
if (addRandomBtn) addRandomBtn.addEventListener('click', addRandomTracks); if (addRandomBtn) addRandomBtn.addEventListener('click', addRandomTracks);
if (queueSearchInput) queueSearchInput.addEventListener('input', searchTracksForQueue); if (queueSearchInput) queueSearchInput.addEventListener('input', searchTracksForQueue);
@ -103,8 +105,6 @@ function renderPage() {
<div class="track-album">${track.album || 'Unknown Album'}</div> <div class="track-album">${track.album || 'Unknown Album'}</div>
</div> </div>
<div class="track-actions"> <div class="track-actions">
<button onclick="playTrack(${track.id})" class="btn btn-sm btn-success"> Play</button>
<button onclick="streamTrack(${track.id})" class="btn btn-sm btn-info">🎵 Stream</button>
<button onclick="addToQueue(${track.id}, 'end')" class="btn btn-sm btn-primary"> Add to Queue</button> <button onclick="addToQueue(${track.id}, 'end')" class="btn btn-sm btn-primary"> Add to Queue</button>
<button onclick="deleteTrack(${track.id})" class="btn btn-sm btn-danger">🗑 Delete</button> <button onclick="deleteTrack(${track.id})" class="btn btn-sm btn-danger">🗑 Delete</button>
</div> </div>
@ -185,6 +185,7 @@ async function scanLibrary() {
// Filter tracks based on search // Filter tracks based on search
function filterTracks() { function filterTracks() {
const query = document.getElementById('track-search').value.toLowerCase();
const filtered = tracks.filter(track => const filtered = tracks.filter(track =>
(track.title || '').toLowerCase().includes(query) || (track.title || '').toLowerCase().includes(query) ||
(track.artist || '').toLowerCase().includes(query) || (track.artist || '').toLowerCase().includes(query) ||
@ -367,14 +368,20 @@ function displayStreamQueue() {
let html = '<div class="queue-items">'; let html = '<div class="queue-items">';
streamQueue.forEach((item, index) => { streamQueue.forEach((item, index) => {
if (item) { if (item) {
const isFirst = index === 0;
const isLast = index === streamQueue.length - 1;
html += ` html += `
<div class="queue-item" data-track-id="${item.id}"> <div class="queue-item" data-track-id="${item.id}" data-index="${index}">
<span class="queue-position">${index + 1}</span> <span class="queue-position">${index + 1}</span>
<div class="queue-track-info"> <div class="queue-track-info">
<div class="track-title">${item.title || 'Unknown'}</div> <div class="track-title">${item.title || 'Unknown'}</div>
<div class="track-artist">${item.artist || 'Unknown Artist'}</div> <div class="track-artist">${item.artist || 'Unknown Artist'}</div>
</div> </div>
<button class="btn btn-sm btn-danger" onclick="removeFromQueue(${item.id})">Remove</button> <div class="queue-actions">
<button class="btn btn-sm btn-secondary" onclick="moveTrackUp(${index})" ${isFirst ? 'disabled' : ''}></button>
<button class="btn btn-sm btn-secondary" onclick="moveTrackDown(${index})" ${isLast ? 'disabled' : ''}></button>
<button class="btn btn-sm btn-danger" onclick="removeFromQueue(${item.id})">Remove</button>
</div>
</div> </div>
`; `;
} }
@ -409,6 +416,74 @@ async function clearStreamQueue() {
} }
} }
// Load queue from M3U file
async function loadQueueFromM3U() {
if (!confirm('Load queue from stream-queue.m3u file? This will replace the current queue.')) {
return;
}
try {
const response = await fetch('/api/asteroid/stream/queue/load-m3u', {
method: 'POST'
});
const result = await response.json();
const data = result.data || result;
if (data.status === 'success') {
alert(`Successfully loaded ${data.count} tracks from M3U file!`);
loadStreamQueue();
} else {
alert('Error loading from M3U: ' + (data.message || 'Unknown error'));
}
} catch (error) {
console.error('Error loading from M3U:', error);
alert('Error loading from M3U: ' + error.message);
}
}
// Move track up in queue
async function moveTrackUp(index) {
if (index === 0) return;
// Swap with previous track
const newQueue = [...streamQueue];
[newQueue[index - 1], newQueue[index]] = [newQueue[index], newQueue[index - 1]];
await reorderQueue(newQueue);
}
// Move track down in queue
async function moveTrackDown(index) {
if (index === streamQueue.length - 1) return;
// Swap with next track
const newQueue = [...streamQueue];
[newQueue[index], newQueue[index + 1]] = [newQueue[index + 1], newQueue[index]];
await reorderQueue(newQueue);
}
// Reorder the queue
async function reorderQueue(newQueue) {
try {
const trackIds = newQueue.map(track => track.id).join(',');
const response = await fetch('/api/asteroid/stream/queue/reorder?track-ids=' + trackIds, {
method: 'POST'
});
const result = await response.json();
const data = result.data || result;
if (data.status === 'success') {
loadStreamQueue();
} else {
alert('Error reordering queue: ' + (data.message || 'Unknown error'));
}
} catch (error) {
console.error('Error reordering queue:', error);
alert('Error reordering queue');
}
}
// Remove track from queue // Remove track from queue
async function removeFromQueue(trackId) { async function removeFromQueue(trackId) {
try { try {
@ -561,35 +636,25 @@ function displayQueueSearchResults(results) {
// Live stream info update // Live stream info update
async function updateLiveStreamInfo() { async function updateLiveStreamInfo() {
try { try {
const response = await fetch('/api/asteroid/icecast-status'); const response = await fetch('/api/asteroid/partial/now-playing-inline');
if (!response.ok) { const contentType = response.headers.get("content-type");
if (!contentType.includes('text/plain')) {
console.error('Unexpected content type:', contentType);
return; return;
} }
const result = await response.json(); const nowPlayingText = await response.text();
const nowPlayingEl = document.getElementById('live-now-playing');
// Handle Radiance API response format if (nowPlayingEl) {
const data = result.data || result; nowPlayingEl.textContent = nowPlayingText;
// Sources are nested in icestats
const sources = data.icestats?.source;
if (sources) {
const mainStream = Array.isArray(sources)
? sources.find(s => s.listenurl?.includes('/asteroid.aac') || s.listenurl?.includes('/asteroid.mp3'))
: sources;
if (mainStream && mainStream.title) {
const nowPlayingEl = document.getElementById('live-now-playing');
if (nowPlayingEl) {
const parts = mainStream.title.split(' - ');
const artist = parts[0] || 'Unknown';
const track = parts.slice(1).join(' - ') || 'Unknown';
nowPlayingEl.textContent = `${artist} - ${track}`;
}
}
} }
} catch (error) { } catch (error) {
console.error('Could not fetch stream info:', error); console.error('Could not fetch stream info:', error);
const nowPlayingEl = document.getElementById('live-now-playing');
if (nowPlayingEl) {
nowPlayingEl.textContent = 'Error loading stream info';
}
} }
} }

View File

@ -60,33 +60,15 @@ function changeStreamQuality() {
// Update now playing info from Icecast // Update now playing info from Icecast
async function updateNowPlaying() { async function updateNowPlaying() {
try { try {
const response = await fetch('/api/asteroid/icecast-status') const response = await fetch('/api/asteroid/partial/now-playing')
const data = await response.json() const contentType = response.headers.get("content-type")
// Handle RADIANCE API wrapper format if (!contentType.includes('text/html')) {
const icecastData = data.data || data; throw new Error('Error connecting to stream')
if (icecastData.icestats && icecastData.icestats.source) {
// Find the high quality stream (asteroid.mp3)
const sources = Array.isArray(icecastData.icestats.source) ? icecastData.icestats.source : [icecastData.icestats.source];
const mainStream = sources.find(s => s.listenurl && s.listenurl.includes('asteroid.mp3'));
if (mainStream && mainStream.title) {
// Parse "Artist - Track" format
const titleParts = mainStream.title.split(' - ');
const artist = titleParts.length > 1 ? titleParts[0] : 'Unknown Artist';
const track = titleParts.length > 1 ? titleParts.slice(1).join(' - ') : mainStream.title;
document.querySelector('[data-text="now-playing-artist"]').textContent = artist;
document.querySelector('[data-text="now-playing-track"]').textContent = track;
document.querySelector('[data-text="listeners"]').textContent = mainStream.listeners || '0';
// Update stream status
const statusElement = document.querySelector('.live-stream p:nth-child(3) span');
if (statusElement) {
statusElement.textContent = '● LIVE - ' + track;
statusElement.style.color = '#00ff00';
}
}
} }
const data = await response.text()
document.getElementById('now-playing').innerHTML = data
} catch(error) { } catch(error) {
console.log('Could not fetch stream status:', error); console.log('Could not fetch stream status:', error);
} }
@ -107,7 +89,110 @@ window.addEventListener('DOMContentLoaded', function() {
} }
// Update playing information right after load // Update playing information right after load
updateNowPlaying(); updateNowPlaying();
// Auto-reconnect on stream errors
const audioElement = document.getElementById('live-audio');
if (audioElement) {
audioElement.addEventListener('error', function(e) {
console.log('Stream error, attempting reconnect in 3 seconds...');
setTimeout(function() {
audioElement.load();
audioElement.play().catch(err => console.log('Reconnect failed:', err));
}, 3000);
});
audioElement.addEventListener('stalled', function() {
console.log('Stream stalled, reloading...');
audioElement.load();
audioElement.play().catch(err => console.log('Reload failed:', err));
});
}
}); });
// Update every 10 seconds // Update every 10 seconds
setInterval(updateNowPlaying, 10000); setInterval(updateNowPlaying, 10000);
// Pop-out player functionality
let popoutWindow = null;
function openPopoutPlayer() {
// Check if popout is already open
if (popoutWindow && !popoutWindow.closed) {
popoutWindow.focus();
return;
}
// Calculate centered position
const width = 420;
const height = 300;
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;
// Open popout window
const features = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no,status=no,menubar=no,toolbar=no,location=no`;
popoutWindow = window.open('/asteroid/popout-player', 'AsteroidPlayer', features);
// Update button state
updatePopoutButton(true);
}
function updatePopoutButton(isOpen) {
const btn = document.getElementById('popout-btn');
if (btn) {
if (isOpen) {
btn.textContent = '✓ Player Open';
btn.classList.remove('btn-info');
btn.classList.add('btn-success');
} else {
btn.textContent = '🗗 Pop Out Player';
btn.classList.remove('btn-success');
btn.classList.add('btn-info');
}
}
}
// Listen for messages from popout window
window.addEventListener('message', function(event) {
if (event.data.type === 'popout-opened') {
updatePopoutButton(true);
} else if (event.data.type === 'popout-closed') {
updatePopoutButton(false);
popoutWindow = null;
}
});
// Check if popout is still open periodically
setInterval(function() {
if (popoutWindow && popoutWindow.closed) {
updatePopoutButton(false);
popoutWindow = null;
}
}, 1000);
// Frameset mode functionality
function enableFramesetMode() {
// Save preference
localStorage.setItem('useFrameset', 'true');
// Redirect to frameset wrapper
window.location.href = '/asteroid/frameset';
}
function disableFramesetMode() {
// Clear preference
localStorage.removeItem('useFrameset');
// Redirect to regular view
window.location.href = '/asteroid/';
}
// Check if user prefers frameset mode on page load
window.addEventListener('DOMContentLoaded', function() {
const path = window.location.pathname;
const isFramesetPage = path.includes('/frameset') || path.includes('/content') ||
path.includes('/audio-player-frame') || path.includes('/player-content');
if (localStorage.getItem('useFrameset') === 'true' && !isFramesetPage && path === '/asteroid/') {
// User wants frameset but is on regular front page, redirect
window.location.href = '/asteroid/frameset';
}
});

View File

@ -566,41 +566,24 @@ function changeLiveStreamQuality() {
} }
} }
// Live stream functionality // Live stream informatio update
async function updateLiveStream() { async function updateNowPlaying() {
try { try {
const response = await fetch('/api/asteroid/icecast-status') const response = await fetch('/api/asteroid/partial/now-playing')
if (!response.ok) { const contentType = response.headers.get("content-type")
throw new Error(`HTTP ${response.status}`); if (!contentType.includes('text/html')) {
throw new Error('Error connecting to stream')
} }
const result = await response.json();
// Handle RADIANCE API wrapper format
const data = result.data || result;
if (data.icestats && data.icestats.source) {
const sources = Array.isArray(data.icestats.source) ? data.icestats.source : [data.icestats.source];
const mainStream = sources.find(s => s.listenurl && s.listenurl.includes('asteroid.mp3'));
if (mainStream && mainStream.title) { const data = await response.text()
const titleParts = mainStream.title.split(' - '); document.getElementById('now-playing').innerHTML = data
const artist = titleParts.length > 1 ? titleParts[0] : 'Unknown Artist';
const track = titleParts.length > 1 ? titleParts.slice(1).join(' - ') : mainStream.title;
const nowPlayingEl = document.getElementById('live-now-playing'); } catch(error) {
const listenersEl = document.getElementById('live-listeners'); console.log('Could not fetch stream status:', error);
if (nowPlayingEl) nowPlayingEl.textContent = `${artist} - ${track}`;
if (listenersEl) listenersEl.textContent = mainStream.listeners || '0';
} else {
}
}
} catch (error) {
console.error('Live stream update error:', error);
const nowPlayingEl = document.getElementById('live-now-playing');
if (nowPlayingEl) nowPlayingEl.textContent = 'Stream unavailable';
} }
} }
// Initial update after 1 second
setTimeout(updateNowPlaying, 1000);
// Update live stream info every 10 seconds // Update live stream info every 10 seconds
setTimeout(updateLiveStream, 1000); // Initial update after 1 second setInterval(updateNowPlaying, 10000);
setInterval(updateLiveStream, 10000);

View File

@ -72,8 +72,8 @@
(defun convert-to-docker-path (host-path) (defun convert-to-docker-path (host-path)
"Convert host file path to Docker container path" "Convert host file path to Docker container path"
;; Replace /home/glenn/Projects/Code/asteroid/music/library/ with /app/music/ ;; Replace the music library path with /app/music/
(let ((library-prefix "/home/glenn/Projects/Code/asteroid/music/library/")) (let ((library-prefix (namestring *music-library-path*)))
(if (and (stringp host-path) (if (and (stringp host-path)
(>= (length host-path) (length library-prefix)) (>= (length host-path) (length library-prefix))
(string= host-path library-prefix :end1 (length library-prefix))) (string= host-path library-prefix :end1 (length library-prefix)))
@ -177,3 +177,46 @@
(setf *stream-queue* (subseq track-ids 0 (min count (length track-ids)))) (setf *stream-queue* (subseq track-ids 0 (min count (length track-ids))))
(regenerate-stream-playlist) (regenerate-stream-playlist)
*stream-queue*)))) *stream-queue*))))
(defun convert-from-docker-path (docker-path)
"Convert Docker container path back to host file path"
(if (and (stringp docker-path)
(>= (length docker-path) 11)
(string= docker-path "/app/music/" :end1 11))
(concatenate 'string
(namestring *music-library-path*)
(subseq docker-path 11))
docker-path))
(defun load-queue-from-m3u-file ()
"Load the stream queue from the stream-queue.m3u file"
(let* ((m3u-path (merge-pathnames "stream-queue.m3u"
(asdf:system-source-directory :asteroid)))
(track-ids '())
(all-tracks (db:select "tracks" (db:query :all))))
(when (probe-file m3u-path)
(with-open-file (stream m3u-path :direction :input)
(loop for line = (read-line stream nil)
while line
do (unless (or (string= line "")
(char= (char line 0) #\#))
;; This is a file path line
(let* ((docker-path (string-trim '(#\Space #\Tab #\Return #\Newline) line))
(host-path (convert-from-docker-path docker-path)))
;; Find track by file path
(let ((track (find-if
(lambda (trk)
(let ((fp (gethash "file-path" trk)))
(let ((file-path (if (listp fp) (first fp) fp)))
(string= file-path host-path))))
all-tracks)))
(when track
(let ((id (gethash "_id" track)))
(push (if (listp id) (first id) id) track-ids)))))))))
;; Reverse to maintain order from file
(setf track-ids (nreverse track-ids))
(setf *stream-queue* track-ids)
(regenerate-stream-playlist)
(length track-ids)))

View File

@ -1,19 +1 @@
#EXTM3U #EXTM3U
#EXTINF:0,
/app/music/Model500/2015 - Digital Solutions/02-model_500-electric_night.flac
#EXTINF:0,
/app/music/Kraftwerk/1978 - The Man-Machine \[2009 Digital Remaster]/02 - Spacelab.flac
#EXTINF:0,
/app/music/Kraftwerk/1981 - Computer World \[2009 Digital Remaster]/03 - Numbers.flac
#EXTINF:0,
/app/music/Model500/2015 - Digital Solutions/08-model_500-digital_solutions.flac
#EXTINF:0,
/app/music/Model500/2015 - Digital Solutions/02-model_500-electric_night.flac
#EXTINF:0,
/app/music/This Mortal Coil/1984 - It'll End In Tears/09 - Barramundi.flac
#EXTINF:0,
/app/music/Underworld/1996 - Second Toughest In The Infants/04. Underworld - Rowla.flac
#EXTINF:0,
/app/music/This Mortal Coil/1984 - It'll End In Tears/10 - Dreams Made Flesh.flac
#EXTINF:0,
/app/music/Underworld/1996 - Second Toughest In The Infants/07. Underworld - Blueski.flac

View File

@ -12,9 +12,9 @@
<div class="container"> <div class="container">
<h1>🎛️ ADMIN DASHBOARD</h1> <h1>🎛️ ADMIN DASHBOARD</h1>
<div class="nav"> <div class="nav">
<a href="/asteroid/">Home</a> <a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/player/">Player</a> <a href="/asteroid/player-content" target="content-frame">Player</a>
<a href="/asteroid/profile">Profile</a> <a href="/asteroid/profile" target="content-frame">Profile</a>
<a href="/asteroid/admin/users">👥 Users</a> <a href="/asteroid/admin/users">👥 Users</a>
<a href="/asteroid/logout" class="btn-logout">Logout</a> <a href="/asteroid/logout" class="btn-logout">Logout</a>
</div> </div>
@ -127,6 +127,7 @@
<div class="queue-controls"> <div class="queue-controls">
<button id="refresh-queue" class="btn btn-secondary">🔄 Refresh Queue</button> <button id="refresh-queue" class="btn btn-secondary">🔄 Refresh Queue</button>
<button id="load-from-m3u" class="btn btn-success">📂 Load Queue from M3U</button>
<button id="clear-queue-btn" class="btn btn-warning">🗑️ Clear Queue</button> <button id="clear-queue-btn" class="btn btn-warning">🗑️ Clear Queue</button>
<button id="add-random-tracks" class="btn btn-info">🎲 Add 10 Random Tracks</button> <button id="add-random-tracks" class="btn btn-info">🎲 Add 10 Random Tracks</button>
</div> </div>

View File

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
<style>
body {
margin: 0;
padding: 10px;
background: #1a1a1a;
font-family: 'VT323', monospace;
}
.persistent-player {
display: flex;
align-items: center;
gap: 15px;
max-width: 100%;
}
.player-label {
color: #00ff00;
font-weight: bold;
white-space: nowrap;
}
.quality-selector {
display: flex;
align-items: center;
gap: 5px;
}
.quality-selector label {
color: #00ff00;
font-size: 0.9em;
}
.quality-selector select {
background: #2a2a2a;
color: #00ff00;
border: 1px solid #00ff00;
padding: 3px 8px;
font-family: 'VT323', monospace;
}
audio {
flex: 1;
min-width: 200px;
}
.now-playing-mini {
color: #00ff00;
font-size: 0.9em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 300px;
}
</style>
</head>
<body>
<div class="persistent-player">
<span class="player-label">🟢 LIVE:</span>
<div class="quality-selector">
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
<label for="stream-quality">Quality:</label>
<select id="stream-quality" onchange="changeStreamQuality()">
<option value="aac">AAC 96k</option>
<option value="mp3">MP3 128k</option>
<option value="low">MP3 64k</option>
</select>
</div>
<audio id="persistent-audio" controls preload="metadata">
<source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)">
</audio>
<span class="now-playing-mini" id="mini-now-playing">Loading...</span>
<button onclick="disableFramesetMode()" style="background: #2a2a2a; color: #00ff00; border: 1px solid #00ff00; padding: 5px 10px; cursor: pointer; font-family: 'VT323', monospace; font-size: 0.85em; white-space: nowrap;">
✕ Disable
</button>
</div>
<script>
// Configure audio element for better streaming
document.addEventListener('DOMContentLoaded', function() {
const audioElement = document.getElementById('persistent-audio');
// Try to enable low-latency mode if supported
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: 'Asteroid Radio Live Stream',
artist: 'Asteroid Radio',
album: 'Live Broadcast'
});
}
// Add event listeners for debugging
audioElement.addEventListener('waiting', function() {
console.log('Audio buffering...');
});
audioElement.addEventListener('playing', function() {
console.log('Audio playing');
});
audioElement.addEventListener('error', function(e) {
console.error('Audio error:', e);
});
});
// Stream quality configuration
function getStreamConfig(streamBaseUrl, encoding) {
const config = {
aac: {
url: streamBaseUrl + '/asteroid.aac',
type: 'audio/aac'
},
mp3: {
url: streamBaseUrl + '/asteroid.mp3',
type: 'audio/mpeg'
},
low: {
url: streamBaseUrl + '/asteroid-low.mp3',
type: 'audio/mpeg'
}
};
return config[encoding];
}
// Change stream quality
function changeStreamQuality() {
const selector = document.getElementById('stream-quality');
const streamBaseUrl = document.getElementById('stream-base-url').value;
const config = getStreamConfig(streamBaseUrl, selector.value);
const audioElement = document.getElementById('persistent-audio');
const sourceElement = document.getElementById('audio-source');
const wasPlaying = !audioElement.paused;
sourceElement.src = config.url;
sourceElement.type = config.type;
audioElement.load();
if (wasPlaying) {
audioElement.play().catch(e => console.log('Autoplay prevented:', e));
}
}
// Update mini now playing display
async function updateMiniNowPlaying() {
try {
const response = await fetch('/api/asteroid/partial/now-playing-inline');
if (response.ok) {
const text = await response.text();
document.getElementById('mini-now-playing').textContent = text;
}
} catch(error) {
console.log('Could not fetch now playing:', error);
}
}
// Update every 10 seconds
setTimeout(updateMiniNowPlaying, 1000);
setInterval(updateMiniNowPlaying, 10000);
// Disable frameset mode function
function disableFramesetMode() {
// Clear preference
localStorage.removeItem('useFrameset');
// Redirect parent window to regular view
window.parent.location.href = '/asteroid/';
}
</script>
</body>
</html>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title lquery="(text title)">🎵 ASTEROID RADIO 🎵</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
// Prevent nested framesets - break out if we're already in a frame
if (window.self !== window.top) {
window.top.location.href = window.self.location.href;
}
</script>
</head>
<frameset rows="*,80" frameborder="0" border="0" framespacing="0">
<frame src="/asteroid/content" name="content-frame" noresize>
<frame src="/asteroid/audio-player-frame" name="player-frame" noresize scrolling="no">
<noframes>
<body>
<p>Your browser does not support frames. Please use a modern browser or visit <a href="/asteroid/content">the main site</a>.</p>
</body>
</noframes>
</frameset>
</html>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title data-text="title">🎵 ASTEROID RADIO 🎵</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
<script src="/asteroid/static/js/auth-ui.js"></script>
<script src="/asteroid/static/js/front-page.js"></script>
</head>
<body>
<div class="container">
<header>
<h1 data-text="station-name">🎵 ASTEROID RADIO 🎵</h1>
<nav class="nav">
<a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/player-content" target="content-frame">Player</a>
<a href="/asteroid/status" target="content-frame">Status</a>
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
<a href="/asteroid/login" target="content-frame" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</nav>
</header>
<main>
<div class="status">
<h2>Station Status</h2>
<p data-text="status-message">🟢 LIVE - Broadcasting asteroid music for hackers</p>
<p>Current listeners: <span data-text="listeners">0</span></p>
<p>Stream quality: <span data-text="stream-quality">AAC 96kbps Stereo</span></p>
</div>
<div class="live-stream">
<h2 style="color: #00ff00;">🟢 LIVE STREAM</h2>
<p><em>The live stream player is now in the persistent bar at the bottom of the page.</em></p>
<p><strong>Stream URL:</strong> <code id="stream-url" lquery="(text default-stream-url)"></code></p>
<p><strong>Format:</strong> <span id="stream-format" lquery="(text default-stream-encoding-desc)"></span></p>
<p><strong>Status:</strong> <span id="stream-status" style="color: #00ff00;">● BROADCASTING</span></p>
</div>
<div id="now-playing" class="now-playing"></div>
</main>
</div>
</body>
</html>

View File

@ -33,7 +33,17 @@
</div> </div>
<div class="live-stream"> <div class="live-stream">
<h2 style="color: #00ff00;">🟢 LIVE STREAM</h2> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h2 style="color: #00ff00; margin: 0;">🟢 LIVE STREAM</h2>
<div style="display: flex; gap: 10px;">
<button id="popout-btn" class="btn btn-info" onclick="openPopoutPlayer()" style="font-size: 0.9em;">
🗗 Pop Out Player
</button>
<button id="frameset-btn" class="btn btn-secondary" onclick="enableFramesetMode()" style="font-size: 0.9em;">
🖼️ Enable Persistent Player
</button>
</div>
</div>
<!-- Stream Quality Selector --> <!-- Stream Quality Selector -->
<div class="live-stream-quality"> <div class="live-stream-quality">
@ -56,12 +66,7 @@
</audio> </audio>
</div> </div>
<div class="now-playing"> <div id="now-playing" class="now-playing"></div>
<h2>Now Playing</h2>
<p>Artist: <span data-text="now-playing-artist">The Void</span></p>
<p>Track: <span data-text="now-playing-track">Silence</span></p>
<p>Listeners: <span data-text="listeners">0</span></p>
</div>
</main> </main>
</div> </div>
</body> </body>

View File

@ -11,9 +11,9 @@
<header> <header>
<h1>🎵 ASTEROID RADIO - LOGIN</h1> <h1>🎵 ASTEROID RADIO - LOGIN</h1>
<nav class="nav"> <nav class="nav">
<a href="/asteroid/">Home</a> <a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/player">Player</a> <a href="/asteroid/player-content" target="content-frame">Player</a>
<a href="/asteroid/status">Status</a> <a href="/asteroid/status" target="content-frame">Status</a>
<a href="/asteroid/register">Register</a> <a href="/asteroid/register">Register</a>
</nav> </nav>
</header> </header>

View File

@ -0,0 +1,21 @@
<h2>Now Playing</h2>
<c:if test="stats">
<c:then>
<c:using value="stats">
<!--<p>Artist: <span>The Void</span></p>-->
<p>Track: <span lquery="(text title)">The Void - Silence</span></p>
<p>Listeners: <span lquery="(text listeners)">1</span></p>
</c:using>
</c:then>
<c:else>
<c:if test="connection-error">
<c:then>
<div class="message error">
<span>There was an error trying to get information from stream.</span>
</div>
</c:then>
</c:if>
<p>Track: <span>NA</span></p>
<p>Listeners: <span>NA</span></p>
</c:else>
</c:if>

View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title data-text="title">Asteroid Radio - Web Player</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
<script src="/asteroid/static/js/auth-ui.js"></script>
<script src="/asteroid/static/js/player.js"></script>
</head>
<body>
<div class="container">
<h1>🎵 WEB PLAYER</h1>
<div class="nav">
<a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
<a href="/asteroid/login" target="content-frame" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</div>
<!-- Live Stream Section - Note about persistent player -->
<div class="player-section">
<h2 style="color: #00ff00;">🟢 Live Radio Stream</h2>
<p><em>The live stream player is now in the persistent bar at the bottom of the page. It will continue playing as you navigate between pages!</em></p>
</div>
<div id="now-playing" class="now-playing"></div>
<!-- Track Browser -->
<div class="player-section">
<h2>Personal Track Library</h2>
<div class="track-browser">
<input type="text" id="search-tracks" placeholder="Search tracks..." class="search-input">
<select id="library-tracks-per-page" class="sort-select" onchange="changeLibraryTracksPerPage()" style="margin: 10px 0px;">
<option value="10">10 per page</option>
<option value="20" selected>20 per page</option>
<option value="50">50 per page</option>
</select>
<div id="track-list" class="track-list">
<div class="loading">Loading tracks...</div>
</div>
<!-- Pagination Controls -->
<div id="library-pagination-controls" style="display: none; margin-top: 20px; text-align: center;">
<button onclick="libraryGoToPage(1)" class="btn btn-secondary">« First</button>
<button onclick="libraryPreviousPage()" class="btn btn-secondary"> Prev</button>
<span id="library-page-info" style="margin: 0 15px; font-weight: bold;">Page 1 of 1</span>
<button onclick="libraryNextPage()" class="btn btn-secondary">Next </button>
<button onclick="libraryGoToLastPage()" class="btn btn-secondary">Last »</button>
</div>
</div>
</div>
<!-- Audio Player Widget -->
<div class="player-section">
<h2>Audio Player</h2>
<div class="audio-player">
<div class="now-playing">
<div class="track-art">🎵</div>
<div class="track-details">
<div class="track-title" id="current-title">No track selected</div>
<div class="track-artist" id="current-artist">Unknown Artist</div>
<div class="track-album" id="current-album">Unknown Album</div>
</div>
</div>
<audio id="audio-player" controls preload="none" style="width: 100%; margin: 20px 0;">
Your browser does not support the audio element.
</audio>
<div class="player-controls">
<button id="prev-btn" class="btn btn-secondary">⏮️ Previous</button>
<button id="play-pause-btn" class="btn btn-primary">▶️ Play</button>
<button id="next-btn" class="btn btn-secondary">⏭️ Next</button>
<button id="shuffle-btn" class="btn btn-info">🔀 Shuffle</button>
<button id="repeat-btn" class="btn btn-warning">🔁 Repeat</button>
</div>
<div class="player-info">
<div class="time-display">
<span id="current-time">0:00</span> / <span id="total-time">0:00</span>
</div>
<div class="volume-control">
<label for="volume-slider">🔊</label>
<input type="range" id="volume-slider" min="0" max="100" value="50" class="volume-slider">
</div>
</div>
</div>
</div>
<!-- Playlist Management -->
<div class="player-section">
<h2>Playlists</h2>
<div class="playlist-controls">
<input type="text" id="new-playlist-name" placeholder="New playlist name..." class="playlist-input">
<button id="create-playlist" class="btn btn-success"> Create Playlist</button>
</div>
<div class="playlist-list">
<div id="playlists-container">
<div class="no-playlists">No playlists created yet.</div>
</div>
</div>
</div>
<!-- Queue -->
<div class="player-section">
<h2>Play Queue</h2>
<div class="queue-controls">
<button id="clear-queue" class="btn btn-danger">🗑️ Clear Queue</button>
<button id="save-queue" class="btn btn-info">💾 Save as Playlist</button>
</div>
<div id="play-queue" class="play-queue">
<div class="empty-queue">Queue is empty</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -12,9 +12,9 @@
<div class="container"> <div class="container">
<h1>🎵 WEB PLAYER</h1> <h1>🎵 WEB PLAYER</h1>
<div class="nav"> <div class="nav">
<a href="/asteroid/">Home</a> <a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a> <a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" data-show-if-admin>Admin</a> <a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
<a href="/asteroid/login" data-show-if-logged-out>Login</a> <a href="/asteroid/login" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" data-show-if-logged-out>Register</a> <a href="/asteroid/register" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a> <a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
@ -25,8 +25,6 @@
<h2 style="color: #00ff00;">🟢 Live Radio Stream</h2> <h2 style="color: #00ff00;">🟢 Live Radio Stream</h2>
<div class="live-stream"> <div class="live-stream">
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)"> <input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
<p><strong>Now Playing:</strong> <span id="live-now-playing">Loading...</span></p>
<p><strong>Listeners:</strong> <span id="live-listeners">0</span></p>
<!-- Stream Quality Selector --> <!-- Stream Quality Selector -->
<div class="live-stream-quality"> <div class="live-stream-quality">
<label for="live-stream-quality"><strong>Quality:</strong></label> <label for="live-stream-quality"><strong>Quality:</strong></label>
@ -45,6 +43,8 @@
</div> </div>
</div> </div>
<div id="now-playing" class="now-playing"></div>
<!-- Track Browser --> <!-- Track Browser -->
<div class="player-section"> <div class="player-section">
<h2>Personal Track Library</h2> <h2>Personal Track Library</h2>

View File

@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>🎵 Asteroid Radio - Player</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
<style>
body {
margin: 0;
padding: 10px;
background: #0a0a0a;
overflow: hidden;
}
.popout-container {
max-width: 400px;
margin: 0 auto;
}
.popout-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #2a3441;
}
.popout-title {
font-size: 1.2em;
color: #00ff00;
}
.close-btn {
background: #ff4444;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
border-radius: 3px;
font-size: 0.9em;
}
.close-btn:hover {
background: #ff6666;
}
.now-playing-mini {
background: #1a1a1a;
padding: 10px;
border-radius: 5px;
margin-bottom: 10px;
border: 1px solid #2a3441;
}
.track-info-mini {
font-size: 0.9em;
}
.track-title-mini {
color: #00ff00;
font-weight: bold;
margin-bottom: 3px;
}
.track-artist-mini {
color: #4488ff;
font-size: 0.85em;
}
.quality-selector {
margin: 10px 0;
padding: 10px;
background: #1a1a1a;
border-radius: 5px;
border: 1px solid #2a3441;
}
.quality-selector label {
color: #00ff00;
margin-right: 10px;
}
.quality-selector select {
background: #0a0a0a;
color: #00ff00;
border: 1px solid #2a3441;
padding: 5px;
border-radius: 3px;
}
audio {
width: 100%;
margin: 10px 0;
}
.status-mini {
text-align: center;
color: #888;
font-size: 0.85em;
margin-top: 10px;
}
</style>
<script src="/asteroid/static/js/front-page.js"></script>
</head>
<body>
<div class="popout-container">
<div class="popout-header">
<div class="popout-title">🎵 Asteroid Radio</div>
<button class="close-btn" onclick="window.close()">✖ Close</button>
</div>
<div class="now-playing-mini">
<div class="track-info-mini">
<div class="track-title-mini" id="popout-track-title">Loading...</div>
<div class="track-artist-mini" id="popout-track-artist">Please wait</div>
</div>
</div>
<div class="quality-selector">
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
<label for="popout-stream-quality"><strong>Quality:</strong></label>
<select id="popout-stream-quality" onchange="changeStreamQuality()">
<option value="aac">AAC 96kbps</option>
<option value="mp3">MP3 128kbps</option>
<option value="low">MP3 64kbps</option>
</select>
</div>
<audio id="live-audio" controls autoplay style="width: 100%;">
<source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)">
Your browser does not support the audio element.
</audio>
<div class="status-mini">
<span style="color: #00ff00;">● LIVE</span>
</div>
</div>
<script>
// Stream quality configuration for popout
function getStreamConfig(streamBaseUrl, encoding) {
const config = {
aac: {
url: `${streamBaseUrl}/asteroid.aac`,
format: 'AAC 96kbps Stereo',
type: 'audio/aac',
mount: 'asteroid.aac'
},
mp3: {
url: `${streamBaseUrl}/asteroid.mp3`,
format: 'MP3 128kbps Stereo',
type: 'audio/mpeg',
mount: 'asteroid.mp3'
},
low: {
url: `${streamBaseUrl}/asteroid-low.mp3`,
format: 'MP3 64kbps Stereo',
type: 'audio/mpeg',
mount: 'asteroid-low.mp3'
}
};
return config[encoding];
}
// Change stream quality in popout
function changeStreamQuality() {
const selector = document.getElementById('popout-stream-quality');
const streamBaseUrl = document.getElementById('stream-base-url');
const config = getStreamConfig(streamBaseUrl.value, selector.value);
// Update audio player
const audioElement = document.getElementById('live-audio');
const sourceElement = document.getElementById('audio-source');
const wasPlaying = !audioElement.paused;
sourceElement.src = config.url;
sourceElement.type = config.type;
audioElement.load();
// Resume playback if it was playing
if (wasPlaying) {
audioElement.play().catch(e => console.log('Autoplay prevented:', e));
}
}
// Update now playing info for popout
async function updatePopoutNowPlaying() {
try {
const response = await fetch('/api/asteroid/partial/now-playing-inline');
const html = await response.text();
// Parse the HTML to extract track info
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const trackText = doc.body.textContent || doc.body.innerText || '';
// Try to split artist - title format
const parts = trackText.split(' - ');
if (parts.length >= 2) {
document.getElementById('popout-track-artist').textContent = parts[0].trim();
document.getElementById('popout-track-title').textContent = parts.slice(1).join(' - ').trim();
} else {
document.getElementById('popout-track-title').textContent = trackText.trim();
document.getElementById('popout-track-artist').textContent = 'Asteroid Radio';
}
} catch (error) {
console.error('Error updating now playing:', error);
}
}
// Update every 10 seconds
setInterval(updatePopoutNowPlaying, 10000);
// Initial update
updatePopoutNowPlaying();
// Auto-reconnect on stream errors
const audioElement = document.getElementById('live-audio');
audioElement.addEventListener('error', function(e) {
console.log('Stream error, attempting reconnect in 3 seconds...');
setTimeout(function() {
audioElement.load();
audioElement.play().catch(err => console.log('Reconnect failed:', err));
}, 3000);
});
audioElement.addEventListener('stalled', function() {
console.log('Stream stalled, reloading...');
audioElement.load();
audioElement.play().catch(err => console.log('Reload failed:', err));
});
// Notify parent window that popout is open
if (window.opener && !window.opener.closed) {
window.opener.postMessage({ type: 'popout-opened' }, '*');
}
// Notify parent when closing
window.addEventListener('beforeunload', function() {
if (window.opener && !window.opener.closed) {
window.opener.postMessage({ type: 'popout-closed' }, '*');
}
});
</script>
</body>
</html>

View File

@ -12,9 +12,9 @@
<div class="container"> <div class="container">
<h1>👤 USER PROFILE</h1> <h1>👤 USER PROFILE</h1>
<div class="nav"> <div class="nav">
<a href="/asteroid/">Home</a> <a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/player/">Player</a> <a href="/asteroid/player-content" target="content-frame">Player</a>
<a href="/asteroid/admin/" data-show-if-admin>Admin</a> <a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
<a href="/asteroid/logout" class="btn-logout">Logout</a> <a href="/asteroid/logout" class="btn-logout">Logout</a>
</div> </div>

View File

@ -11,9 +11,9 @@
<header> <header>
<h1>🎵 ASTEROID RADIO - REGISTER</h1> <h1>🎵 ASTEROID RADIO - REGISTER</h1>
<nav class="nav"> <nav class="nav">
<a href="/asteroid/">Home</a> <a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/player">Player</a> <a href="/asteroid/player-content" target="content-frame">Player</a>
<a href="/asteroid/status">Status</a> <a href="/asteroid/status" target="content-frame">Status</a>
<a href="/asteroid/login">Login</a> <a href="/asteroid/login">Login</a>
</nav> </nav>
</header> </header>

View File

@ -11,8 +11,8 @@
<div class="container"> <div class="container">
<h1>👥 USER MANAGEMENT</h1> <h1>👥 USER MANAGEMENT</h1>
<div class="nav"> <div class="nav">
<a href="/asteroid/">Home</a> <a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/admin">Admin</a> <a href="/asteroid/admin" target="content-frame">Admin</a>
<a href="/asteroid/logout" class="btn-logout">Logout</a> <a href="/asteroid/logout" class="btn-logout">Logout</a>
</div> </div>

View File

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