Compare commits
15 Commits
01f5806959
...
90bb9a1650
| Author | SHA1 | Date |
|---|---|---|
|
|
90bb9a1650 | |
|
|
0bb93c53a4 | |
|
|
bc36a00322 | |
|
|
637650a5ef | |
|
|
fd02e4c1d1 | |
|
|
1c85464a5f | |
|
|
f1eb43b325 | |
|
|
a458a85823 | |
|
|
ab3acf1279 | |
|
|
c4fd96289b | |
|
|
0930fc2c1c | |
|
|
a2ae329d54 | |
|
|
66e97aaf37 | |
|
|
a795680e99 | |
|
|
d8abd9661d |
|
|
@ -0,0 +1,8 @@
|
||||||
|
docker/
|
||||||
|
music/
|
||||||
|
data/
|
||||||
|
*.org
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile*
|
||||||
|
Makefile
|
||||||
|
.git/
|
||||||
|
|
@ -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" ]
|
||||||
491
README.org
491
README.org
|
|
@ -1,35 +1,45 @@
|
||||||
#+TITLE: Asteroid Radio - Internet Streaming Implementation
|
#+TITLE: Asteroid Radio - Internet Radio Streaming Platform
|
||||||
#+AUTHOR: Database Implementation Branch
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-09-11
|
#+DATE: 2025-10-26
|
||||||
|
|
||||||
* Overview
|
* Overview
|
||||||
|
|
||||||
This branch implements a complete internet radio streaming system for Asteroid Radio, transforming it from a simple web interface into a fully functional streaming radio station with live broadcasting capabilities.
|
Asteroid Radio is a complete internet radio streaming platform built with Common Lisp, featuring a hacker-themed terminal aesthetic. The project combines the Radiance web framework with Icecast/Liquidsoap streaming infrastructure to create a full-featured music streaming platform with live broadcasting capabilities.
|
||||||
|
|
||||||
|
** Project Links
|
||||||
|
- *Repository*: https://github.com/fade/asteroid
|
||||||
|
- *IRC*: #asteroid.music on irc.libera.chat
|
||||||
|
- *Documentation*: See =docs/= directory for comprehensive guides
|
||||||
|
|
||||||
* Key Features
|
* Key Features
|
||||||
|
|
||||||
** Live Internet Radio Streaming
|
** Live Internet Radio Streaming
|
||||||
- Continuous MP3 streaming at 128kbps stereo
|
- Multiple quality streams: 128kbps MP3, 96kbps AAC, 64kbps MP3
|
||||||
- Professional audio processing with crossfading and normalization
|
- Professional audio processing with crossfading and ReplayGain normalization
|
||||||
- Icecast2 streaming server integration
|
- Icecast2 streaming server integration
|
||||||
- Liquidsoap audio pipeline for reliable broadcasting
|
- Liquidsoap audio pipeline for reliable broadcasting
|
||||||
|
- Stream queue control for curated programming
|
||||||
|
|
||||||
** Music Library Management
|
** Music Library Management
|
||||||
- Database-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
|
||||||
|
- Recursive directory scanning
|
||||||
|
|
||||||
** Web Interface
|
** Web Interface
|
||||||
- RADIANCE framework with CLIP templating
|
- RADIANCE framework with CLIP templating
|
||||||
- Admin dashboard for library management
|
- 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
|
||||||
|
- Responsive design for desktop and mobile
|
||||||
|
- Role-based access control (Admin/DJ/Listener)
|
||||||
|
|
||||||
** Network Broadcasting
|
** Network Broadcasting
|
||||||
- WSL-compatible networking for internal network access
|
- Dynamic stream URL detection for multi-environment support
|
||||||
- Professional streaming URLs for media players
|
- Professional streaming URLs for media players
|
||||||
- Multi-listener support via Icecast2
|
- Multi-listener support via Icecast2
|
||||||
|
- Docker-based deployment for easy setup
|
||||||
|
|
||||||
* Architecture Changes
|
* Architecture Changes
|
||||||
|
|
||||||
|
|
@ -40,46 +50,82 @@ This branch implements a complete internet radio streaming system for Asteroid R
|
||||||
- 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
|
- *Liquidsoap*: Audio processing and streaming pipeline - Docker containerized
|
||||||
- *RADIANCE*: Web server and API (port 8080)
|
- *RADIANCE*: Web server and API (port 8080)
|
||||||
- *Database*: Track metadata and playlist storage
|
- *PostgreSQL*: Database backend (configured, ready for migration)
|
||||||
|
- *Docker Compose*: Container orchestration
|
||||||
|
|
||||||
** File Structure
|
** 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
|
||||||
├── asteroid-radio.liq # Liquidsoap streaming configuration
|
├── stream-control.lisp # Stream queue management
|
||||||
├── playlist.m3u # Generated playlist for streaming
|
├── user-management.lisp # User administration
|
||||||
├── start-asteroid-radio.sh # Launch script for all services
|
├── playlist-management.lisp # Playlist operations
|
||||||
├── stop-asteroid-radio.sh # Stop script for all services
|
├── test-server.sh # Automated test suite
|
||||||
|
├── docker/ # Docker infrastructure
|
||||||
|
│ ├── docker-compose.yml # Container orchestration
|
||||||
|
│ ├── asteroid-radio-docker.liq # Liquidsoap config
|
||||||
|
│ ├── icecast.xml # Icecast configuration
|
||||||
|
│ └── music/ # Music library mount
|
||||||
├── template/ # CLIP HTML templates
|
├── template/ # CLIP HTML templates
|
||||||
│ ├── front-page.chtml # Main page with live stream
|
│ ├── front-page.chtml # Main page with live stream
|
||||||
│ ├── admin.chtml # Admin dashboard
|
│ ├── admin.chtml # Admin dashboard
|
||||||
│ └── player.chtml # Web player interface
|
│ ├── player.chtml # Web player interface
|
||||||
|
│ └── users.chtml # User management
|
||||||
├── static/ # CSS and assets
|
├── static/ # CSS and assets
|
||||||
│ └── asteroid.lass # LASS stylesheet
|
│ └── asteroid.lass # LASS stylesheet
|
||||||
└── music/ # Music library
|
├── docs/ # Comprehensive documentation
|
||||||
├── incoming/ # Upload staging area
|
│ ├── README.org # Documentation index
|
||||||
└── library/ # Processed music files
|
│ ├── PROJECT-OVERVIEW.org # Architecture overview
|
||||||
|
│ ├── PROJECT-HISTORY.org # Development timeline
|
||||||
|
│ ├── INSTALLATION.org # Setup guide
|
||||||
|
│ └── ... # Additional guides
|
||||||
|
└── music/ # Music library (local dev)
|
||||||
#+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
|
||||||
|
|
@ -90,216 +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: =asteroid-radio.liq=
|
** Docker Configuration
|
||||||
#+BEGIN_SRC liquidsoap
|
Liquidsoap runs in a Docker container with configuration in =docker/asteroid-radio-docker.liq=
|
||||||
#!/usr/bin/liquidsoap
|
|
||||||
|
|
||||||
# Set log level for debugging
|
** Key Features
|
||||||
settings.log.level := 4
|
- *Multiple outputs*: Generates 3 simultaneous streams (MP3 128k, AAC 96k, MP3 64k)
|
||||||
|
- *Audio processing*: Crossfading, normalization, ReplayGain
|
||||||
|
- *Stream queue*: Reads from M3U playlist for curated programming
|
||||||
|
- *Telnet control*: Remote control interface on port 1234
|
||||||
|
- *Metadata*: Broadcasts track information to listeners
|
||||||
|
|
||||||
# Create playlist from directory
|
** Management
|
||||||
radio = playlist(mode="randomize", reload=3600, "/path/to/music/library/")
|
|
||||||
|
|
||||||
# Add audio processing
|
|
||||||
radio = amplify(1.0, radio)
|
|
||||||
|
|
||||||
# Fallback with sine wave for debugging
|
|
||||||
radio = fallback(track_sensitive=false, [radio, sine(440.0)])
|
|
||||||
|
|
||||||
# Output to Icecast2
|
|
||||||
output.icecast(
|
|
||||||
%mp3(bitrate=128),
|
|
||||||
host="localhost",
|
|
||||||
port=8000,
|
|
||||||
password="b3l0wz3r0",
|
|
||||||
mount="asteroid.mp3",
|
|
||||||
name="Asteroid Radio",
|
|
||||||
description="Music for Hackers - Streaming from the Asteroid",
|
|
||||||
genre="Electronic/Alternative",
|
|
||||||
url="http://localhost:8080/asteroid/",
|
|
||||||
radio
|
|
||||||
)
|
|
||||||
#+END_SRC
|
|
||||||
|
|
||||||
** Installation (Ubuntu/Debian)
|
|
||||||
#+BEGIN_SRC bash
|
#+BEGIN_SRC bash
|
||||||
sudo apt update
|
# Start Liquidsoap container
|
||||||
sudo apt install liquidsoap
|
cd docker
|
||||||
|
docker compose up -d liquidsoap
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose logs -f liquidsoap
|
||||||
|
|
||||||
|
# Restart streaming
|
||||||
|
docker compose restart liquidsoap
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
** Features
|
** Telnet Control
|
||||||
- *Random playlist*: Shuffles music library continuously
|
|
||||||
- *Auto-reload*: Playlist refreshes every hour
|
|
||||||
- *Audio processing*: Amplification and normalization
|
|
||||||
- *Fallback*: Sine tone if no music available (debugging)
|
|
||||||
- *Metadata*: Station info broadcast to listeners
|
|
||||||
|
|
||||||
* Network Access
|
|
||||||
|
|
||||||
** Local Development
|
|
||||||
- *Web Interface*: =http://localhost:8080/asteroid/=
|
|
||||||
- *Live Stream*: =http://localhost:8000/asteroid.mp3=
|
|
||||||
- *Admin Panel*: =http://localhost:8080/asteroid/admin=
|
|
||||||
|
|
||||||
** WSL Network Access
|
|
||||||
- *WSL IP*: Check with =ip addr show eth0=
|
|
||||||
- *Web Interface*: =http://[WSL-IP]:8080/asteroid/=
|
|
||||||
- *Live Stream*: =http://[WSL-IP]:8000/asteroid.mp3=
|
|
||||||
|
|
||||||
** Internal Network Broadcasting
|
|
||||||
- Services bind to all interfaces (0.0.0.0)
|
|
||||||
- Accessible from any device on local network
|
|
||||||
- Compatible with media players (VLC, iTunes, etc.)
|
|
||||||
|
|
||||||
* Usage Instructions
|
|
||||||
|
|
||||||
** Starting the Radio Station
|
|
||||||
#+BEGIN_SRC bash
|
#+BEGIN_SRC bash
|
||||||
# Launch all services
|
# Connect to Liquidsoap
|
||||||
./start-asteroid-radio.sh
|
telnet localhost 1234
|
||||||
|
|
||||||
|
# Or use netcat for scripting
|
||||||
|
echo "request.queue" | nc localhost 1234
|
||||||
|
echo "request.skip" | nc localhost 1234
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|
||||||
** Stopping the Radio Station
|
* User Management
|
||||||
#+BEGIN_SRC bash
|
|
||||||
# Stop all services
|
|
||||||
./stop-asteroid-radio.sh
|
|
||||||
#+END_SRC
|
|
||||||
|
|
||||||
** Adding Music
|
** Roles
|
||||||
1. Copy MP3/FLAC files to =music/incoming/=
|
- *Admin*: Full system access, user management, stream control
|
||||||
2. Visit admin panel: =http://[IP]:8080/asteroid/admin=
|
- *DJ*: Content management, playlist creation, library access
|
||||||
3. Click "Copy Files from Incoming"
|
- *Listener*: Basic playback and personal playlists
|
||||||
4. Files are processed and added to streaming playlist
|
|
||||||
|
|
||||||
** Listening to the Stream
|
** Default Credentials
|
||||||
- *Web Browser*: Visit main page for embedded player
|
- Username: =admin=
|
||||||
- *Media Player*: Open =http://[IP]:8000/asteroid.mp3=
|
- Password: =asteroid123=
|
||||||
- *Mobile Apps*: Use internet radio apps with stream URL
|
- ⚠️ 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/tracks= - List all tracks with metadata
|
- =GET /api/asteroid/tracks= - List all tracks
|
||||||
- =GET /tracks/{id}/stream= - Stream individual track
|
- =GET /api/asteroid/admin/tracks= - Admin track listing
|
||||||
- =POST /api/scan-library= - Scan and update music library
|
- =POST /api/asteroid/admin/scan-library= - Scan music library
|
||||||
- =POST /api/copy-files= - Process files from incoming directory
|
|
||||||
|
|
||||||
** Player Control
|
** Player Control
|
||||||
- =POST /api/player/play= - Start playback
|
- =GET /api/asteroid/player/status= - Player status
|
||||||
- =POST /api/player/pause= - Pause playback
|
- =POST /api/asteroid/player/play= - Play track
|
||||||
- =POST /api/player/stop= - Stop playback
|
- =POST /api/asteroid/player/pause= - Pause playback
|
||||||
- =GET /api/status= - Get server status
|
- =POST /api/asteroid/player/stop= - Stop playback
|
||||||
|
- =POST /api/asteroid/player/resume= - Resume playback
|
||||||
|
|
||||||
** Search and Filter
|
** Playlist Management
|
||||||
- =GET /api/tracks?search={query}= - Search tracks
|
- =GET /api/asteroid/playlists= - List user playlists
|
||||||
- =GET /api/tracks?sort={field}= - Sort by field
|
- =POST /api/asteroid/playlists/create= - Create playlist
|
||||||
- =GET /api/tracks?artist={name}= - Filter by artist
|
- =GET /api/asteroid/playlists/get= - Get playlist details
|
||||||
|
- =POST /api/asteroid/playlists/add-track= - Add track to playlist
|
||||||
|
|
||||||
* Database Schema
|
** Stream Queue Control (Admin)
|
||||||
|
- =GET /api/asteroid/stream/queue= - Get broadcast queue
|
||||||
|
- =POST /api/asteroid/stream/queue/add= - Add track to queue
|
||||||
|
- =POST /api/asteroid/stream/queue/remove= - Remove from queue
|
||||||
|
- =POST /api/asteroid/stream/queue/clear= - Clear queue
|
||||||
|
|
||||||
** Tracks Collection
|
See =docs/API-ENDPOINTS.org= for complete API documentation.
|
||||||
#+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
|
|
||||||
|
|
||||||
** Playlists Collection (Future)
|
* Database
|
||||||
#+BEGIN_SRC lisp
|
|
||||||
(db:create "playlists" '((name :text)
|
** Current: Radiance DB
|
||||||
(description :text)
|
- File-based database abstraction
|
||||||
(created-date :integer)
|
- Tracks, users, playlists, sessions
|
||||||
(track-ids :text)))
|
- Suitable for development and small deployments
|
||||||
#+END_SRC
|
|
||||||
|
** PostgreSQL (Configured)
|
||||||
|
- Docker container ready
|
||||||
|
- Full schema defined
|
||||||
|
- Migration pending
|
||||||
|
- See =docs/POSTGRESQL-SETUP.org= for details
|
||||||
|
|
||||||
|
* Documentation
|
||||||
|
|
||||||
|
Comprehensive documentation available in the =docs/= directory:
|
||||||
|
|
||||||
|
- *README.org* - Documentation index
|
||||||
|
- *PROJECT-OVERVIEW.org* - Architecture and features
|
||||||
|
- *PROJECT-HISTORY.org* - Development timeline and milestones
|
||||||
|
- *INSTALLATION.org* - Complete installation guide
|
||||||
|
- *DEVELOPMENT.org* - Developer setup and guidelines
|
||||||
|
- *DOCKER-STREAMING.org* - Docker streaming infrastructure
|
||||||
|
- *API-ENDPOINTS.org* - REST API reference
|
||||||
|
- *STREAM-CONTROL.org* - Stream queue management
|
||||||
|
- *USER-MANAGEMENT-SYSTEM.org* - User administration
|
||||||
|
- *PLAYLIST-SYSTEM.org* - Playlist functionality
|
||||||
|
- *TESTING.org* - Automated testing guide
|
||||||
|
- *POSTGRESQL-SETUP.org* - Database setup
|
||||||
|
|
||||||
* Dependencies
|
* 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
|
||||||
- Playlist creation and management interface
|
1. Fork the repository
|
||||||
- Now-playing status tracking and display
|
2. Create a feature branch
|
||||||
- Direct browser file uploads with progress
|
3. Make your changes
|
||||||
- Listener statistics and analytics
|
4. Run test suite
|
||||||
- Scheduled programming and automation
|
5. Submit pull request
|
||||||
|
|
||||||
** Technical Improvements
|
** Community
|
||||||
- WebSocket integration for real-time updates
|
- *IRC*: #asteroid.music on irc.libera.chat
|
||||||
- Advanced audio processing options
|
- *Issues*: GitHub issue tracker
|
||||||
- Multi-bitrate streaming support
|
- *Discussions*: GitHub discussions
|
||||||
- Mobile-responsive interface enhancements
|
|
||||||
|
** Core Team
|
||||||
|
- 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*
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
:pathname "./"
|
:pathname "./"
|
||||||
:components ((:file "app-utils")
|
:components ((:file "app-utils")
|
||||||
(:file "module")
|
(:file "module")
|
||||||
|
(:file "conditions")
|
||||||
(:file "database")
|
(:file "database")
|
||||||
(:file "template-utils")
|
(:file "template-utils")
|
||||||
(:file "stream-media")
|
(:file "stream-media")
|
||||||
|
|
|
||||||
279
asteroid.lisp
279
asteroid.lisp
|
|
@ -37,21 +37,18 @@
|
||||||
(define-api asteroid/admin/scan-library () ()
|
(define-api asteroid/admin/scan-library () ()
|
||||||
"API endpoint to scan music library"
|
"API endpoint to scan music library"
|
||||||
(require-role :admin)
|
(require-role :admin)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((tracks-added (scan-music-library)))
|
(let ((tracks-added (scan-music-library)))
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Library scan completed")
|
("message" . "Library scan completed")
|
||||||
("tracks-added" . ,tracks-added))))
|
("tracks-added" . ,tracks-added))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Scan failed: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/admin/tracks () ()
|
(define-api asteroid/admin/tracks () ()
|
||||||
"API endpoint to view all tracks in database"
|
"API endpoint to view all tracks in database"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((tracks (db:select "tracks" (db:query :all))))
|
(let ((tracks (with-db-error-handling "select"
|
||||||
|
(db:select "tracks" (db:query :all)))))
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("tracks" . ,(mapcar (lambda (track)
|
("tracks" . ,(mapcar (lambda (track)
|
||||||
`(("id" . ,(gethash "_id" track))
|
`(("id" . ,(gethash "_id" track))
|
||||||
|
|
@ -61,17 +58,13 @@
|
||||||
("duration" . ,(first (gethash "duration" track)))
|
("duration" . ,(first (gethash "duration" track)))
|
||||||
("format" . ,(first (gethash "format" track)))
|
("format" . ,(first (gethash "format" track)))
|
||||||
("bitrate" . ,(first (gethash "bitrate" track)))))
|
("bitrate" . ,(first (gethash "bitrate" track)))))
|
||||||
tracks)))))
|
tracks)))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error retrieving tracks: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
;; Playlist API endpoints
|
;; Playlist API endpoints
|
||||||
(define-api asteroid/playlists () ()
|
(define-api asteroid/playlists () ()
|
||||||
"Get all playlists for current user"
|
"Get all playlists for current user"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let* ((user (get-current-user))
|
(let* ((user (get-current-user))
|
||||||
(user-id-raw (gethash "_id" user))
|
(user-id-raw (gethash "_id" user))
|
||||||
(user-id (if (listp user-id-raw) (first user-id-raw) user-id-raw))
|
(user-id (if (listp user-id-raw) (first user-id-raw) user-id-raw))
|
||||||
|
|
@ -98,16 +91,12 @@
|
||||||
("description" . ,(if (listp desc-val) (first desc-val) desc-val))
|
("description" . ,(if (listp desc-val) (first desc-val) desc-val))
|
||||||
("track-count" . ,track-count)
|
("track-count" . ,track-count)
|
||||||
("created-date" . ,(if (listp created-val) (first created-val) created-val))))))
|
("created-date" . ,(if (listp created-val) (first created-val) created-val))))))
|
||||||
playlists)))))
|
playlists)))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error retrieving playlists: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/playlists/create (name &optional description) ()
|
(define-api asteroid/playlists/create (name &optional description) ()
|
||||||
"Create a new playlist"
|
"Create a new playlist"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let* ((user (get-current-user))
|
(let* ((user (get-current-user))
|
||||||
(user-id-raw (gethash "_id" user))
|
(user-id-raw (gethash "_id" user))
|
||||||
(user-id (if (listp user-id-raw) (first user-id-raw) user-id-raw)))
|
(user-id (if (listp user-id-raw) (first user-id-raw) user-id-raw)))
|
||||||
|
|
@ -115,32 +104,22 @@
|
||||||
(if (string= "true" (post/get "browser"))
|
(if (string= "true" (post/get "browser"))
|
||||||
(redirect "/asteroid/")
|
(redirect "/asteroid/")
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Playlist created successfully")))))
|
("message" . "Playlist created successfully")))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error creating playlist: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/playlists/add-track (playlist-id track-id) ()
|
(define-api asteroid/playlists/add-track (playlist-id track-id) ()
|
||||||
"Add a track to a playlist"
|
"Add a track to a playlist"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((pl-id (parse-integer playlist-id :junk-allowed t))
|
(let ((pl-id (parse-integer playlist-id :junk-allowed t))
|
||||||
(tr-id (parse-integer track-id :junk-allowed t)))
|
(tr-id (parse-integer track-id :junk-allowed t)))
|
||||||
(add-track-to-playlist pl-id tr-id)
|
(add-track-to-playlist pl-id tr-id)
|
||||||
(if (string= "true" (post/get "browser"))
|
|
||||||
(redirect "/asteroid/")
|
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Track added to playlist")))))
|
("message" . "Track added to playlist"))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error adding track: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/playlists/get (playlist-id) ()
|
(define-api asteroid/playlists/get (playlist-id) ()
|
||||||
"Get playlist details with tracks"
|
"Get playlist details with tracks"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let* ((id (parse-integer playlist-id :junk-allowed t))
|
(let* ((id (parse-integer playlist-id :junk-allowed t))
|
||||||
(playlist (get-playlist-by-id id)))
|
(playlist (get-playlist-by-id id)))
|
||||||
(if playlist
|
(if playlist
|
||||||
|
|
@ -164,18 +143,15 @@
|
||||||
valid-tracks)))))))
|
valid-tracks)))))))
|
||||||
(api-output `(("status" . "error")
|
(api-output `(("status" . "error")
|
||||||
("message" . "Playlist not found"))
|
("message" . "Playlist not found"))
|
||||||
:status 404)))
|
:status 404)))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error retrieving playlist: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
;; API endpoint to get all tracks (for web player)
|
;; API endpoint to get all tracks (for web player)
|
||||||
(define-api asteroid/tracks () ()
|
(define-api asteroid/tracks () ()
|
||||||
"Get all tracks for web player"
|
"Get all tracks for web player"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((tracks (db:select "tracks" (db:query :all))))
|
(let ((tracks (with-db-error-handling "select"
|
||||||
|
(db:select "tracks" (db:query :all)))))
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("tracks" . ,(mapcar (lambda (track)
|
("tracks" . ,(mapcar (lambda (track)
|
||||||
`(("id" . ,(gethash "_id" track))
|
`(("id" . ,(gethash "_id" track))
|
||||||
|
|
@ -184,111 +160,77 @@
|
||||||
("album" . ,(gethash "album" track))
|
("album" . ,(gethash "album" track))
|
||||||
("duration" . ,(gethash "duration" track))
|
("duration" . ,(gethash "duration" track))
|
||||||
("format" . ,(gethash "format" track))))
|
("format" . ,(gethash "format" track))))
|
||||||
tracks)))))
|
tracks)))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error retrieving tracks: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
;; Stream Control API Endpoints
|
;; Stream Control API Endpoints
|
||||||
(define-api asteroid/stream/queue () ()
|
(define-api asteroid/stream/queue () ()
|
||||||
"Get the current stream queue"
|
"Get the current stream queue"
|
||||||
(require-role :admin)
|
(require-role :admin)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((queue (get-stream-queue)))
|
(let ((queue (get-stream-queue)))
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("queue" . ,(mapcar (lambda (track-id)
|
("queue" . ,(mapcar (lambda (track-id)
|
||||||
(let ((track (get-track-by-id track-id)))
|
(let ((track (get-track-by-id track-id)))
|
||||||
(when track
|
|
||||||
`(("id" . ,track-id)
|
`(("id" . ,track-id)
|
||||||
("title" . ,(gethash "title" track))
|
("title" . ,(gethash "title" track))
|
||||||
("artist" . ,(gethash "artist" track))
|
("artist" . ,(gethash "artist" track))
|
||||||
("album" . ,(gethash "album" track))))))
|
("album" . ,(gethash "album" track)))))
|
||||||
queue)))))
|
queue)))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error getting queue: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/stream/queue/add (track-id &optional (position "end")) ()
|
(define-api asteroid/stream/queue/add (track-id &optional (position "end")) ()
|
||||||
"Add a track to the stream queue"
|
"Add a track to the stream queue"
|
||||||
(require-role :admin)
|
(require-role :admin)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((tr-id (parse-integer track-id :junk-allowed t))
|
(let ((tr-id (parse-integer track-id :junk-allowed t))
|
||||||
(pos (if (string= position "next") :next :end)))
|
(pos (if (string= position "next") :next :end)))
|
||||||
(add-to-stream-queue tr-id pos)
|
(add-to-stream-queue tr-id pos)
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Track added to stream queue"))))
|
("message" . "Track added to stream queue"))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error adding to queue: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/stream/queue/remove (track-id) ()
|
(define-api asteroid/stream/queue/remove (track-id) ()
|
||||||
"Remove a track from the stream queue"
|
"Remove a track from the stream queue"
|
||||||
(require-role :admin)
|
(require-role :admin)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((tr-id (parse-integer track-id :junk-allowed t)))
|
(let ((tr-id (parse-integer track-id :junk-allowed t)))
|
||||||
(remove-from-stream-queue tr-id)
|
(remove-from-stream-queue tr-id)
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Track removed from stream queue"))))
|
("message" . "Track removed from stream queue"))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error removing from queue: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/stream/queue/clear () ()
|
(define-api asteroid/stream/queue/clear () ()
|
||||||
"Clear the entire stream queue"
|
"Clear the entire stream queue"
|
||||||
(require-role :admin)
|
(require-role :admin)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(progn
|
|
||||||
(clear-stream-queue)
|
(clear-stream-queue)
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Stream queue cleared"))))
|
("message" . "Stream queue cleared")))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error clearing queue: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/stream/queue/add-playlist (playlist-id) ()
|
(define-api asteroid/stream/queue/add-playlist (playlist-id) ()
|
||||||
"Add all tracks from a playlist to the stream queue"
|
"Add all tracks from a playlist to the stream queue"
|
||||||
(require-role :admin)
|
(require-role :admin)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((pl-id (parse-integer playlist-id :junk-allowed t)))
|
(let ((pl-id (parse-integer playlist-id :junk-allowed t)))
|
||||||
(add-playlist-to-stream-queue pl-id)
|
(add-playlist-to-stream-queue pl-id)
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Playlist added to stream queue"))))
|
("message" . "Playlist added to stream queue"))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error adding playlist to queue: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/stream/queue/reorder (track-ids) ()
|
(define-api asteroid/stream/queue/reorder (track-ids) ()
|
||||||
"Reorder the stream queue (expects comma-separated track IDs)"
|
"Reorder the stream queue (expects comma-separated track IDs)"
|
||||||
(require-role :admin)
|
(require-role :admin)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((ids (mapcar (lambda (id-str) (parse-integer id-str :junk-allowed t))
|
(let ((ids (mapcar (lambda (id-str) (parse-integer id-str :junk-allowed t))
|
||||||
(cl-ppcre:split "," track-ids))))
|
(cl-ppcre:split "," track-ids))))
|
||||||
(reorder-stream-queue ids)
|
(reorder-stream-queue ids)
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Stream queue reordered"))))
|
("message" . "Stream queue reordered"))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error reordering queue: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/stream/queue/load-m3u () ()
|
(define-api asteroid/stream/queue/load-m3u () ()
|
||||||
"Load stream queue from stream-queue.m3u file"
|
"Load stream queue from stream-queue.m3u file"
|
||||||
(require-role :admin)
|
(require-role :admin)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let ((count (load-queue-from-m3u-file)))
|
(let ((count (load-queue-from-m3u-file)))
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Queue loaded from M3U file")
|
("message" . "Queue loaded from M3U file")
|
||||||
("count" . ,count))))
|
("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"
|
||||||
|
|
@ -315,15 +257,19 @@
|
||||||
|
|
||||||
(define-page stream-track #@"/tracks/(.*)/stream" (:uri-groups (track-id))
|
(define-page stream-track #@"/tracks/(.*)/stream" (:uri-groups (track-id))
|
||||||
"Stream audio file by track ID"
|
"Stream audio file by track ID"
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let* ((id (parse-integer track-id))
|
(let* ((id (parse-integer track-id))
|
||||||
(track (get-track-by-id id)))
|
(track (get-track-by-id id)))
|
||||||
(if track
|
(unless track
|
||||||
|
(signal-not-found "track" id))
|
||||||
(let* ((file-path (first (gethash "file-path" track)))
|
(let* ((file-path (first (gethash "file-path" track)))
|
||||||
(format (first (gethash "format" track)))
|
(format (first (gethash "format" track)))
|
||||||
(file (probe-file file-path)))
|
(file (probe-file file-path)))
|
||||||
(if file
|
(unless file
|
||||||
(progn
|
(error 'not-found-error
|
||||||
|
:message "Audio file not found on disk"
|
||||||
|
:resource-type "file"
|
||||||
|
:resource-id file-path))
|
||||||
;; Set appropriate headers for audio streaming
|
;; Set appropriate headers for audio streaming
|
||||||
(setf (radiance:header "Content-Type") (get-mime-type-for-format format))
|
(setf (radiance:header "Content-Type") (get-mime-type-for-format format))
|
||||||
(setf (radiance:header "Accept-Ranges") "bytes")
|
(setf (radiance:header "Accept-Ranges") "bytes")
|
||||||
|
|
@ -332,22 +278,7 @@
|
||||||
(db:update "tracks" (db:query (:= '_id id))
|
(db:update "tracks" (db:query (:= '_id id))
|
||||||
`(("play-count" ,(1+ (first (gethash "play-count" track))))))
|
`(("play-count" ,(1+ (first (gethash "play-count" track))))))
|
||||||
;; Return file contents
|
;; Return file contents
|
||||||
(alexandria:read-file-into-byte-vector file))
|
(alexandria:read-file-into-byte-vector file)))))
|
||||||
(progn
|
|
||||||
(setf (radiance:header "Content-Type") "application/json")
|
|
||||||
(cl-json:encode-json-to-string
|
|
||||||
`(("status" . "error")
|
|
||||||
("message" . "Audio file not found on disk"))))))
|
|
||||||
(progn
|
|
||||||
(setf (radiance:header "Content-Type") "application/json")
|
|
||||||
(cl-json:encode-json-to-string
|
|
||||||
`(("status" . "error")
|
|
||||||
("message" . "Track not found"))))))
|
|
||||||
(error (e)
|
|
||||||
(setf (radiance:header "Content-Type") "application/json")
|
|
||||||
(cl-json:encode-json-to-string
|
|
||||||
`(("status" . "error")
|
|
||||||
("message" . ,(format nil "Streaming error: ~a" e)))))))
|
|
||||||
|
|
||||||
;; Player state management
|
;; Player state management
|
||||||
(defvar *current-track* nil "Currently playing track")
|
(defvar *current-track* nil "Currently playing track")
|
||||||
|
|
@ -390,11 +321,11 @@
|
||||||
;; Player control API endpoints
|
;; Player control API endpoints
|
||||||
(define-api asteroid/player/play (track-id) ()
|
(define-api asteroid/player/play (track-id) ()
|
||||||
"Start playing a track by ID"
|
"Start playing a track by ID"
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let* ((id (parse-integer track-id))
|
(let* ((id (parse-integer track-id))
|
||||||
(track (get-track-by-id id)))
|
(track (get-track-by-id id)))
|
||||||
(if track
|
(unless track
|
||||||
(progn
|
(signal-not-found "track" id))
|
||||||
(setf *current-track* id)
|
(setf *current-track* id)
|
||||||
(setf *player-state* :playing)
|
(setf *player-state* :playing)
|
||||||
(setf *current-position* 0)
|
(setf *current-position* 0)
|
||||||
|
|
@ -403,14 +334,7 @@
|
||||||
("track" . (("id" . ,id)
|
("track" . (("id" . ,id)
|
||||||
("title" . ,(first (gethash "title" track)))
|
("title" . ,(first (gethash "title" track)))
|
||||||
("artist" . ,(first (gethash "artist" track)))))
|
("artist" . ,(first (gethash "artist" track)))))
|
||||||
("player" . ,(get-player-status)))))
|
("player" . ,(get-player-status)))))))
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . "Track not found"))
|
|
||||||
:status 404)))
|
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Play error: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/player/pause () ()
|
(define-api asteroid/player/pause () ()
|
||||||
"Pause current playback"
|
"Pause current playback"
|
||||||
|
|
@ -519,40 +443,34 @@
|
||||||
;; Front page - regular view by default
|
;; 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"
|
|
||||||
(asdf:system-source-directory :asteroid))))
|
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "front-page")
|
||||||
:title "🎵 ASTEROID RADIO 🎵"
|
:title "🎵 ASTEROID RADIO 🎵"
|
||||||
:station-name "🎵 ASTEROID RADIO 🎵"
|
:station-name "🎵 ASTEROID RADIO 🎵"
|
||||||
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
|
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
|
||||||
:listeners "0"
|
:listeners "0"
|
||||||
:stream-quality "128kbps MP3"
|
:stream-quality "128kbps MP3"
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac"
|
:default-stream-encoding "audio/aac"
|
||||||
:default-stream-encoding-desc "AAC 96kbps Stereo"
|
:default-stream-encoding-desc "AAC 96kbps Stereo"
|
||||||
:now-playing-artist "The Void"
|
:now-playing-artist "The Void"
|
||||||
:now-playing-track "Silence"
|
:now-playing-track "Silence"
|
||||||
:now-playing-album "Startup Sounds"
|
:now-playing-album "Startup Sounds"
|
||||||
:now-playing-duration "∞")))
|
:now-playing-duration "∞"))
|
||||||
|
|
||||||
;; Frameset wrapper for persistent player mode
|
;; Frameset wrapper for persistent player mode
|
||||||
(define-page frameset-wrapper #@"/frameset" ()
|
(define-page frameset-wrapper #@"/frameset" ()
|
||||||
"Frameset wrapper with persistent audio player"
|
"Frameset wrapper with persistent audio player"
|
||||||
(let ((template-path (merge-pathnames "template/frameset-wrapper.chtml"
|
|
||||||
(asdf:system-source-directory :asteroid))))
|
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "frameset-wrapper")
|
||||||
:title "🎵 ASTEROID RADIO 🎵")))
|
:title "🎵 ASTEROID RADIO 🎵"))
|
||||||
|
|
||||||
;; Content frame - front page content without player
|
;; Content frame - front page content without player
|
||||||
(define-page front-page-content #@"/content" ()
|
(define-page front-page-content #@"/content" ()
|
||||||
"Front page content (displayed in content frame)"
|
"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
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "front-page-content")
|
||||||
:title "🎵 ASTEROID RADIO 🎵"
|
:title "🎵 ASTEROID RADIO 🎵"
|
||||||
:station-name "🎵 ASTEROID RADIO 🎵"
|
:station-name "🎵 ASTEROID RADIO 🎵"
|
||||||
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
|
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
|
||||||
|
|
@ -562,29 +480,27 @@
|
||||||
:now-playing-artist "The Void"
|
:now-playing-artist "The Void"
|
||||||
:now-playing-track "Silence"
|
:now-playing-track "Silence"
|
||||||
:now-playing-album "Startup Sounds"
|
:now-playing-album "Startup Sounds"
|
||||||
:now-playing-duration "∞")))
|
:now-playing-duration "∞"))
|
||||||
|
|
||||||
;; Persistent audio player frame (bottom frame)
|
;; Persistent audio player frame (bottom frame)
|
||||||
(define-page audio-player-frame #@"/audio-player-frame" ()
|
(define-page audio-player-frame #@"/audio-player-frame" ()
|
||||||
"Persistent audio player frame (bottom of page)"
|
"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
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "audio-player-frame")
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac")))
|
:default-stream-encoding "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 (format nil "static/~a" path)
|
||||||
(asdf:system-source-directory :asteroid))))
|
(asdf:system-source-directory :asteroid))))
|
||||||
|
|
||||||
;; Status check functions
|
;; Status check functions
|
||||||
(defun check-icecast-status ()
|
(defun check-icecast-status ()
|
||||||
"Check if Icecast server is running and accessible"
|
"Check if Icecast server is running and accessible"
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((response (drakma:http-request (concatenate 'string *stream-base-url* "/status-json.xsl")
|
(let ((response (drakma:http-request (format nil "~a/status-json.xsl" *stream-base-url*)
|
||||||
:want-stream nil
|
:want-stream nil
|
||||||
:connection-timeout 2)))
|
:connection-timeout 2)))
|
||||||
(if response "🟢 Running" "🔴 Not Running"))
|
(if response "🟢 Running" "🔴 Not Running"))
|
||||||
|
|
@ -606,13 +522,11 @@
|
||||||
(define-page admin #@"/admin" ()
|
(define-page admin #@"/admin" ()
|
||||||
"Admin dashboard"
|
"Admin dashboard"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(let ((template-path (merge-pathnames "template/admin.chtml"
|
(let ((track-count (handler-case
|
||||||
(asdf:system-source-directory :asteroid)))
|
|
||||||
(track-count (handler-case
|
|
||||||
(length (db:select "tracks" (db:query :all)))
|
(length (db:select "tracks" (db:query :all)))
|
||||||
(error () 0))))
|
(error () 0))))
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "admin")
|
||||||
:title "🎵 ASTEROID RADIO - Admin Dashboard"
|
:title "🎵 ASTEROID RADIO - Admin Dashboard"
|
||||||
:server-status "🟢 Running"
|
:server-status "🟢 Running"
|
||||||
:database-status (handler-case
|
:database-status (handler-case
|
||||||
|
|
@ -623,26 +537,22 @@
|
||||||
:track-count (format nil "~d" track-count)
|
:track-count (format nil "~d" track-count)
|
||||||
:library-path "/home/glenn/Projects/Code/asteroid/music/library/"
|
:library-path "/home/glenn/Projects/Code/asteroid/music/library/"
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac"))))
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*))))
|
||||||
|
|
||||||
;; User Management page (requires authentication)
|
;; User Management page (requires authentication)
|
||||||
(define-page users-management #@"/admin/user" ()
|
(define-page users-management #@"/admin/user" ()
|
||||||
"User Management dashboard"
|
"User Management dashboard"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(let ((template-path (merge-pathnames "template/users.chtml"
|
|
||||||
(asdf:system-source-directory :asteroid))))
|
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "users")
|
||||||
:title "🎵 ASTEROID RADIO - User Management")))
|
:title "🎵 ASTEROID RADIO - User Management"))
|
||||||
|
|
||||||
;; User Profile page (requires authentication)
|
;; User Profile page (requires authentication)
|
||||||
(define-page user-profile #@"/profile" ()
|
(define-page user-profile #@"/profile" ()
|
||||||
"User profile page"
|
"User profile page"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(let ((template-path (merge-pathnames "template/profile.chtml"
|
|
||||||
(asdf:system-source-directory :asteroid))))
|
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "profile")
|
||||||
:title "🎧 admin - Profile | Asteroid Radio"
|
:title "🎧 admin - Profile | Asteroid Radio"
|
||||||
:username "admin"
|
:username "admin"
|
||||||
:user-role "admin"
|
:user-role "admin"
|
||||||
|
|
@ -673,7 +583,7 @@
|
||||||
:top-artist-4 ""
|
:top-artist-4 ""
|
||||||
:top-artist-4-plays ""
|
:top-artist-4-plays ""
|
||||||
:top-artist-5 ""
|
:top-artist-5 ""
|
||||||
:top-artist-5-plays "")))
|
:top-artist-5-plays ""))
|
||||||
|
|
||||||
;; Helper functions for profile page - TEMPORARILY COMMENTED OUT
|
;; Helper functions for profile page - TEMPORARILY COMMENTED OUT
|
||||||
#|
|
#|
|
||||||
|
|
@ -710,7 +620,7 @@
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(let* ((current-user (auth:current-user))
|
(let* ((current-user (auth:current-user))
|
||||||
(username (gethash "username" current-user))
|
(username (gethash "username" current-user))
|
||||||
(template-path (merge-pathnames "template/profile.chtml"
|
(template-path (merge-pathnames "template/profile.ctml"
|
||||||
(asdf:system-source-directory :asteroid))))
|
(asdf:system-source-directory :asteroid))))
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(plump:parse (alexandria:read-file-into-string template-path))
|
||||||
|
|
@ -750,7 +660,7 @@
|
||||||
;; Auth status API endpoint
|
;; Auth status API endpoint
|
||||||
(define-api asteroid/auth-status () ()
|
(define-api asteroid/auth-status () ()
|
||||||
"Check if user is logged in and their role"
|
"Check if user is logged in and their role"
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let* ((user-id (session:field "user-id"))
|
(let* ((user-id (session:field "user-id"))
|
||||||
(user (when user-id (find-user-by-id user-id))))
|
(user (when user-id (find-user-by-id user-id))))
|
||||||
(api-output `(("loggedIn" . ,(if user t nil))
|
(api-output `(("loggedIn" . ,(if user t nil))
|
||||||
|
|
@ -758,18 +668,13 @@
|
||||||
("username" . ,(if user
|
("username" . ,(if user
|
||||||
(let ((username (gethash "username" user)))
|
(let ((username (gethash "username" user)))
|
||||||
(if (listp username) (first username) username))
|
(if (listp username) (first username) username))
|
||||||
nil)))))
|
nil)))))))
|
||||||
(error (e)
|
|
||||||
(api-output `(("loggedIn" . nil)
|
|
||||||
("isAdmin" . nil)
|
|
||||||
("error" . ,(format nil "~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
;; User profile API endpoints
|
;; User profile API endpoints
|
||||||
(define-api asteroid/user/profile () ()
|
(define-api asteroid/user/profile () ()
|
||||||
"Get current user profile information"
|
"Get current user profile information"
|
||||||
(require-authentication)
|
(require-authentication)
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let* ((user-id (session:field "user-id"))
|
(let* ((user-id (session:field "user-id"))
|
||||||
(user (find-user-by-id user-id)))
|
(user (find-user-by-id user-id)))
|
||||||
(if user
|
(if user
|
||||||
|
|
@ -779,13 +684,7 @@
|
||||||
("role" . ,(first (gethash "role" user)))
|
("role" . ,(first (gethash "role" user)))
|
||||||
("created_at" . ,(first (gethash "created-date" user)))
|
("created_at" . ,(first (gethash "created-date" user)))
|
||||||
("last_active" . ,(first (gethash "last-login" user)))))))
|
("last_active" . ,(first (gethash "last-login" user)))))))
|
||||||
(api-output `(("status" . "error")
|
(signal-not-found "user" user-id)))))
|
||||||
("message" . "User not found"))
|
|
||||||
:status 404)))
|
|
||||||
(error (e)
|
|
||||||
(api-output `(("status" . "error")
|
|
||||||
("message" . ,(format nil "Error loading profile: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
(define-api asteroid/user/listening-stats () ()
|
(define-api asteroid/user/listening-stats () ()
|
||||||
"Get user listening statistics"
|
"Get user listening statistics"
|
||||||
|
|
@ -862,40 +761,34 @@
|
||||||
:success-message ""))))
|
:success-message ""))))
|
||||||
|
|
||||||
(define-page player #@"/player" ()
|
(define-page player #@"/player" ()
|
||||||
(let ((template-path (merge-pathnames "template/player.chtml"
|
|
||||||
(asdf:system-source-directory :asteroid))))
|
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "player")
|
||||||
:title "Asteroid Radio - Web Player"
|
:title "Asteroid Radio - Web Player"
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:bitrate "128kbps MP3"
|
:bitrate "128kbps MP3"
|
||||||
:now-playing-artist "The Void"
|
:now-playing-artist "The Void"
|
||||||
:now-playing-track "Silence"
|
:now-playing-track "Silence"
|
||||||
:now-playing-album "Startup Sounds"
|
:now-playing-album "Startup Sounds"
|
||||||
:player-status "Stopped")))
|
:player-status "Stopped"))
|
||||||
|
|
||||||
;; Player content frame (for frameset mode)
|
;; Player content frame (for frameset mode)
|
||||||
(define-page player-content #@"/player-content" ()
|
(define-page player-content #@"/player-content" ()
|
||||||
"Player page content (displayed in content frame)"
|
"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
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "player-content")
|
||||||
:title "Asteroid Radio - Web Player"
|
:title "Asteroid Radio - Web Player"
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac")))
|
:default-stream-encoding "audio/aac"))
|
||||||
|
|
||||||
(define-page popout-player #@"/popout-player" ()
|
(define-page popout-player #@"/popout-player" ()
|
||||||
"Pop-out player window"
|
"Pop-out player window"
|
||||||
(let ((template-path (merge-pathnames "template/popout-player.chtml"
|
|
||||||
(asdf:system-source-directory :asteroid))))
|
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "popout-player")
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac")))
|
:default-stream-encoding "audio/aac"))
|
||||||
|
|
||||||
(define-api asteroid/status () ()
|
(define-api asteroid/status () ()
|
||||||
"Get server status"
|
"Get server status"
|
||||||
|
|
@ -907,14 +800,14 @@
|
||||||
("artist" . "The Void")
|
("artist" . "The Void")
|
||||||
("album" . "Startup Sounds")))
|
("album" . "Startup Sounds")))
|
||||||
("listeners" . 0)
|
("listeners" . 0)
|
||||||
("stream-url" . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
|
("stream-url" . ,(format nil "~a/asteroid.mp3" *stream-base-url*))
|
||||||
("stream-status" . "live"))))
|
("stream-status" . "live"))))
|
||||||
|
|
||||||
;; Live stream status from Icecast
|
;; Live stream status from Icecast
|
||||||
(define-api asteroid/icecast-status () ()
|
(define-api asteroid/icecast-status () ()
|
||||||
"Get live status from Icecast server"
|
"Get live status from Icecast server"
|
||||||
(handler-case
|
(with-error-handling
|
||||||
(let* ((icecast-url (concatenate 'string *stream-base-url* "/admin/stats.xml"))
|
(let* ((icecast-url (format nil "~a/admin/stats.xml" *stream-base-url*))
|
||||||
(response (drakma:http-request icecast-url
|
(response (drakma:http-request icecast-url
|
||||||
:want-stream nil
|
:want-stream nil
|
||||||
:basic-authorization '("admin" "asteroid_admin_2024"))))
|
:basic-authorization '("admin" "asteroid_admin_2024"))))
|
||||||
|
|
@ -936,7 +829,7 @@
|
||||||
(listeners (if listenersp (cl-ppcre:regex-replace-all ".*<listeners>(.*?)</listeners>.*" source-section "\\1") "0")))
|
(listeners (if listenersp (cl-ppcre:regex-replace-all ".*<listeners>(.*?)</listeners>.*" source-section "\\1") "0")))
|
||||||
;; Return JSON in format expected by frontend
|
;; Return JSON in format expected by frontend
|
||||||
(api-output
|
(api-output
|
||||||
`(("icestats" . (("source" . (("listenurl" . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
|
`(("icestats" . (("source" . (("listenurl" . ,(format nil "~a/asteroid.mp3" *stream-base-url*))
|
||||||
("title" . ,title)
|
("title" . ,title)
|
||||||
("listeners" . ,(parse-integer listeners :junk-allowed t)))))))))
|
("listeners" . ,(parse-integer listeners :junk-allowed t)))))))))
|
||||||
;; No source found, return empty
|
;; No source found, return empty
|
||||||
|
|
@ -944,11 +837,7 @@
|
||||||
`(("icestats" . (("source" . nil))))))))
|
`(("icestats" . (("source" . nil))))))))
|
||||||
(api-output
|
(api-output
|
||||||
`(("error" . "Could not connect to Icecast server"))
|
`(("error" . "Could not connect to Icecast server"))
|
||||||
:status 503)))
|
:status 503)))))
|
||||||
(error (e)
|
|
||||||
(api-output
|
|
||||||
`(("error" . ,(format nil "Icecast connection failed: ~a" e)))
|
|
||||||
:status 500))))
|
|
||||||
|
|
||||||
|
|
||||||
;; RADIANCE server management functions
|
;; RADIANCE server management functions
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
;;;; conditions.lisp - Custom error conditions for Asteroid Radio
|
||||||
|
;;;; Provides a hierarchy of error conditions for better error handling and debugging
|
||||||
|
|
||||||
|
(in-package :asteroid)
|
||||||
|
|
||||||
|
;;; Base Condition Hierarchy
|
||||||
|
|
||||||
|
(define-condition asteroid-error (error)
|
||||||
|
((message
|
||||||
|
:initarg :message
|
||||||
|
:reader error-message
|
||||||
|
:documentation "Human-readable error message"))
|
||||||
|
(:documentation "Base condition for all Asteroid-specific errors")
|
||||||
|
(:report (lambda (condition stream)
|
||||||
|
(format stream "Asteroid Error: ~a" (error-message condition)))))
|
||||||
|
|
||||||
|
;;; Specific Error Types
|
||||||
|
|
||||||
|
(define-condition database-error (asteroid-error)
|
||||||
|
((operation
|
||||||
|
:initarg :operation
|
||||||
|
:reader error-operation
|
||||||
|
:initform nil
|
||||||
|
:documentation "Database operation that failed (e.g., 'select', 'insert')"))
|
||||||
|
(:documentation "Signaled when a database operation fails")
|
||||||
|
(:report (lambda (condition stream)
|
||||||
|
(format stream "Database Error~@[ during ~a~]: ~a"
|
||||||
|
(error-operation condition)
|
||||||
|
(error-message condition)))))
|
||||||
|
|
||||||
|
(define-condition authentication-error (asteroid-error)
|
||||||
|
((user
|
||||||
|
:initarg :user
|
||||||
|
:reader error-user
|
||||||
|
:initform nil
|
||||||
|
:documentation "Username or user ID that failed authentication"))
|
||||||
|
(:documentation "Signaled when authentication fails")
|
||||||
|
(:report (lambda (condition stream)
|
||||||
|
(format stream "Authentication Error~@[ for user ~a~]: ~a"
|
||||||
|
(error-user condition)
|
||||||
|
(error-message condition)))))
|
||||||
|
|
||||||
|
(define-condition authorization-error (asteroid-error)
|
||||||
|
((required-role
|
||||||
|
:initarg :required-role
|
||||||
|
:reader error-required-role
|
||||||
|
:initform nil
|
||||||
|
:documentation "Role required for the operation"))
|
||||||
|
(:documentation "Signaled when user lacks required permissions")
|
||||||
|
(:report (lambda (condition stream)
|
||||||
|
(format stream "Authorization Error~@[ (requires ~a)~]: ~a"
|
||||||
|
(error-required-role condition)
|
||||||
|
(error-message condition)))))
|
||||||
|
|
||||||
|
(define-condition not-found-error (asteroid-error)
|
||||||
|
((resource-type
|
||||||
|
:initarg :resource-type
|
||||||
|
:reader error-resource-type
|
||||||
|
:initform nil
|
||||||
|
:documentation "Type of resource that wasn't found (e.g., 'track', 'user')")
|
||||||
|
(resource-id
|
||||||
|
:initarg :resource-id
|
||||||
|
:reader error-resource-id
|
||||||
|
:initform nil
|
||||||
|
:documentation "ID of the resource that wasn't found"))
|
||||||
|
(:documentation "Signaled when a requested resource doesn't exist")
|
||||||
|
(:report (lambda (condition stream)
|
||||||
|
(format stream "Not Found~@[ (~a~@[ ~a~])~]: ~a"
|
||||||
|
(error-resource-type condition)
|
||||||
|
(error-resource-id condition)
|
||||||
|
(error-message condition)))))
|
||||||
|
|
||||||
|
(define-condition validation-error (asteroid-error)
|
||||||
|
((field
|
||||||
|
:initarg :field
|
||||||
|
:reader error-field
|
||||||
|
:initform nil
|
||||||
|
:documentation "Field that failed validation"))
|
||||||
|
(:documentation "Signaled when input validation fails")
|
||||||
|
(:report (lambda (condition stream)
|
||||||
|
(format stream "Validation Error~@[ in field ~a~]: ~a"
|
||||||
|
(error-field condition)
|
||||||
|
(error-message condition)))))
|
||||||
|
|
||||||
|
(define-condition asteroid-stream-error (asteroid-error)
|
||||||
|
((stream-type
|
||||||
|
:initarg :stream-type
|
||||||
|
:reader error-stream-type
|
||||||
|
:initform nil
|
||||||
|
:documentation "Type of stream (e.g., 'icecast', 'liquidsoap')"))
|
||||||
|
(:documentation "Signaled when stream operations fail")
|
||||||
|
(:report (lambda (condition stream)
|
||||||
|
(format stream "Stream Error~@[ (~a)~]: ~a"
|
||||||
|
(error-stream-type condition)
|
||||||
|
(error-message condition)))))
|
||||||
|
|
||||||
|
;;; Error Handling Macros
|
||||||
|
|
||||||
|
(defmacro with-error-handling (&body body)
|
||||||
|
"Wrap API endpoint code with standard error handling.
|
||||||
|
Catches specific Asteroid errors and returns appropriate HTTP status codes.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
(define-api my-endpoint () ()
|
||||||
|
(with-error-handling
|
||||||
|
(do-something-that-might-fail)))"
|
||||||
|
`(handler-case
|
||||||
|
(progn ,@body)
|
||||||
|
(not-found-error (e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . ,(error-message e)))
|
||||||
|
:status 404))
|
||||||
|
(authentication-error (e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . ,(error-message e)))
|
||||||
|
:status 401))
|
||||||
|
(authorization-error (e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . ,(error-message e)))
|
||||||
|
:status 403))
|
||||||
|
(validation-error (e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . ,(error-message e)))
|
||||||
|
:status 400))
|
||||||
|
(database-error (e)
|
||||||
|
(format t "Database error: ~a~%" e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "Database operation failed"))
|
||||||
|
:status 500))
|
||||||
|
(asteroid-stream-error (e)
|
||||||
|
(format t "Stream error: ~a~%" e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "Stream operation failed"))
|
||||||
|
:status 500))
|
||||||
|
(asteroid-error (e)
|
||||||
|
(format t "Asteroid error: ~a~%" e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . ,(error-message e)))
|
||||||
|
:status 500))
|
||||||
|
(error (e)
|
||||||
|
(format t "Unexpected error: ~a~%" e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "An unexpected error occurred"))
|
||||||
|
:status 500))))
|
||||||
|
|
||||||
|
(defmacro with-db-error-handling (operation &body body)
|
||||||
|
"Wrap database operations with error handling.
|
||||||
|
Automatically converts database errors to database-error conditions.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
(with-db-error-handling \"select\"
|
||||||
|
(db:select 'tracks (db:query :all)))"
|
||||||
|
`(handler-case
|
||||||
|
(progn ,@body)
|
||||||
|
(error (e)
|
||||||
|
(error 'database-error
|
||||||
|
:message (format nil "~a" e)
|
||||||
|
:operation ,operation))))
|
||||||
|
|
||||||
|
;;; Helper Functions
|
||||||
|
|
||||||
|
(defun signal-not-found (resource-type resource-id)
|
||||||
|
"Signal a not-found-error with the given resource information."
|
||||||
|
(error 'not-found-error
|
||||||
|
:message (format nil "~a not found" resource-type)
|
||||||
|
:resource-type resource-type
|
||||||
|
:resource-id resource-id))
|
||||||
|
|
||||||
|
(defun signal-validation-error (field message)
|
||||||
|
"Signal a validation-error for the given field."
|
||||||
|
(error 'validation-error
|
||||||
|
:message message
|
||||||
|
:field field))
|
||||||
|
|
||||||
|
(defun signal-auth-error (user message)
|
||||||
|
"Signal an authentication-error for the given user."
|
||||||
|
(error 'authentication-error
|
||||||
|
:message message
|
||||||
|
:user user))
|
||||||
|
|
||||||
|
(defun signal-authz-error (required-role message)
|
||||||
|
"Signal an authorization-error with the required role."
|
||||||
|
(error 'authorization-error
|
||||||
|
:message message
|
||||||
|
:required-role required-role))
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -423,51 +423,6 @@ curl -X POST http://localhost:8080/api/asteroid/playlists/create \
|
||||||
|
|
||||||
When =browser=true= is passed, endpoints will redirect to appropriate pages instead of returning JSON.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
#+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.
|
||||||
|
|
||||||
|
#+BEGIN_QUOTE
|
||||||
|
*Note on Package Managers*: Examples use =apt= (Debian/Ubuntu). Replace with your distribution's package manager (=dnf=, =pacman=, =zypper=, =apk=, etc.).
|
||||||
|
#+END_QUOTE
|
||||||
|
|
||||||
* Architecture
|
* Architecture
|
||||||
|
|
||||||
** Container Stack
|
** Container Stack
|
||||||
|
|
@ -37,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
|
||||||
|
|
|
||||||
|
|
@ -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**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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*
|
||||||
|
|
@ -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-03
|
#+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,26 +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
|
||||||
- ✅ **Web Player** - Browser-based player with queue management
|
- ✅ **Multiple Player Modes** - Inline, pop-out, and persistent frameset players
|
||||||
|
- ✅ **Stream Queue Control** - Admin control over broadcast stream queue (M3U-based)
|
||||||
- ✅ **REST API** - Comprehensive JSON API with 15+ endpoints
|
- ✅ **REST API** - Comprehensive JSON API with 15+ endpoints
|
||||||
- ✅ **Music Streaming** - Multiple quality formats (MP3, AAC)
|
- ✅ **Music Streaming** - Multiple quality formats (128k MP3, 96k AAC, 64k MP3)
|
||||||
- ✅ **Rate Limiting** - Anti-abuse protection
|
- ✅ **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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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*
|
||||||
|
|
|
||||||
|
|
@ -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=
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
(in-package :asteroid)
|
(in-package :asteroid)
|
||||||
|
|
||||||
(defun icecast-now-playing (icecast-base-url)
|
(defun icecast-now-playing (icecast-base-url)
|
||||||
(let* ((icecast-url (concatenate 'string icecast-base-url "/admin/stats.xml"))
|
(let* ((icecast-url (format nil "~a/admin/stats.xml" icecast-base-url))
|
||||||
(response (drakma:http-request icecast-url
|
(response (drakma:http-request icecast-url
|
||||||
:want-stream nil
|
:want-stream nil
|
||||||
:basic-authorization '("admin" "asteroid_admin_2024"))))
|
:basic-authorization '("admin" "asteroid_admin_2024"))))
|
||||||
|
|
@ -22,31 +22,29 @@
|
||||||
(listenersp (cl-ppcre:all-matches "<listeners>" source-section))
|
(listenersp (cl-ppcre:all-matches "<listeners>" source-section))
|
||||||
(title (if titlep (cl-ppcre:regex-replace-all ".*<title>(.*?)</title>.*" source-section "\\1") "Unknown"))
|
(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")))
|
(listeners (if listenersp (cl-ppcre:regex-replace-all ".*<listeners>(.*?)</listeners>.*" source-section "\\1") "0")))
|
||||||
`((:listenurl . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
|
`((:listenurl . ,(format nil "~a/asteroid.mp3" *stream-base-url*))
|
||||||
(:title . ,title)
|
(:title . ,title)
|
||||||
(:listeners . ,(parse-integer listeners :junk-allowed t))))
|
(:listeners . ,(parse-integer listeners :junk-allowed t))))
|
||||||
`((:listenurl . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
|
`((:listenurl . ,(format nil "~a/asteroid.mp3" *stream-base-url*))
|
||||||
(:title . "Unknown")
|
(:title . "Unknown")
|
||||||
(:listeners . "Unknown"))))))))
|
(:listeners . "Unknown"))))))))
|
||||||
|
|
||||||
(define-api asteroid/partial/now-playing () ()
|
(define-api asteroid/partial/now-playing () ()
|
||||||
"Get Partial HTML with live status from Icecast server"
|
"Get Partial HTML with live status from Icecast server"
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((now-playing-stats (icecast-now-playing *stream-base-url*))
|
(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
|
(if now-playing-stats
|
||||||
(progn
|
(progn
|
||||||
;; TODO: it should be able to define a custom api-output for this
|
;; TODO: it should be able to define a custom api-output for this
|
||||||
;; (api-output <clip-parser> :format "html"))
|
;; (api-output <clip-parser> :format "html"))
|
||||||
(setf (header "Content-Type") "text/html")
|
(setf (header "Content-Type") "text/html")
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "partial/now-playing")
|
||||||
:stats now-playing-stats))
|
:stats now-playing-stats))
|
||||||
(progn
|
(progn
|
||||||
(setf (header "Content-Type") "text/html")
|
(setf (header "Content-Type") "text/html")
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(load-template "partial/now-playing")
|
||||||
:connection-error t
|
:connection-error t
|
||||||
:stats nil))))
|
:stats nil))))
|
||||||
(error (e)
|
(error (e)
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,32 @@
|
||||||
|
|
||||||
(in-package :asteroid)
|
(in-package :asteroid)
|
||||||
|
|
||||||
|
;; Template directory configuration
|
||||||
|
(defparameter *template-directory*
|
||||||
|
(merge-pathnames "template/" (asdf:system-source-directory :asteroid))
|
||||||
|
"Base directory for all CLIP templates")
|
||||||
|
|
||||||
;; Template cache for parsed templates
|
;; Template cache for parsed templates
|
||||||
(defvar *template-cache* (make-hash-table :test 'equal)
|
(defvar *template-cache* (make-hash-table :test 'equal)
|
||||||
"Cache for parsed template DOMs")
|
"Cache for parsed template DOMs")
|
||||||
|
|
||||||
|
(defun template-path (name)
|
||||||
|
"Build full path to template file.
|
||||||
|
NAME can be either:
|
||||||
|
- Simple name: 'front-page' -> 'template/front-page.ctml'
|
||||||
|
- Path with subdirs: 'partial/now-playing' -> 'template/partial/now-playing.ctml'"
|
||||||
|
(merge-pathnames (format nil "~a.ctml" name) *template-directory*))
|
||||||
|
|
||||||
|
(defun load-template (name)
|
||||||
|
"Load and parse a template by name without caching.
|
||||||
|
Use this for templates that change frequently during development."
|
||||||
|
(plump:parse (alexandria:read-file-into-string (template-path name))))
|
||||||
|
|
||||||
(defun get-template (template-name)
|
(defun get-template (template-name)
|
||||||
"Load and cache a template file"
|
"Load and cache a template file.
|
||||||
|
Use this for production - templates are cached after first load."
|
||||||
(or (gethash template-name *template-cache*)
|
(or (gethash template-name *template-cache*)
|
||||||
(let* ((template-path (merge-pathnames
|
(let ((parsed (load-template template-name)))
|
||||||
(format nil "template/~a.chtml" template-name)
|
|
||||||
(asdf:system-source-directory :asteroid)))
|
|
||||||
(parsed (plump:parse (alexandria:read-file-into-string template-path))))
|
|
||||||
(setf (gethash template-name *template-cache*) parsed)
|
(setf (gethash template-name *template-cache*) parsed)
|
||||||
parsed)))
|
parsed)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'VT323', monospace;
|
||||||
}
|
}
|
||||||
.persistent-player {
|
.persistent-player {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
color: #00ff00;
|
color: #00ff00;
|
||||||
border: 1px solid #00ff00;
|
border: 1px solid #00ff00;
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'VT323', monospace;
|
||||||
}
|
}
|
||||||
audio {
|
audio {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
|
|
||||||
<span class="now-playing-mini" id="mini-now-playing">Loading...</span>
|
<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: 'Courier New', monospace; font-size: 0.85em; white-space: nowrap;">
|
<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
|
✕ Disable
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -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)
|
||||||
|
(unless (db:connected-p)
|
||||||
|
(sleep 3)) ; Give database more time to initialize
|
||||||
(handler-case
|
(handler-case
|
||||||
(progn
|
(progn
|
||||||
(format t "Retrying user management setup...~%")
|
(format t "Retrying user management setup...~%")
|
||||||
(create-default-admin)
|
(create-default-admin)
|
||||||
(format t "User management initialization complete.~%"))
|
(format t "User management initialization complete.~%")
|
||||||
|
(return))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "Error initializing user system: ~a~%" e))))
|
(format t "Error initializing user system: ~a~%" e)))))
|
||||||
:name "user-init"))))
|
:name "user-init"))))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue