Compare commits
19 Commits
0dee51f344
...
775bc16261
| Author | SHA1 | Date |
|---|---|---|
|
|
775bc16261 | |
|
|
1c1cc995bd | |
|
|
fba21cb5c4 | |
|
|
c743e25e3d | |
|
|
a458a85823 | |
|
|
ab3acf1279 | |
|
|
c4fd96289b | |
|
|
0930fc2c1c | |
|
|
a2ae329d54 | |
|
|
66e97aaf37 | |
|
|
a795680e99 | |
|
|
d8abd9661d | |
|
|
01f5806959 | |
|
|
74cd3625f3 | |
|
|
9721fbbc8a | |
|
|
b3fd00cb4d | |
|
|
4d0b54f7d6 | |
|
|
f3d012cbc6 | |
|
|
d0efc89e33 |
|
|
@ -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" ]
|
||||
601
README.org
601
README.org
|
|
@ -1,59 +1,45 @@
|
|||
#+TITLE: Asteroid Radio - Internet Streaming Platform
|
||||
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade)
|
||||
#+DATE: 2025-10-16
|
||||
#+TITLE: Asteroid Radio - Internet Radio Streaming Platform
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Overview
|
||||
|
||||
Asteroid Radio is a modern, web-based music streaming platform built with Common Lisp and the Radiance framework. It provides a complete internet radio streaming system with live broadcasting, user management, playlist control, and professional audio processing.
|
||||
Asteroid Radio is a complete internet radio streaming platform built with Common Lisp, featuring a hacker-themed terminal aesthetic. The project combines the Radiance web framework with Icecast/Liquidsoap streaming infrastructure to create a full-featured music streaming platform with live broadcasting capabilities.
|
||||
|
||||
** Project Links
|
||||
- *Repository*: https://github.com/fade/asteroid
|
||||
- *IRC*: #asteroid.music on irc.libera.chat
|
||||
- *Documentation*: See =docs/= directory for comprehensive guides
|
||||
|
||||
* Key Features
|
||||
|
||||
** Live Internet Radio Streaming
|
||||
- Multiple streaming formats: AAC 96kbps (high efficiency), MP3 128kbps (standard), MP3 64kbps (low bandwidth)
|
||||
- Professional audio processing with ReplayGain for consistent volume without pumping
|
||||
- Smooth crossfading between tracks (5 second transitions)
|
||||
- Dynamic compression to prevent clipping
|
||||
- Multiple quality streams: 128kbps MP3, 96kbps AAC, 64kbps MP3
|
||||
- Professional audio processing with crossfading and ReplayGain normalization
|
||||
- Icecast2 streaming server integration
|
||||
- Liquidsoap audio pipeline with watch-based playlist reloading
|
||||
|
||||
** Stream Queue Control System
|
||||
- Curated stream queue management (play tracks in specific order)
|
||||
- Add tracks to end of queue or as "next to play"
|
||||
- Add entire playlists to stream queue
|
||||
- Real-time queue reordering
|
||||
- Automatic fallback to random playback when queue is empty
|
||||
- Stream history tracking
|
||||
- Admin-only queue control via REST API
|
||||
- Liquidsoap audio pipeline for reliable broadcasting
|
||||
- Stream queue control for curated programming
|
||||
|
||||
** Music Library Management
|
||||
- PostgreSQL-backed track storage with metadata extraction
|
||||
- Database-backed track storage with metadata extraction
|
||||
- Support for MP3, FLAC, OGG, and WAV formats
|
||||
- Automatic metadata extraction using taglib
|
||||
- Track search, filtering, and sorting capabilities
|
||||
- Pagination support for large libraries
|
||||
- Individual track streaming on-demand
|
||||
|
||||
** User Management System
|
||||
- User registration and authentication
|
||||
- Role-based access control (admin/user)
|
||||
- User profiles and session management
|
||||
- Personal playlist creation and management
|
||||
- Admin interface for user administration
|
||||
- Track search, filtering, sorting, and pagination
|
||||
- Recursive directory scanning
|
||||
|
||||
** Web Interface
|
||||
- RADIANCE framework with CLIP templating
|
||||
- Modern admin dashboard with JavaScript controls
|
||||
- Web player with HTML5 audio controls
|
||||
- Admin dashboard for library and user management
|
||||
- Multiple player modes: inline, pop-out, and persistent frameset
|
||||
- Live stream integration with embedded player
|
||||
- Dark hacker-themed aesthetic with VT323 font
|
||||
- Responsive design for desktop and mobile
|
||||
- Role-based access control (Admin/DJ/Listener)
|
||||
|
||||
** Network Broadcasting
|
||||
- Docker-based streaming infrastructure
|
||||
- WSL-compatible networking for internal network access
|
||||
- Dynamic stream URL detection for multi-environment support
|
||||
- Professional streaming URLs for media players
|
||||
- Multi-listener support via Icecast2
|
||||
- Telnet control interface for live DJ operations
|
||||
- Docker-based deployment for easy setup
|
||||
|
||||
* Architecture Changes
|
||||
|
||||
|
|
@ -64,62 +50,82 @@ Asteroid Radio is a modern, web-based music streaming platform built with Common
|
|||
- Database abstraction layer for track storage
|
||||
|
||||
** Streaming Stack
|
||||
- *Icecast2*: Streaming server (port 8000)
|
||||
- *Liquidsoap*: Audio processing and streaming pipeline with telnet control (port 1234)
|
||||
- *Icecast2*: Streaming server (port 8000) - Docker containerized
|
||||
- *Liquidsoap*: Audio processing and streaming pipeline - Docker containerized
|
||||
- *RADIANCE*: Web server and API (port 8080)
|
||||
- *PostgreSQL*: User accounts, track metadata, and playlist storage
|
||||
- *Docker Compose*: Container orchestration for streaming services
|
||||
- *PostgreSQL*: Database backend (configured, ready for migration)
|
||||
- *Docker Compose*: Container orchestration
|
||||
|
||||
** File Structure
|
||||
#+BEGIN_SRC
|
||||
asteroid/
|
||||
├── asteroid.lisp # Main server with RADIANCE routes
|
||||
├── asteroid.asd # System definition with dependencies
|
||||
├── stream-control.lisp # Stream queue management system
|
||||
├── stream-media.lisp # Media streaming and track management
|
||||
├── user-management.lisp # User authentication and profiles
|
||||
├── playlist-management.lisp # User playlist operations
|
||||
├── auth-routes.lisp # Authentication endpoints
|
||||
├── stream-queue.m3u # Generated stream queue playlist
|
||||
├── docker/ # Docker streaming infrastructure
|
||||
├── stream-control.lisp # Stream queue management
|
||||
├── user-management.lisp # User administration
|
||||
├── playlist-management.lisp # Playlist operations
|
||||
├── test-server.sh # Automated test suite
|
||||
├── docker/ # Docker infrastructure
|
||||
│ ├── docker-compose.yml # Container orchestration
|
||||
│ ├── asteroid-radio-docker.liq # Liquidsoap configuration
|
||||
│ └── start.sh # Container startup script
|
||||
│ ├── asteroid-radio-docker.liq # Liquidsoap config
|
||||
│ ├── icecast.xml # Icecast configuration
|
||||
│ └── music/ # Music library mount
|
||||
├── template/ # CLIP HTML templates
|
||||
│ ├── front-page.chtml # Main page with live stream
|
||||
│ ├── admin.chtml # Admin dashboard with queue controls
|
||||
│ ├── admin.chtml # Admin dashboard
|
||||
│ ├── player.chtml # Web player interface
|
||||
│ └── login.chtml # User authentication
|
||||
├── static/ # Frontend assets
|
||||
│ ├── asteroid.lass # LASS stylesheet source
|
||||
│ ├── asteroid.css # Compiled CSS
|
||||
│ └── js/
|
||||
│ ├── admin.js # Admin interface controls
|
||||
│ └── player.js # Web player functionality
|
||||
├── docs/ # Documentation
|
||||
│ ├── STREAM-CONTROL.org # Queue management guide
|
||||
│ ├── API-REFERENCE.org # Complete API documentation
|
||||
│ ├── USER-MANAGEMENT-SYSTEM.org # User system guide
|
||||
│ └── INSTALLATION.org # Setup instructions
|
||||
└── music/ # Music library
|
||||
└── library/ # Music files
|
||||
│ └── users.chtml # User management
|
||||
├── static/ # CSS and assets
|
||||
│ └── asteroid.lass # LASS stylesheet
|
||||
├── docs/ # Comprehensive documentation
|
||||
│ ├── README.org # Documentation index
|
||||
│ ├── PROJECT-OVERVIEW.org # Architecture overview
|
||||
│ ├── PROJECT-HISTORY.org # Development timeline
|
||||
│ ├── INSTALLATION.org # Setup guide
|
||||
│ └── ... # Additional guides
|
||||
└── music/ # Music library (local dev)
|
||||
#+END_SRC
|
||||
|
||||
* Track Upload Workflow
|
||||
* Quick Start
|
||||
|
||||
** Current Implementation (Manual Upload)
|
||||
1. *Copy files to staging*: Place MP3/FLAC files in =music/incoming/=
|
||||
2. *Access admin panel*: Navigate to =http://[IP]:8080/asteroid/admin=
|
||||
3. *Process files*: Click "Copy Files from Incoming" button
|
||||
4. *Database update*: Files are moved to =music/library/= and metadata extracted
|
||||
5. *Automatic playlist*: =playlist.m3u= is regenerated for streaming
|
||||
** Docker Installation (Recommended)
|
||||
#+BEGIN_SRC bash
|
||||
# Clone repository
|
||||
git clone https://github.com/fade/asteroid
|
||||
cd asteroid/docker
|
||||
|
||||
** File Processing Steps
|
||||
1. Files copied from =music/incoming/= to =music/library/=
|
||||
2. Metadata extracted using taglib (title, artist, album, duration, bitrate)
|
||||
3. Database record created with file path and metadata
|
||||
4. Playlist file updated for Liquidsoap streaming
|
||||
5. Files immediately available for on-demand streaming
|
||||
# Start all services
|
||||
docker compose up -d
|
||||
|
||||
# Verify streams are working
|
||||
curl -I http://localhost:8000/asteroid.mp3
|
||||
curl -I http://localhost:8000/asteroid.aac
|
||||
curl -I http://localhost:8000/asteroid-low.mp3
|
||||
#+END_SRC
|
||||
|
||||
** Access Points
|
||||
- *Web Interface*: http://localhost:8080/asteroid/
|
||||
- *Admin Panel*: http://localhost:8080/asteroid/admin
|
||||
- *High Quality MP3*: http://localhost:8000/asteroid.mp3 (128kbps)
|
||||
- *High Quality AAC*: http://localhost:8000/asteroid.aac (96kbps)
|
||||
- *Low Quality MP3*: http://localhost:8000/asteroid-low.mp3 (64kbps)
|
||||
- *Icecast Admin*: http://localhost:8000/admin/ (admin/asteroid_admin_2024)
|
||||
|
||||
* Music Library Management
|
||||
|
||||
** Adding Music
|
||||
1. *Copy files*: Place MP3/FLAC files in =docker/music/= directory
|
||||
2. *Access admin panel*: Navigate to =http://localhost:8080/asteroid/admin=
|
||||
3. *Scan library*: Click "Scan Library" to index new tracks
|
||||
4. *Metadata extraction*: Track information automatically extracted
|
||||
5. *Stream queue*: Optionally add tracks to broadcast queue
|
||||
|
||||
** Library Scanning
|
||||
1. Recursive directory scanning of music folder
|
||||
2. Metadata extracted using taglib (title, artist, album, duration)
|
||||
3. Database records created with file paths and metadata
|
||||
4. Tracks immediately available for playback and streaming
|
||||
5. Supports nested folder structures
|
||||
|
||||
** Supported Formats
|
||||
- *MP3*: Primary format, best compatibility
|
||||
|
|
@ -130,288 +136,259 @@ asteroid/
|
|||
* Icecast2 Integration
|
||||
|
||||
** Configuration
|
||||
- *Server*: localhost:8000
|
||||
- *Mount point*: =/asteroid.mp3=
|
||||
- *Password*: =b3l0wz3r0= (configured in Liquidsoap)
|
||||
- *Format*: MP3 128kbps stereo
|
||||
- *Server*: localhost:8000 (Docker container)
|
||||
- *Mount points*: =/asteroid.mp3=, =/asteroid.aac=, =/asteroid-low.mp3=
|
||||
- *Password*: =H1tn31EhsyLrfRmo= (configured in Docker setup)
|
||||
- *Formats*: MP3 128kbps, AAC 96kbps, MP3 64kbps
|
||||
|
||||
** Docker Setup
|
||||
Icecast2 runs in a Docker container - no manual installation needed.
|
||||
|
||||
** Installation (Ubuntu/Debian)
|
||||
#+BEGIN_SRC bash
|
||||
sudo apt update
|
||||
sudo apt install icecast2
|
||||
sudo systemctl enable icecast2
|
||||
sudo systemctl start icecast2
|
||||
# Managed via docker-compose
|
||||
cd docker
|
||||
docker compose up -d icecast
|
||||
#+END_SRC
|
||||
|
||||
** Stream Access
|
||||
- *Direct URL*: =http://[IP]:8000/asteroid.mp3=
|
||||
- *Admin interface*: =http://[IP]:8000/admin/=
|
||||
- *Statistics*: =http://[IP]:8000/status.xsl=
|
||||
- *High Quality MP3*: =http://localhost:8000/asteroid.mp3= (128kbps)
|
||||
- *High Quality AAC*: =http://localhost:8000/asteroid.aac= (96kbps)
|
||||
- *Low Quality MP3*: =http://localhost:8000/asteroid-low.mp3= (64kbps)
|
||||
- *Admin interface*: =http://localhost:8000/admin/= (admin/asteroid_admin_2024)
|
||||
- *Statistics*: =http://localhost:8000/status.xsl=
|
||||
|
||||
* Liquidsoap Integration
|
||||
|
||||
** Configuration File: =docker/asteroid-radio-docker.liq=
|
||||
#+BEGIN_SRC liquidsoap
|
||||
#!/usr/bin/liquidsoap
|
||||
** Docker Configuration
|
||||
Liquidsoap runs in a Docker container with configuration in =docker/asteroid-radio-docker.liq=
|
||||
|
||||
# Allow running as root in Docker
|
||||
set("init.allow_root", true)
|
||||
log.level.set(4)
|
||||
** Key Features
|
||||
- *Multiple outputs*: Generates 3 simultaneous streams (MP3 128k, AAC 96k, MP3 64k)
|
||||
- *Audio processing*: Crossfading, normalization, ReplayGain
|
||||
- *Stream queue*: Reads from M3U playlist for curated programming
|
||||
- *Telnet control*: Remote control interface on port 1234
|
||||
- *Metadata*: Broadcasts track information to listeners
|
||||
|
||||
# Enable telnet server for remote control
|
||||
settings.server.telnet.set(true)
|
||||
settings.server.telnet.port.set(1234)
|
||||
settings.server.telnet.bind_addr.set("0.0.0.0")
|
||||
|
||||
# Create playlist source from managed stream queue
|
||||
radio = playlist(
|
||||
mode="normal", # Play in order (not randomized)
|
||||
reload=5, # Check for playlist updates every 5 seconds
|
||||
reload_mode="watch", # Watch file for changes
|
||||
"/app/stream-queue.m3u"
|
||||
)
|
||||
|
||||
# Fallback to directory scan if queue is empty
|
||||
radio_fallback = playlist.safe(
|
||||
mode="randomize",
|
||||
reload=3600,
|
||||
"/app/music/"
|
||||
)
|
||||
|
||||
radio = fallback(track_sensitive=false, [radio, radio_fallback])
|
||||
|
||||
# Use ReplayGain for consistent volume without pumping
|
||||
radio = amplify(1.0, override="replaygain", radio)
|
||||
|
||||
# Add smooth crossfade between tracks (5 seconds)
|
||||
radio = crossfade(
|
||||
duration=5.0,
|
||||
fade_in=3.0,
|
||||
fade_out=3.0,
|
||||
radio
|
||||
)
|
||||
|
||||
# Add compressor to prevent clipping
|
||||
radio = compress(
|
||||
ratio=3.0,
|
||||
threshold=-15.0,
|
||||
attack=50.0,
|
||||
release=400.0,
|
||||
radio
|
||||
)
|
||||
|
||||
# Output to Icecast2 in multiple formats
|
||||
output.icecast(%mp3(bitrate=128), host="icecast", port=8000,
|
||||
password="H1tn31EhsyLrfRmo", mount="asteroid.mp3",
|
||||
name="Asteroid Radio", radio)
|
||||
|
||||
output.icecast(%fdkaac(bitrate=96), host="icecast", port=8000,
|
||||
password="H1tn31EhsyLrfRmo", mount="asteroid.aac",
|
||||
name="Asteroid Radio (AAC)", radio)
|
||||
|
||||
output.icecast(%mp3(bitrate=64), host="icecast", port=8000,
|
||||
password="H1tn31EhsyLrfRmo", mount="asteroid-low.mp3",
|
||||
name="Asteroid Radio (Low Quality)", radio)
|
||||
#+END_SRC
|
||||
|
||||
** Installation (Ubuntu/Debian)
|
||||
** Management
|
||||
#+BEGIN_SRC bash
|
||||
sudo apt update
|
||||
sudo apt install liquidsoap
|
||||
#+END_SRC
|
||||
|
||||
** Features
|
||||
- *Stream queue control*: Curated playlist with watch-based reloading (5 second updates)
|
||||
- *Smart fallback*: Random playback when queue is empty
|
||||
- *ReplayGain processing*: Consistent volume without pumping artifacts
|
||||
- *Crossfading*: Smooth 5-second transitions between tracks
|
||||
- *Dynamic compression*: Prevents clipping and maintains audio quality
|
||||
- *Multi-format output*: AAC 96kbps, MP3 128kbps, MP3 64kbps
|
||||
- *Telnet control*: Live DJ operations via port 1234
|
||||
- *Metadata broadcasting*: Track info sent to listeners
|
||||
|
||||
* Network Access
|
||||
|
||||
** Local Development
|
||||
- *Web Interface*: =http://localhost:8080/asteroid/=
|
||||
- *Live Stream*: =http://localhost:8000/asteroid.mp3=
|
||||
- *Admin Panel*: =http://localhost:8080/asteroid/admin=
|
||||
|
||||
** WSL Network Access
|
||||
- *WSL IP*: Check with =ip addr show eth0=
|
||||
- *Web Interface*: =http://[WSL-IP]:8080/asteroid/=
|
||||
- *Live Stream*: =http://[WSL-IP]:8000/asteroid.mp3=
|
||||
|
||||
** Internal Network Broadcasting
|
||||
- Services bind to all interfaces (0.0.0.0)
|
||||
- Accessible from any device on local network
|
||||
- Compatible with media players (VLC, iTunes, etc.)
|
||||
|
||||
* Usage Instructions
|
||||
|
||||
** Starting the Radio Station
|
||||
#+BEGIN_SRC bash
|
||||
# Start Docker streaming services
|
||||
# Start Liquidsoap container
|
||||
cd docker
|
||||
docker-compose up -d
|
||||
docker compose up -d liquidsoap
|
||||
|
||||
# Start Asteroid web application
|
||||
sbcl --load asteroid.lisp
|
||||
# View logs
|
||||
docker compose logs -f liquidsoap
|
||||
|
||||
# Restart streaming
|
||||
docker compose restart liquidsoap
|
||||
#+END_SRC
|
||||
|
||||
** Stopping the Radio Station
|
||||
** Telnet Control
|
||||
#+BEGIN_SRC bash
|
||||
# Stop Docker services
|
||||
cd docker
|
||||
docker-compose down
|
||||
# Connect to Liquidsoap
|
||||
telnet localhost 1234
|
||||
|
||||
# Or use netcat for scripting
|
||||
echo "request.queue" | nc localhost 1234
|
||||
echo "request.skip" | nc localhost 1234
|
||||
#+END_SRC
|
||||
|
||||
** Adding Music
|
||||
1. Copy MP3/FLAC files to =music/incoming/=
|
||||
2. Visit admin panel: =http://[IP]:8080/asteroid/admin=
|
||||
3. Click "Copy Files from Incoming"
|
||||
4. Files are processed and added to streaming playlist
|
||||
* User Management
|
||||
|
||||
** Listening to the Stream
|
||||
- *Web Browser*: Visit main page for embedded player
|
||||
- *Media Player*: Open =http://[IP]:8000/asteroid.mp3=
|
||||
- *Mobile Apps*: Use internet radio apps with stream URL
|
||||
** Roles
|
||||
- *Admin*: Full system access, user management, stream control
|
||||
- *DJ*: Content management, playlist creation, library access
|
||||
- *Listener*: Basic playback and personal playlists
|
||||
|
||||
** Default Credentials
|
||||
- Username: =admin=
|
||||
- Password: =asteroid123=
|
||||
- ⚠️ Change default password after first login
|
||||
|
||||
** User Administration
|
||||
- Create/manage users via admin panel
|
||||
- Role-based access control
|
||||
- User profiles and preferences
|
||||
- Session management
|
||||
|
||||
* Player Modes
|
||||
|
||||
** Inline Player
|
||||
- Embedded in web pages
|
||||
- Standard HTML5 audio controls
|
||||
- Queue management
|
||||
|
||||
** Pop-Out Player
|
||||
- Standalone player window
|
||||
- Independent from main browser window
|
||||
- Persistent across page navigation
|
||||
|
||||
** Frameset Player
|
||||
- Bottom-frame persistent player
|
||||
- Audio continues during site navigation
|
||||
- Seamless listening experience
|
||||
|
||||
* API Endpoints
|
||||
|
||||
Asteroid Radio provides a comprehensive REST API with 15+ endpoints.
|
||||
|
||||
** Status & Authentication
|
||||
- =GET /api/asteroid/status= - Server status
|
||||
- =GET /api/asteroid/auth-status= - Authentication status
|
||||
- =GET /api/asteroid/icecast-status= - Streaming status
|
||||
|
||||
** Track Management
|
||||
- =GET /api/asteroid/tracks= - List all tracks with pagination
|
||||
- =GET /api/asteroid/tracks/:id/stream= - Stream individual track
|
||||
- =POST /api/asteroid/scan-library= - Scan and update music library
|
||||
- =GET /api/asteroid/tracks?search={query}= - Search tracks
|
||||
- =GET /api/asteroid/tracks= - List all tracks
|
||||
- =GET /api/asteroid/admin/tracks= - Admin track listing
|
||||
- =POST /api/asteroid/admin/scan-library= - Scan music library
|
||||
|
||||
** Stream Queue Control (Admin Only)
|
||||
- =GET /api/asteroid/stream/queue= - Get current stream queue
|
||||
- =POST /api/asteroid/stream/queue/add= - Add track to queue (position: end/next)
|
||||
- =POST /api/asteroid/stream/queue/remove= - Remove track from queue
|
||||
- =POST /api/asteroid/stream/queue/clear= - Clear entire queue
|
||||
- =POST /api/asteroid/stream/queue/add-playlist= - Add playlist to queue
|
||||
- =POST /api/asteroid/stream/queue/reorder= - Reorder queue tracks
|
||||
- =GET /api/asteroid/stream/history= - Get stream history
|
||||
|
||||
** User Management
|
||||
- =POST /api/asteroid/auth/register= - Register new user
|
||||
- =POST /api/asteroid/auth/login= - User login
|
||||
- =POST /api/asteroid/auth/logout= - User logout
|
||||
- =GET /api/asteroid/auth/profile= - Get user profile
|
||||
- =GET /api/asteroid/users= - List users (admin only)
|
||||
- =POST /api/asteroid/users/:id/role= - Update user role (admin only)
|
||||
** Player Control
|
||||
- =GET /api/asteroid/player/status= - Player status
|
||||
- =POST /api/asteroid/player/play= - Play track
|
||||
- =POST /api/asteroid/player/pause= - Pause playback
|
||||
- =POST /api/asteroid/player/stop= - Stop playback
|
||||
- =POST /api/asteroid/player/resume= - Resume playback
|
||||
|
||||
** Playlist Management
|
||||
- =GET /api/asteroid/playlists= - List user's playlists
|
||||
- =POST /api/asteroid/playlists= - Create new playlist
|
||||
- =GET /api/asteroid/playlists/:id= - Get playlist details
|
||||
- =DELETE /api/asteroid/playlists/:id= - Delete playlist
|
||||
- =POST /api/asteroid/playlists/:id/tracks= - Add track to playlist
|
||||
- =DELETE /api/asteroid/playlists/:id/tracks/:track-id= - Remove track from playlist
|
||||
- =GET /api/asteroid/playlists= - List user playlists
|
||||
- =POST /api/asteroid/playlists/create= - Create playlist
|
||||
- =GET /api/asteroid/playlists/get= - Get playlist details
|
||||
- =POST /api/asteroid/playlists/add-track= - Add track to playlist
|
||||
|
||||
See =docs/API-REFERENCE.org= for complete API documentation.
|
||||
** Stream Queue Control (Admin)
|
||||
- =GET /api/asteroid/stream/queue= - Get broadcast queue
|
||||
- =POST /api/asteroid/stream/queue/add= - Add track to queue
|
||||
- =POST /api/asteroid/stream/queue/remove= - Remove from queue
|
||||
- =POST /api/asteroid/stream/queue/clear= - Clear queue
|
||||
|
||||
* Database Schema
|
||||
See =docs/API-ENDPOINTS.org= for complete API documentation.
|
||||
|
||||
** Tracks Table
|
||||
#+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
|
||||
* Database
|
||||
|
||||
** Users Table
|
||||
#+BEGIN_SRC lisp
|
||||
(db:create "users" '((username :text)
|
||||
(email :text)
|
||||
(password-hash :text)
|
||||
(role :text) ; "admin" or "user"
|
||||
(created-at :integer)
|
||||
(last-login :integer)))
|
||||
#+END_SRC
|
||||
** Current: Radiance DB
|
||||
- File-based database abstraction
|
||||
- Tracks, users, playlists, sessions
|
||||
- Suitable for development and small deployments
|
||||
|
||||
** Playlists Table
|
||||
#+BEGIN_SRC lisp
|
||||
(db:create "playlists" '((name :text)
|
||||
(description :text)
|
||||
(user-id :integer)
|
||||
(created-at :integer)
|
||||
(track-ids :text))) ; JSON array of track IDs
|
||||
#+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
|
||||
|
||||
** Lisp Dependencies (asteroid.asd)
|
||||
- =:radiance= - Web framework
|
||||
- =:r-clip= - Templating system
|
||||
- =:lass= - CSS generation
|
||||
- =:cl-json= - JSON handling
|
||||
- =:alexandria= - Utilities
|
||||
- =:local-time= - Time handling
|
||||
** Lisp Dependencies
|
||||
- =radiance= - Web framework
|
||||
- =r-clip= - CLIP templating
|
||||
- =lass= - CSS preprocessing
|
||||
- =cl-json= - JSON handling
|
||||
- =alexandria= - Common Lisp utilities
|
||||
- =local-time= - Time handling
|
||||
- =taglib= - Audio metadata extraction
|
||||
|
||||
** System Dependencies
|
||||
- =icecast2= - Streaming server
|
||||
- =liquidsoap= - Audio processing
|
||||
- =taglib= - Metadata extraction (via audio-streams)
|
||||
** System Dependencies (Docker)
|
||||
- Docker Engine 20.10+
|
||||
- Docker Compose 2.0+
|
||||
- All streaming components containerized
|
||||
|
||||
* Development Notes
|
||||
* Testing
|
||||
|
||||
** RADIANCE Configuration
|
||||
- Domain: "asteroid"
|
||||
- Routes use =#@= syntax for URL patterns
|
||||
- Database abstraction via =db:= functions
|
||||
- CLIP templates with =data-text= attributes
|
||||
** Automated Test Suite
|
||||
#+BEGIN_SRC bash
|
||||
# Run comprehensive tests
|
||||
./test-server.sh
|
||||
|
||||
** Database Queries
|
||||
- Use quoted symbols for field names: =(:= '_id id)=
|
||||
- RADIANCE returns hash tables with string keys
|
||||
- Primary key is "_id" internally, "id" in JSON responses
|
||||
# Verbose mode
|
||||
./test-server.sh -v
|
||||
#+END_SRC
|
||||
|
||||
** Streaming Considerations
|
||||
- MP3 files with spaces in names require playlist.m3u approach
|
||||
- Liquidsoap fallback prevents stream silence
|
||||
- Icecast2 mount points must match Liquidsoap configuration
|
||||
** Test Coverage
|
||||
- 25+ automated tests
|
||||
- API endpoint validation
|
||||
- HTML page rendering
|
||||
- Static file serving
|
||||
- JSON response format
|
||||
- Authentication flows
|
||||
|
||||
* Future Enhancements
|
||||
* Contributing
|
||||
|
||||
** Planned Features
|
||||
- Web UI for drag-and-drop queue management
|
||||
- Real-time now-playing display with WebSocket updates
|
||||
- Direct browser file uploads with progress bars
|
||||
- Listener statistics and analytics dashboard
|
||||
- Scheduled programming and automation
|
||||
- Social features (playlist sharing, discovery)
|
||||
- Mobile native applications
|
||||
** Development Workflow
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Run test suite
|
||||
5. Submit pull request
|
||||
|
||||
** Technical Improvements
|
||||
- WebSocket integration for real-time updates
|
||||
- Telnet integration for skip/next commands from web UI
|
||||
- Auto-queue filling (add tracks when queue runs low)
|
||||
- Genre-based smart queues
|
||||
- Listener request system
|
||||
- Full-text search capabilities
|
||||
** Community
|
||||
- *IRC*: #asteroid.music on irc.libera.chat
|
||||
- *Issues*: GitHub issue tracker
|
||||
- *Discussions*: GitHub discussions
|
||||
|
||||
** Core Team
|
||||
- Brian O'Reilly (Fade) - Project founder
|
||||
- Glenn Thompson (glenneth) - Core developer
|
||||
- Luis Pereira - UI/UX
|
||||
|
||||
* Troubleshooting
|
||||
|
||||
** Common Issues
|
||||
- *No audio in stream*: Check Liquidsoap logs, verify MP3 files
|
||||
- *Database errors*: Ensure proper field name quoting in queries
|
||||
- *Network access*: Verify WSL IP and firewall settings
|
||||
- *File upload issues*: Check permissions on music directories
|
||||
** Docker Issues
|
||||
#+BEGIN_SRC bash
|
||||
# Check container status
|
||||
docker compose ps
|
||||
|
||||
** Debugging
|
||||
- Enable Liquidsoap debug logging: =settings.log.level := 4=
|
||||
- Check Icecast admin interface for stream status
|
||||
- Monitor RADIANCE logs for web server issues
|
||||
- Verify database connectivity and collections
|
||||
# View logs
|
||||
docker compose logs icecast
|
||||
docker compose logs liquidsoap
|
||||
|
||||
# Restart services
|
||||
docker compose restart
|
||||
#+END_SRC
|
||||
|
||||
** Stream Not Playing
|
||||
- Verify containers are running
|
||||
- Check music files exist in =docker/music/=
|
||||
- Test stream URLs with curl
|
||||
- Review Liquidsoap logs
|
||||
|
||||
** Database Issues
|
||||
- Check Radiance DB file permissions
|
||||
- Verify database collections exist
|
||||
- Review application logs
|
||||
|
||||
For detailed troubleshooting, see documentation in =docs/= directory.
|
||||
|
||||
* License
|
||||
|
||||
This implementation maintains compatibility with the original Asteroid Radio project license while adding comprehensive streaming capabilities for internet radio broadcasting.
|
||||
See LICENSE file for details.
|
||||
|
||||
* Acknowledgments
|
||||
|
||||
Built with:
|
||||
- Common Lisp (SBCL)
|
||||
- Radiance web framework
|
||||
- Icecast2 streaming server
|
||||
- Liquidsoap audio processing
|
||||
- Docker containerization
|
||||
|
||||
Special thanks to all contributors and the Common Lisp community.
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-10-26*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -40,4 +40,5 @@
|
|||
(:file "playlist-management")
|
||||
(:file "stream-control")
|
||||
(:file "auth-routes")
|
||||
(:file "frontend-partials")
|
||||
(:file "asteroid")))
|
||||
|
|
|
|||
|
|
@ -277,6 +277,19 @@
|
|||
("message" . ,(format nil "Error reordering queue: ~a" e)))
|
||||
:status 500))))
|
||||
|
||||
(define-api asteroid/stream/queue/load-m3u () ()
|
||||
"Load stream queue from stream-queue.m3u file"
|
||||
(require-role :admin)
|
||||
(handler-case
|
||||
(let ((count (load-queue-from-m3u-file)))
|
||||
(api-output `(("status" . "success")
|
||||
("message" . "Queue loaded from M3U file")
|
||||
("count" . ,count))))
|
||||
(error (e)
|
||||
(api-output `(("status" . "error")
|
||||
("message" . ,(format nil "Error loading from M3U: ~a" e)))
|
||||
:status 500))))
|
||||
|
||||
(defun get-track-by-id (track-id)
|
||||
"Get a track by its ID - handles type mismatches"
|
||||
;; Try direct query first
|
||||
|
|
@ -503,7 +516,7 @@
|
|||
("message" . "Listening history cleared successfully"))))
|
||||
|#
|
||||
|
||||
;; Front page
|
||||
;; Front page - regular view by default
|
||||
(define-page front-page #@"/" ()
|
||||
"Main front page"
|
||||
(let ((template-path (merge-pathnames "template/front-page.chtml"
|
||||
|
|
@ -524,6 +537,44 @@
|
|||
:now-playing-album "Startup Sounds"
|
||||
:now-playing-duration "∞")))
|
||||
|
||||
;; Frameset wrapper for persistent player mode
|
||||
(define-page frameset-wrapper #@"/frameset" ()
|
||||
"Frameset wrapper with persistent audio player"
|
||||
(let ((template-path (merge-pathnames "template/frameset-wrapper.chtml"
|
||||
(asdf:system-source-directory :asteroid))))
|
||||
(clip:process-to-string
|
||||
(plump:parse (alexandria:read-file-into-string template-path))
|
||||
:title "🎵 ASTEROID RADIO 🎵")))
|
||||
|
||||
;; Content frame - front page content without player
|
||||
(define-page front-page-content #@"/content" ()
|
||||
"Front page content (displayed in content frame)"
|
||||
(let ((template-path (merge-pathnames "template/front-page-content.chtml"
|
||||
(asdf:system-source-directory :asteroid))))
|
||||
(clip:process-to-string
|
||||
(plump:parse (alexandria:read-file-into-string template-path))
|
||||
:title "🎵 ASTEROID RADIO 🎵"
|
||||
:station-name "🎵 ASTEROID RADIO 🎵"
|
||||
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
|
||||
:listeners "0"
|
||||
:stream-quality "128kbps MP3"
|
||||
:stream-base-url *stream-base-url*
|
||||
:now-playing-artist "The Void"
|
||||
:now-playing-track "Silence"
|
||||
:now-playing-album "Startup Sounds"
|
||||
:now-playing-duration "∞")))
|
||||
|
||||
;; Persistent audio player frame (bottom frame)
|
||||
(define-page audio-player-frame #@"/audio-player-frame" ()
|
||||
"Persistent audio player frame (bottom of page)"
|
||||
(let ((template-path (merge-pathnames "template/audio-player-frame.chtml"
|
||||
(asdf:system-source-directory :asteroid))))
|
||||
(clip:process-to-string
|
||||
(plump:parse (alexandria:read-file-into-string template-path))
|
||||
:stream-base-url *stream-base-url*
|
||||
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
|
||||
:default-stream-encoding "audio/aac")))
|
||||
|
||||
;; Configure static file serving for other files
|
||||
(define-page static #@"/static/(.*)" (:uri-groups (path))
|
||||
(serve-file (merge-pathnames (concatenate 'string "static/" path)
|
||||
|
|
@ -824,6 +875,28 @@
|
|||
:now-playing-album "Startup Sounds"
|
||||
:player-status "Stopped")))
|
||||
|
||||
;; Player content frame (for frameset mode)
|
||||
(define-page player-content #@"/player-content" ()
|
||||
"Player page content (displayed in content frame)"
|
||||
(let ((template-path (merge-pathnames "template/player-content.chtml"
|
||||
(asdf:system-source-directory :asteroid))))
|
||||
(clip:process-to-string
|
||||
(plump:parse (alexandria:read-file-into-string template-path))
|
||||
:title "Asteroid Radio - Web Player"
|
||||
:stream-base-url *stream-base-url*
|
||||
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
|
||||
:default-stream-encoding "audio/aac")))
|
||||
|
||||
(define-page popout-player #@"/popout-player" ()
|
||||
"Pop-out player window"
|
||||
(let ((template-path (merge-pathnames "template/popout-player.chtml"
|
||||
(asdf:system-source-directory :asteroid))))
|
||||
(clip:process-to-string
|
||||
(plump:parse (alexandria:read-file-into-string template-path))
|
||||
:stream-base-url *stream-base-url*
|
||||
:default-stream-url (concatenate 'string *stream-base-url* "/asteroid.aac")
|
||||
:default-stream-encoding "audio/aac")))
|
||||
|
||||
(define-api asteroid/status () ()
|
||||
"Get server status"
|
||||
(api-output `(("status" . "running")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ set("init.allow_root", true)
|
|||
# Set log level for debugging
|
||||
log.level.set(4)
|
||||
|
||||
# Audio buffering settings to prevent choppiness
|
||||
settings.frame.audio.samplerate.set(44100)
|
||||
settings.frame.audio.channels.set(2)
|
||||
settings.audio.converter.samplerate.libsamplerate.quality.set("best")
|
||||
|
||||
# Enable telnet server for remote control
|
||||
settings.server.telnet.set(true)
|
||||
settings.server.telnet.port.set(1234)
|
||||
|
|
@ -19,8 +24,8 @@ settings.server.telnet.bind_addr.set("0.0.0.0")
|
|||
# Falls back to directory scan if playlist file doesn't exist
|
||||
radio = playlist(
|
||||
mode="normal", # Play in order (not randomized)
|
||||
reload=5, # Check for playlist updates every 5 seconds
|
||||
reload_mode="watch", # Watch file for changes
|
||||
reload=30, # Check for playlist updates every 30 seconds
|
||||
reload_mode="seconds", # Reload every N seconds (prevents running out of tracks)
|
||||
"/app/stream-queue.m3u"
|
||||
)
|
||||
|
||||
|
|
@ -34,24 +39,11 @@ radio_fallback = playlist.safe(
|
|||
# Use main playlist, fall back to directory scan
|
||||
radio = fallback(track_sensitive=false, [radio, radio_fallback])
|
||||
|
||||
# Add some audio processing
|
||||
# Use ReplayGain for consistent volume without pumping
|
||||
radio = amplify(1.0, override="replaygain", radio)
|
||||
|
||||
# Add smooth crossfade between tracks (5 seconds)
|
||||
# Simple crossfade for smooth transitions
|
||||
radio = crossfade(
|
||||
duration=5.0, # 5 second crossfade
|
||||
fade_in=3.0, # 3 second fade in
|
||||
fade_out=3.0, # 3 second fade out
|
||||
radio
|
||||
)
|
||||
|
||||
# Add a compressor to prevent clipping
|
||||
radio = compress(
|
||||
ratio=3.0, # Compression ratio
|
||||
threshold=-15.0, # Threshold in dB
|
||||
attack=50.0, # Attack time in ms
|
||||
release=400.0, # Release time in ms
|
||||
duration=3.0, # 3 second crossfade
|
||||
fade_in=2.0, # 2 second fade in
|
||||
fade_out=2.0, # 2 second fade out
|
||||
radio
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
- icecast
|
||||
volumes:
|
||||
- ../music/library:/app/music:ro
|
||||
- ${MUSIC_LIBRARY:-../music/library}:/app/music:ro
|
||||
- ./asteroid-radio-docker.liq:/app/asteroid-radio.liq:ro
|
||||
- ../stream-queue.m3u:/app/stream-queue.m3u:ro
|
||||
- ${QUEUE_PLAYLIST:-../stream-queue.m3u}:/app/stream-queue.m3u:ro
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- asteroid-network
|
||||
|
|
|
|||
|
|
@ -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
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-10
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Overview
|
||||
|
||||
|
|
@ -340,186 +340,6 @@ curl -X POST http://localhost:8080/api/asteroid/playlists/add-track \
|
|||
}
|
||||
#+END_SRC
|
||||
|
||||
* Stream Queue Control Endpoints (Admin Only)
|
||||
|
||||
** GET /api/asteroid/stream/queue
|
||||
|
||||
Get the current stream queue.
|
||||
|
||||
*** Authentication
|
||||
Required (Admin role)
|
||||
|
||||
*** Response
|
||||
#+BEGIN_SRC json
|
||||
{
|
||||
"status": "success",
|
||||
"queue": [
|
||||
{
|
||||
"id": "track-id-123",
|
||||
"title": "Track Name",
|
||||
"artist": "Artist Name",
|
||||
"album": "Album Name",
|
||||
"duration": 245
|
||||
}
|
||||
],
|
||||
"queueLength": 10
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** POST /api/asteroid/stream/queue/add
|
||||
|
||||
Add a track to the stream queue.
|
||||
|
||||
*** Authentication
|
||||
Required (Admin role)
|
||||
|
||||
*** Parameters
|
||||
- =track-id= (required) - ID of the track to add
|
||||
- =position= (optional) - "end" (default) or "next"
|
||||
|
||||
*** Example Request
|
||||
#+BEGIN_SRC bash
|
||||
# Add to end of queue
|
||||
curl -X POST http://localhost:8080/api/asteroid/stream/queue/add \
|
||||
-d "track-id=123" \
|
||||
-b cookies.txt
|
||||
|
||||
# Add as next track
|
||||
curl -X POST http://localhost:8080/api/asteroid/stream/queue/add \
|
||||
-d "track-id=123&position=next" \
|
||||
-b cookies.txt
|
||||
#+END_SRC
|
||||
|
||||
*** Response
|
||||
#+BEGIN_SRC json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Track added to stream queue"
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** POST /api/asteroid/stream/queue/remove
|
||||
|
||||
Remove a track from the stream queue.
|
||||
|
||||
*** Authentication
|
||||
Required (Admin role)
|
||||
|
||||
*** Parameters
|
||||
- =track-id= (required) - ID of the track to remove
|
||||
|
||||
*** Example Request
|
||||
#+BEGIN_SRC bash
|
||||
curl -X POST http://localhost:8080/api/asteroid/stream/queue/remove \
|
||||
-d "track-id=123" \
|
||||
-b cookies.txt
|
||||
#+END_SRC
|
||||
|
||||
*** Response
|
||||
#+BEGIN_SRC json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Track removed from stream queue"
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** POST /api/asteroid/stream/queue/clear
|
||||
|
||||
Clear the entire stream queue.
|
||||
|
||||
*** Authentication
|
||||
Required (Admin role)
|
||||
|
||||
*** Response
|
||||
#+BEGIN_SRC json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Stream queue cleared"
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** POST /api/asteroid/stream/queue/add-playlist
|
||||
|
||||
Add all tracks from a playlist to the stream queue.
|
||||
|
||||
*** Authentication
|
||||
Required (Admin role)
|
||||
|
||||
*** Parameters
|
||||
- =playlist-id= (required) - ID of the playlist to add
|
||||
|
||||
*** Example Request
|
||||
#+BEGIN_SRC bash
|
||||
curl -X POST http://localhost:8080/api/asteroid/stream/queue/add-playlist \
|
||||
-d "playlist-id=5" \
|
||||
-b cookies.txt
|
||||
#+END_SRC
|
||||
|
||||
*** Response
|
||||
#+BEGIN_SRC json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Playlist added to stream queue",
|
||||
"tracksAdded": 15
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** POST /api/asteroid/stream/queue/reorder
|
||||
|
||||
Reorder the stream queue.
|
||||
|
||||
*** Authentication
|
||||
Required (Admin role)
|
||||
|
||||
*** Parameters
|
||||
- =track-ids= (required) - Comma-separated list of track IDs in desired order
|
||||
|
||||
*** Example Request
|
||||
#+BEGIN_SRC bash
|
||||
curl -X POST http://localhost:8080/api/asteroid/stream/queue/reorder \
|
||||
-d "track-ids=123,456,789" \
|
||||
-b cookies.txt
|
||||
#+END_SRC
|
||||
|
||||
*** Response
|
||||
#+BEGIN_SRC json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Stream queue reordered"
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
** GET /api/asteroid/stream/history
|
||||
|
||||
Get recently played tracks from the stream.
|
||||
|
||||
*** Authentication
|
||||
Required (Admin role)
|
||||
|
||||
*** Parameters
|
||||
- =limit= (optional) - Number of tracks to return (default: 10)
|
||||
|
||||
*** Example Request
|
||||
#+BEGIN_SRC bash
|
||||
curl "http://localhost:8080/api/asteroid/stream/history?limit=20" \
|
||||
-b cookies.txt
|
||||
#+END_SRC
|
||||
|
||||
*** Response
|
||||
#+BEGIN_SRC json
|
||||
{
|
||||
"status": "success",
|
||||
"history": [
|
||||
{
|
||||
"id": "track-id-123",
|
||||
"title": "Track Name",
|
||||
"artist": "Artist Name",
|
||||
"playedAt": "2025-10-16T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
#+END_SRC
|
||||
|
||||
* Admin Endpoints
|
||||
|
||||
** POST /api/asteroid/admin/scan-library
|
||||
|
|
@ -603,51 +423,6 @@ curl -X POST http://localhost:8080/api/asteroid/playlists/create \
|
|||
|
||||
When =browser=true= is passed, endpoints will redirect to appropriate pages instead of returning JSON.
|
||||
|
||||
* Integration Examples
|
||||
|
||||
** JavaScript/Fetch API
|
||||
|
||||
#+BEGIN_SRC javascript
|
||||
// Get tracks
|
||||
fetch('/api/asteroid/tracks')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Tracks:', data.tracks);
|
||||
});
|
||||
|
||||
// Play a track
|
||||
fetch('/api/asteroid/player/play', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'track-id=123'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Now playing:', data.player.currentTrack);
|
||||
});
|
||||
#+END_SRC
|
||||
|
||||
** Python
|
||||
|
||||
#+BEGIN_SRC python
|
||||
import requests
|
||||
|
||||
# Get server status
|
||||
response = requests.get('http://localhost:8080/api/asteroid/status')
|
||||
print(response.json())
|
||||
|
||||
# Create playlist (with session)
|
||||
session = requests.Session()
|
||||
# ... login first ...
|
||||
response = session.post(
|
||||
'http://localhost:8080/api/asteroid/playlists/create',
|
||||
data={'name': 'My Playlist', 'description': 'Test'}
|
||||
)
|
||||
print(response.json())
|
||||
#+END_SRC
|
||||
|
||||
* Rate Limiting
|
||||
|
||||
API endpoints implement rate limiting to prevent abuse. Excessive requests may result in temporary blocking.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#+TITLE: Asteroid Radio - Interface Reference
|
||||
#+TITLE: Asteroid Radio - API Reference
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-10
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Current Interfaces
|
||||
|
||||
|
|
|
|||
638
docs/DEV-LOG.org
638
docs/DEV-LOG.org
|
|
@ -1,638 +0,0 @@
|
|||
#+TITLE: Asteroid Radio - Development Log
|
||||
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade)
|
||||
#+DATE: 2024-09-11 to 2025-10-16
|
||||
|
||||
* Introduction
|
||||
|
||||
This document chronicles the complete development history of Asteroid Radio, from initial concept to a fully-featured internet radio streaming platform. The project evolved from a simple design document into a sophisticated Common Lisp web application with professional streaming infrastructure.
|
||||
|
||||
** Project Vision
|
||||
|
||||
Asteroid Radio is designed to be the premier streaming platform for "Asteroid Music" - the perfect soundtrack for developers, hackers, and anyone who spends hours deep in code. The mission is to curate and deliver music that enhances focus, creativity, and the flow state.
|
||||
|
||||
** Key Contributors
|
||||
|
||||
- *Glenn Thompson (glenneth)* - Core development, API architecture, user management, testing
|
||||
- *Brian O'Reilly (Fade)* - Project lead, infrastructure, CLIP templating, system architecture
|
||||
- *Luis Pereira (easilok)* - Frontend development, UI/UX improvements, styling
|
||||
|
||||
* Phase 1: Project Foundation (September 2024)
|
||||
|
||||
** 2024-09-11: Initial Commit
|
||||
- Project skeleton created
|
||||
- Basic repository structure established
|
||||
- Initial .gitignore configuration
|
||||
|
||||
** 2024-09-11: Design Document
|
||||
- Added comprehensive design document (design.org)
|
||||
- Defined project goals and architecture
|
||||
- Outlined feature roadmap for internet radio station
|
||||
- Established technical requirements
|
||||
|
||||
** 2024-09-11: Hunchentoot Implementation
|
||||
- Implemented initial web server using Hunchentoot
|
||||
- Basic HTTP server functionality
|
||||
- Foundation for web interface
|
||||
|
||||
* Phase 2: RADIANCE Migration (September 2024)
|
||||
|
||||
** 2024-09-11: Framework Migration
|
||||
- Migrated from Hunchentoot to RADIANCE web framework
|
||||
- Reorganized system around Radiance build conventions
|
||||
- Established proper module structure
|
||||
- Set up template directory structure
|
||||
|
||||
** 2024-09-11: LASS CSS System
|
||||
- Implemented LASS for dynamic CSS generation
|
||||
- Fixed LASS compilation issues
|
||||
- Enabled programmatic stylesheet generation
|
||||
- Established dark hacker theme aesthetic
|
||||
|
||||
** 2024-09-11: Build System
|
||||
- Created build-executable.lisp for deployment
|
||||
- Configured custom SBCL paths
|
||||
- Set up Makefile for build automation
|
||||
- Removed build artifacts from version control
|
||||
|
||||
* Phase 3: CLIP Templating (September 2024)
|
||||
|
||||
** 2024-09-11: Template System
|
||||
- Began CLIP templating implementation
|
||||
- Created initial template structure
|
||||
- Established template conventions
|
||||
- Set up dynamic content rendering
|
||||
|
||||
** 2024-09-11: Complete Template Migration
|
||||
- Completed CLIP template refactoring
|
||||
- Migrated all pages to CLIP system
|
||||
- Implemented template inheritance
|
||||
- Established consistent page structure
|
||||
|
||||
* Phase 4: Database Integration (September-October 2024)
|
||||
|
||||
** 2024-09-11: RADIANCE Database
|
||||
- Implemented RADIANCE database integration
|
||||
- Set up database abstraction layer
|
||||
- Created track storage schema
|
||||
- Established query patterns
|
||||
|
||||
** 2024-09-11: Metadata Extraction
|
||||
- Implemented complete metadata extraction
|
||||
- Added taglib integration for audio files
|
||||
- Extracted title, artist, album, duration, bitrate
|
||||
- Automated metadata processing pipeline
|
||||
|
||||
** 2024-09-11: Internet Radio System
|
||||
- Implemented complete internet radio streaming system
|
||||
- Integrated Icecast2 streaming server
|
||||
- Set up Liquidsoap audio pipeline
|
||||
- Established continuous broadcasting
|
||||
|
||||
** 2024-10-01: Database Refactoring
|
||||
- Refactored database features into discrete files
|
||||
- Improved code organization
|
||||
- Separated concerns for maintainability
|
||||
- Created modular file structure
|
||||
|
||||
* Phase 5: User Management System (October 2024)
|
||||
|
||||
** 2024-10-01: User System Foundation
|
||||
- Added user management dependencies
|
||||
- Created users.lisp module
|
||||
- Implemented user profile system
|
||||
- Set up authentication framework
|
||||
|
||||
** 2024-10-01: Authentication System
|
||||
- Fixed Asteroid Radio authentication system
|
||||
- Implemented secure password hashing
|
||||
- Created session management
|
||||
- Added login/logout functionality
|
||||
|
||||
** 2024-10-01: User Management API
|
||||
- Fixed user management API authentication
|
||||
- Implemented proper data formatting
|
||||
- Created user administration endpoints
|
||||
- Added role-based access control
|
||||
|
||||
** 2024-10-01: Recursive Music Scanning
|
||||
- Implemented recursive directory scanning
|
||||
- Added implicit depth-2 music discovery
|
||||
- Improved library management
|
||||
- Automated music file detection
|
||||
|
||||
* Phase 6: Docker Infrastructure (October 2024)
|
||||
|
||||
** 2024-10-02: Docker Streaming
|
||||
- Added Docker streaming infrastructure
|
||||
- Created Liquidsoap container configuration
|
||||
- Set up Icecast2 container
|
||||
- Established container networking
|
||||
|
||||
** 2024-10-02: Docker Integration
|
||||
- Completed Docker streaming infrastructure
|
||||
- Fixed user management integration
|
||||
- Created docker-compose.yml
|
||||
- Established volume mounts
|
||||
|
||||
** 2024-10-02: Web Interface Integration
|
||||
- Completed Docker streaming integration with web interface
|
||||
- Connected frontend to streaming backend
|
||||
- Implemented status monitoring
|
||||
- Added stream metadata display
|
||||
|
||||
** 2024-10-02: Docker Compose V2
|
||||
- Fixed Docker Compose V2 compatibility
|
||||
- Updated start/stop scripts
|
||||
- Modernized container orchestration
|
||||
- Improved deployment process
|
||||
|
||||
* Phase 7: AAC Streaming (October 2024)
|
||||
|
||||
** 2024-10-02: AAC Support
|
||||
- Added AAC streaming support
|
||||
- Implemented quality selector
|
||||
- Created multiple stream formats
|
||||
- Improved audio efficiency
|
||||
|
||||
** 2024-10-02: Icecast Configuration
|
||||
- Added Icecast mount configurations for all streams
|
||||
- Configured MP3 128kbps stream
|
||||
- Configured AAC 96kbps stream
|
||||
- Configured MP3 64kbps low-bandwidth stream
|
||||
|
||||
** 2024-10-02: Stream Metadata
|
||||
- Restored live stream metadata display
|
||||
- Fixed metadata extraction from Icecast
|
||||
- Implemented real-time track info
|
||||
- Added "now playing" functionality
|
||||
|
||||
** 2024-10-02: Documentation
|
||||
- Converted AAC-STREAMING.md to org-mode format
|
||||
- Updated streaming documentation
|
||||
- Added multi-format stream guides
|
||||
- Documented quality options
|
||||
|
||||
* Phase 8: Docker Refinement (October 2024)
|
||||
|
||||
** 2024-10-02: Docker Configuration
|
||||
- Updated Docker configuration for improved streaming
|
||||
- Optimized container settings
|
||||
- Improved volume management
|
||||
- Enhanced networking setup
|
||||
|
||||
** 2024-10-02: Utility Scripts
|
||||
- Restored Docker utility scripts per Fade's request
|
||||
- Created start-streaming.sh
|
||||
- Created stop-streaming.sh
|
||||
- Added test-streaming.sh
|
||||
|
||||
** 2024-10-02: Gitignore Updates
|
||||
- Added shell script exclusion to gitignore
|
||||
- Cleaned up version control
|
||||
- Removed build artifacts
|
||||
- Improved repository hygiene
|
||||
|
||||
* Phase 9: UI Improvements (October 2024)
|
||||
|
||||
** 2024-10-03: Color Scheme
|
||||
- Updated color scheme from green to blue theme
|
||||
- Implemented consistent color palette
|
||||
- Improved visual hierarchy
|
||||
- Enhanced dark theme aesthetics
|
||||
|
||||
** 2024-10-03: Template Features
|
||||
- Completed Templates section
|
||||
- Implemented CLIP refactoring
|
||||
- Added user management templates
|
||||
- Created pagination templates
|
||||
- Built playlist templates
|
||||
- Fixed UI issues
|
||||
- Documented PostgreSQL setup
|
||||
|
||||
** 2024-10-03: Status Monitoring
|
||||
- Added auto-scan on startup
|
||||
- Implemented live Icecast status checks
|
||||
- Added live Liquidsoap status checks
|
||||
- Created admin dashboard monitoring
|
||||
|
||||
* Phase 10: JavaScript Modularization (October 2024)
|
||||
|
||||
** 2024-10-04: Code Organization
|
||||
- Moved admin JavaScript code to own file (admin.js)
|
||||
- Moved frontpage JavaScript to own file
|
||||
- Moved player JavaScript to own file (player.js)
|
||||
- Moved users JavaScript to own file
|
||||
- Improved code maintainability
|
||||
|
||||
** 2024-10-04: User Management Page
|
||||
- Added user management page
|
||||
- Created admin interface for users
|
||||
- Implemented user listing
|
||||
- Added role management UI
|
||||
|
||||
* Phase 11: Styling Improvements (October 2024)
|
||||
|
||||
** 2024-10-05: LASS Fixes
|
||||
- Fixed: Move font import to LASS file
|
||||
- Fixed: LASS rules moved up one level
|
||||
- Fixed: Pseudo selectors now working in LASS
|
||||
- Improved CSS compilation
|
||||
|
||||
** 2024-10-05: Live Player Styling
|
||||
- Fixed sizing of live player
|
||||
- Improved player responsiveness
|
||||
- Enhanced player controls
|
||||
- Better mobile layout
|
||||
|
||||
** 2024-10-05: Navigation Styling
|
||||
- Used nav styles on front page
|
||||
- Implemented consistent navigation
|
||||
- Improved menu appearance
|
||||
- Enhanced user experience
|
||||
|
||||
* Phase 12: Playlist System (October 2024)
|
||||
|
||||
** 2024-10-04: Schema Fix
|
||||
- Fixed playlist schema mismatch
|
||||
- Used track-ids field consistently
|
||||
- Resolved data structure issues
|
||||
- Improved playlist reliability
|
||||
|
||||
* Phase 13: User Profiles (October 2024)
|
||||
|
||||
** 2024-10-06: Profile Pages
|
||||
- Added user profile page with CLIP template styling
|
||||
- Implemented profile edit functionality
|
||||
- Updated profile page to match site-wide layout
|
||||
- Created consistent styling
|
||||
|
||||
** 2024-10-06: Authentication UI
|
||||
- Added user registration UI improvements
|
||||
- Enhanced authentication interface
|
||||
- Fixed auth form styling (wider 600px forms)
|
||||
- Hidden message boxes for cleaner UI
|
||||
|
||||
* Phase 14: API Architecture (October 2024)
|
||||
|
||||
** 2024-10-07: API-Aware Authentication
|
||||
- WIP: Added API-aware authentication
|
||||
- Implemented detection for API routes
|
||||
- Fixed execution flow issues
|
||||
- Completed API-aware authentication returning JSON
|
||||
|
||||
** 2024-10-07: API Output Refactoring
|
||||
- Fixed api-output usage
|
||||
- Passed structured data with :status and :message
|
||||
- Standardized API responses
|
||||
- Improved error handling
|
||||
|
||||
* Phase 15: API Refactoring (October 2024)
|
||||
|
||||
** 2024-10-08: Define-API Migration
|
||||
- Refactored API endpoints to use Radiance's define-api macro
|
||||
- Modernized API architecture
|
||||
- Improved endpoint consistency
|
||||
- Better integration with Radiance framework
|
||||
|
||||
** 2024-10-08: Frontend Integration
|
||||
- Fixed frontend JavaScript to work with define-api endpoints
|
||||
- Updated AJAX calls
|
||||
- Improved error handling
|
||||
- Enhanced user feedback
|
||||
|
||||
** 2024-10-08: Automated Testing
|
||||
- Added comprehensive automated test suite
|
||||
- Created test-server.sh
|
||||
- Implemented API endpoint testing
|
||||
- Added integration tests
|
||||
|
||||
* Phase 16: Telnet Controls (October 2024)
|
||||
|
||||
** 2024-10-08: Liquidsoap DJ Controls
|
||||
- Added Liquidsoap DJ controls via telnet integration
|
||||
- Implemented remote control interface
|
||||
- Created telnet command system
|
||||
- Enabled live stream manipulation
|
||||
|
||||
* Phase 17: Merge Conflicts and Integration (October 2024)
|
||||
|
||||
** 2024-10-09: Track-IDs Integration
|
||||
- Resolved merge conflict
|
||||
- Integrated track-ids fix with api-output refactoring
|
||||
- Maintained data consistency
|
||||
- Preserved both feature sets
|
||||
|
||||
* Phase 18: Navigation Improvements (October 2024)
|
||||
|
||||
** 2024-10-09: Navbar Enhancement
|
||||
- Improved navbar in all pages
|
||||
- Enhanced nav styling
|
||||
- Created consistent navigation experience
|
||||
- Better responsive behavior
|
||||
|
||||
* Phase 19: Documentation Updates (October 2024)
|
||||
|
||||
** 2024-10-10: API Documentation
|
||||
- Updated documentation authors to Asteroid Radio Development Team
|
||||
- Documentation cleanup: removed outdated files
|
||||
- Added API docs
|
||||
- Updated core documentation
|
||||
- Created API-REFERENCE.org
|
||||
- Created API-ENDPOINTS.org
|
||||
|
||||
** 2024-10-10: Testing Documentation
|
||||
- Updated TESTING.org
|
||||
- Documented test suite
|
||||
- Added testing examples
|
||||
- Improved test coverage documentation
|
||||
|
||||
** 2024-10-10: Template Variables
|
||||
- Made stream base URL variable in templates
|
||||
- Improved configuration flexibility
|
||||
- Better environment handling
|
||||
|
||||
* Phase 20: System Configuration (October 2024)
|
||||
|
||||
** 2024-10-11: Parallel Processing
|
||||
- Added dependency to run music scan in parallel
|
||||
- Improved performance
|
||||
- Faster library scanning
|
||||
- Better resource utilization
|
||||
|
||||
** 2024-10-11: TODO Updates
|
||||
- Updated TODO with UI items
|
||||
- Tracked remaining work
|
||||
- Prioritized features
|
||||
|
||||
** 2024-10-12: Configuration Documentation
|
||||
- Added file for notes on application configuration
|
||||
- Documented IRC log of Shinmera chat
|
||||
- Preserved configuration discussions
|
||||
- Improved setup documentation
|
||||
|
||||
** 2024-10-12: Code Documentation
|
||||
- Added documentation string in scan-directory-for-music-recursively
|
||||
- Improved code readability
|
||||
- Better function documentation
|
||||
|
||||
* Phase 21: Page Flow System (October 2024)
|
||||
|
||||
** 2024-10-12: Role-Based Flow
|
||||
- Implemented role-based page flow
|
||||
- Created user management APIs
|
||||
- Added admin/user routing
|
||||
- Improved user experience
|
||||
|
||||
** 2024-10-12: UI Fixes
|
||||
- Completed UI fixes for page flow feature
|
||||
- Fixed navigation issues
|
||||
- Improved page transitions
|
||||
- Enhanced user feedback
|
||||
|
||||
** 2024-10-12: Documentation
|
||||
- Marked Page Flow feature as complete in TODO
|
||||
- Added session notes for page flow implementation
|
||||
- Documented implementation details
|
||||
|
||||
** 2024-10-12: README Update
|
||||
- Updated README.org file structure
|
||||
- Reflected current project state
|
||||
- Improved documentation accuracy
|
||||
|
||||
* Phase 22: Stream Queue Control (October 2025)
|
||||
|
||||
** 2025-10-14: Queue System Foundation
|
||||
- Added stream queue control system
|
||||
- Implemented in-memory queue management
|
||||
- Created stream-queue.m3u generation
|
||||
- Integrated with Liquidsoap
|
||||
|
||||
** 2025-10-14: Admin UI
|
||||
- Added admin UI for stream queue management
|
||||
- Created queue control interface
|
||||
- Implemented drag-and-drop (planned)
|
||||
- Added queue visualization
|
||||
|
||||
** 2025-10-14: Audio Quality Improvements
|
||||
- Improved audio quality and streaming performance
|
||||
- Replaced normalize() with ReplayGain
|
||||
- Added crossfading (5 seconds)
|
||||
- Implemented dynamic compression
|
||||
- Eliminated volume pumping issues
|
||||
|
||||
** 2025-10-14: Player Improvements
|
||||
- Improved player UI
|
||||
- Reduced buffering
|
||||
- Enhanced playback controls
|
||||
- Better user experience
|
||||
|
||||
* Phase 23: UI Polish (October 2025)
|
||||
|
||||
** 2025-10-14: Browser Compatibility
|
||||
- Fixed scrollbars only visible when required on Chrome browsers
|
||||
- Improved cross-browser compatibility
|
||||
- Better CSS handling
|
||||
|
||||
** 2025-10-14: Responsive Design
|
||||
- Fixed playlist create button wrap on small screens
|
||||
- Improved mobile experience
|
||||
- Enhanced responsive layout
|
||||
|
||||
** 2025-10-14: Icecast Integration
|
||||
- Fixed: avoid Icecast XML shown on frontend when there is no artist
|
||||
- Improved error handling
|
||||
- Better metadata display
|
||||
|
||||
* Phase 24: Documentation Overhaul (October 2025)
|
||||
|
||||
** 2025-10-16: Comprehensive Update
|
||||
- Comprehensive documentation update for current features
|
||||
- Updated README.org with all current capabilities
|
||||
- Documented stream queue control system
|
||||
- Added ReplayGain documentation
|
||||
- Updated multi-format streaming docs
|
||||
- Documented user management system
|
||||
- Complete API endpoint reference
|
||||
- Updated database schema documentation
|
||||
|
||||
** 2025-10-16: API Documentation
|
||||
- Added complete stream queue control API section
|
||||
- Documented all 7 stream queue endpoints
|
||||
- Added authentication requirements
|
||||
- Included request/response examples
|
||||
|
||||
** 2025-10-16: Docker Documentation
|
||||
- Updated Liquidsoap config documentation
|
||||
- Documented stream queue integration
|
||||
- Added queue management examples
|
||||
- Updated audio processing details
|
||||
|
||||
** 2025-10-16: Project Overview
|
||||
- Updated PROJECT-OVERVIEW with stream queue
|
||||
- Added ReplayGain mentions
|
||||
- Updated feature list
|
||||
- Refreshed project vision
|
||||
|
||||
* Technical Milestones
|
||||
|
||||
** Architecture Evolution
|
||||
1. *Hunchentoot* → *RADIANCE* (Major framework migration)
|
||||
2. *Simple HTML* → *CLIP Templates* (Dynamic templating)
|
||||
3. *File-based* → *PostgreSQL Database* (Persistent storage)
|
||||
4. *Single stream* → *Multi-format streaming* (AAC, MP3 high/low)
|
||||
5. *Random playback* → *Queue control* (Curated broadcasting)
|
||||
|
||||
** Audio Processing Evolution
|
||||
1. *Basic amplify* → *Normalize* → *ReplayGain* (Volume consistency)
|
||||
2. *No crossfade* → *5-second crossfade* (Smooth transitions)
|
||||
3. *No compression* → *Dynamic compression* (Clipping prevention)
|
||||
|
||||
** API Evolution
|
||||
1. *Custom routes* → *define-api macro* (Standardization)
|
||||
2. *Mixed responses* → *JSON API* (Consistent format)
|
||||
3. *No auth* → *Session-based auth* (Security)
|
||||
4. *Basic endpoints* → *20+ endpoints* (Comprehensive API)
|
||||
|
||||
** Infrastructure Evolution
|
||||
1. *Native installation* → *Docker containers* (Easy deployment)
|
||||
2. *Single Icecast* → *Icecast + Liquidsoap* (Professional streaming)
|
||||
3. *No telnet* → *Telnet control* (Live DJ operations)
|
||||
|
||||
* Feature Summary
|
||||
|
||||
** Core Features Implemented
|
||||
- ✅ User authentication and registration
|
||||
- ✅ Role-based access control (admin/user)
|
||||
- ✅ Music library management with metadata
|
||||
- ✅ User playlists (create, edit, delete)
|
||||
- ✅ Stream queue control (admin only)
|
||||
- ✅ Multi-format streaming (AAC 96k, MP3 128k/64k)
|
||||
- ✅ ReplayGain audio processing
|
||||
- ✅ Crossfading and compression
|
||||
- ✅ Web player with controls
|
||||
- ✅ Live stream metadata display
|
||||
- ✅ Admin dashboard
|
||||
- ✅ REST API (20+ endpoints)
|
||||
- ✅ Automated testing suite
|
||||
- ✅ Docker deployment
|
||||
- ✅ Telnet DJ controls
|
||||
- ✅ PostgreSQL database
|
||||
- ✅ Responsive design
|
||||
- ✅ Dark hacker theme
|
||||
|
||||
** Planned Features
|
||||
- 🔄 WebSocket real-time updates
|
||||
- 🔄 Drag-and-drop queue management
|
||||
- 🔄 Social features (playlist sharing)??
|
||||
- 🔄 Advanced search and filtering
|
||||
- 🔄 Listener statistics
|
||||
- 🔄 Scheduled programming
|
||||
- 🔄 Auto-queue filling
|
||||
- 🔄 Genre-based smart queues
|
||||
|
||||
* Development Statistics
|
||||
|
||||
** Commit Count by Phase
|
||||
- Phase 1-2 (Foundation): ~15 commits
|
||||
- Phase 3-4 (Templates/DB): ~10 commits
|
||||
- Phase 5-6 (Users/Docker): ~20 commits
|
||||
- Phase 7-9 (Streaming): ~15 commits
|
||||
- Phase 10-14 (UI/Profiles): ~25 commits
|
||||
- Phase 15-17 (API): ~15 commits
|
||||
- Phase 18-21 (Polish): ~20 commits
|
||||
- Phase 22-24 (Queue/Docs): ~10 commits
|
||||
|
||||
** Key Contributors Statistics
|
||||
- Glenn Thompson: ~70 commits (Core development, API, testing)
|
||||
- Brian O'Reilly: ~40 commits (Infrastructure, architecture)
|
||||
- Luis Pereira: ~20 commits (Frontend, UI/UX)
|
||||
|
||||
** Technology Stack
|
||||
- *Language*: Common Lisp (SBCL)
|
||||
- *Framework*: RADIANCE
|
||||
- *Database*: PostgreSQL
|
||||
- *Templating*: CLIP
|
||||
- *CSS*: LASS
|
||||
- *Streaming*: Icecast2 + Liquidsoap
|
||||
- *Containers*: Docker + Docker Compose
|
||||
- *Audio*: ReplayGain, crossfade, compression
|
||||
|
||||
* Lessons Learned
|
||||
|
||||
** Framework Migration
|
||||
The migration from Hunchentoot to RADIANCE was challenging but worthwhile. RADIANCE's module system and database abstraction provided better structure for a growing application.
|
||||
|
||||
** Audio Processing
|
||||
The evolution from normalize() to ReplayGain solved the volume pumping issue. ReplayGain uses track metadata for consistent volume without dynamic compression artifacts.
|
||||
|
||||
** API Design
|
||||
Migrating to Radiance's define-api macro standardized our API and improved maintainability. Consistent JSON responses made frontend integration much easier.
|
||||
|
||||
** Docker Deployment
|
||||
Containerizing the streaming infrastructure simplified deployment and made the system more portable. The separation of concerns between web app and streaming services improved reliability.
|
||||
|
||||
** Testing
|
||||
Adding automated tests early would have caught integration issues faster. The comprehensive test suite added in Phase 15 significantly improved code quality.
|
||||
|
||||
* Future Directions
|
||||
|
||||
** Short Term (Next 3 Months?)
|
||||
- WebSocket integration for real-time updates
|
||||
- Drag-and-drop queue management UI
|
||||
- Telnet integration from web interface
|
||||
- Listener statistics dashboard
|
||||
|
||||
** Medium Term (6-12 Months)
|
||||
- Social features (playlist sharing, discovery)
|
||||
- Advanced search with full-text indexing
|
||||
- Mobile native applications
|
||||
- Scheduled programming system
|
||||
|
||||
** Long Term (12+ Months)
|
||||
- Multi-station support
|
||||
- Federation with other Asteroid Radio instances
|
||||
- Machine learning for music recommendations
|
||||
- Live DJ streaming capabilities
|
||||
|
||||
* Conclusion
|
||||
|
||||
Asteroid Radio has evolved from a simple design document into a sophisticated internet radio platform. The journey from Hunchentoot to RADIANCE, from file-based storage to PostgreSQL, from single-stream to multi-format broadcasting, and from random playback to curated queue control demonstrates the power of iterative development and continuous improvement.
|
||||
|
||||
The project successfully combines the elegance of Common Lisp with modern web technologies, creating a platform that's both technically impressive and user-friendly. The dark hacker aesthetic, professional audio processing, and comprehensive API make Asteroid Radio a unique contribution to the internet radio landscape.
|
||||
|
||||
Most importantly, Asteroid Radio achieves its core mission: providing the perfect soundtrack for developers, hackers, and anyone who spends hours deep in code.
|
||||
|
||||
* Appendix: Key Files
|
||||
|
||||
** Core Application
|
||||
- =asteroid.lisp= - Main application with RADIANCE routes
|
||||
- =asteroid.asd= - System definition
|
||||
- =stream-control.lisp= - Queue management
|
||||
- =stream-media.lisp= - Media streaming
|
||||
- =user-management.lisp= - User authentication
|
||||
- =playlist-management.lisp= - Playlist operations
|
||||
- =auth-routes.lisp= - Authentication endpoints
|
||||
|
||||
** Configuration
|
||||
- =docker/docker-compose.yml= - Container orchestration
|
||||
- =docker/asteroid-radio-docker.liq= - Liquidsoap configuration
|
||||
- =docker/icecast.xml= - Icecast server config
|
||||
|
||||
** Frontend
|
||||
- =static/asteroid.lass= - Stylesheet source
|
||||
- =static/js/admin.js= - Admin interface (289 lines)
|
||||
- =static/js/player.js= - Web player
|
||||
- =template/*.chtml= - CLIP templates
|
||||
|
||||
** Documentation
|
||||
- =README.org= - Project overview
|
||||
- =docs/STREAM-CONTROL.org= - Queue management guide
|
||||
- =docs/API-ENDPOINTS.org= - Complete API reference
|
||||
- =docs/DOCKER-STREAMING.org= - Streaming setup
|
||||
- =docs/DEV-LOG.org= - This document
|
||||
|
||||
---
|
||||
|
||||
*End of Development Log*
|
||||
|
||||
Last Updated: 2025-10-16
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
#+TITLE: Asteroid Radio - Development Guide
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-10
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Development Setup
|
||||
|
||||
#+BEGIN_QUOTE
|
||||
*Note on Package Managers*: Examples use =apt= (Debian/Ubuntu). Replace with your distribution's package manager (=dnf=, =pacman=, =zypper=, =apk=, etc.).
|
||||
#+END_QUOTE
|
||||
|
||||
** Prerequisites
|
||||
|
||||
*** System Dependencies
|
||||
|
|
@ -68,7 +72,7 @@ sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --quit
|
|||
|
||||
*** Clone Repository
|
||||
#+BEGIN_SRC bash
|
||||
git clone <repository-url>
|
||||
git clone https://github.com/fade/asteroid.git
|
||||
cd asteroid
|
||||
#+END_SRC
|
||||
|
||||
|
|
@ -125,9 +129,9 @@ sbcl --eval "(ql:quickload :asteroid)" --eval "(asteroid:start-server)"
|
|||
** Music Library Management
|
||||
|
||||
*** Directory Structure
|
||||
The music directory structure is:
|
||||
The music directory is located directly under the asteroid root directory:
|
||||
#+BEGIN_SRC
|
||||
asteroid/docker/music/ # Host directory (mounted to containers)
|
||||
asteroid/music/ # Music directory (can be symlink)
|
||||
├── artist1/
|
||||
│ ├── album1/
|
||||
│ │ ├── track1.mp3
|
||||
|
|
@ -138,6 +142,11 @@ asteroid/docker/music/ # Host directory (mounted to containers)
|
|||
└── single.wav
|
||||
#+END_SRC
|
||||
|
||||
The =music/= directory can be:
|
||||
- A regular directory with music files
|
||||
- A symlink to your actual music collection
|
||||
- Multiple subdirectories or symlinks within it
|
||||
|
||||
*** Recursive Scanning Capabilities
|
||||
The Asteroid application includes built-in recursive directory scanning:
|
||||
- *Function*: =scan-music-library= in =stream-media.lisp=
|
||||
|
|
@ -149,16 +158,21 @@ The Asteroid application includes built-in recursive directory scanning:
|
|||
*** Adding Music to Development Environment
|
||||
#+BEGIN_SRC bash
|
||||
# Option 1: Copy music files directly
|
||||
cp -r /path/to/your/music/* docker/music/
|
||||
cp -r /path/to/your/music/* music/
|
||||
|
||||
# Option 2: Mount remote directory (for large collections)
|
||||
# Option 2: Symlink entire music directory
|
||||
ln -s /path/to/existing/music music
|
||||
|
||||
# Option 3: Symlink subdirectories within music/
|
||||
mkdir -p music
|
||||
ln -s /path/to/collection1 music/collection1
|
||||
ln -s /path/to/collection2 music/collection2
|
||||
|
||||
# Option 4: Mount remote directory (for large collections)
|
||||
# Edit docker-compose.yml to change volume mount:
|
||||
# volumes:
|
||||
# - /mnt/remote-music:/app/music:ro
|
||||
|
||||
# Option 3: Symlink to existing collection
|
||||
ln -s /path/to/existing/music docker/music/collection
|
||||
|
||||
# Trigger library scan via API
|
||||
curl -X POST http://localhost:8080/api/asteroid/admin/scan-library
|
||||
#+END_SRC
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
#+TITLE: Asteroid Radio - Docker Streaming Setup
|
||||
#+AUTHOR: Docker Team
|
||||
#+DATE: 2025-10-03
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Docker Streaming Overview
|
||||
|
||||
This guide covers the complete Docker-based streaming setup for Asteroid Radio using Icecast2 and Liquidsoap containers. This approach provides a containerized, portable streaming infrastructure that's easy to deploy and maintain.
|
||||
|
||||
** Key Features
|
||||
- Stream queue control system for curated playlists
|
||||
- ReplayGain audio processing for consistent volume
|
||||
- Automatic fallback to random playback
|
||||
- Multi-format streaming (AAC, MP3 high/low)
|
||||
- Telnet control interface for live DJ operations
|
||||
#+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
|
||||
|
||||
|
|
@ -25,12 +22,6 @@ This guide covers the complete Docker-based streaming setup for Asteroid Radio u
|
|||
- *High Quality AAC*: 96kbps AAC stream at /asteroid.aac (better efficiency than MP3)
|
||||
- *Low Quality MP3*: 64kbps MP3 stream at /asteroid-low.mp3 (compatibility)
|
||||
|
||||
** Audio Processing
|
||||
- *ReplayGain*: Consistent volume without pumping artifacts
|
||||
- *Crossfading*: Smooth 5-second transitions between tracks
|
||||
- *Compression*: Dynamic compression to prevent clipping
|
||||
- *Fallback*: Emergency sine wave if all sources fail
|
||||
|
||||
** Network Configuration
|
||||
- *Icecast2*: Port 8000 (streaming and admin)
|
||||
- *Liquidsoap Telnet*: Port 1234 (remote control)
|
||||
|
|
@ -50,7 +41,7 @@ sudo usermod -a -G docker $USER
|
|||
** One-Command Setup
|
||||
#+BEGIN_SRC bash
|
||||
# Clone and start
|
||||
git clone <repository-url> asteroid-radio
|
||||
git clone https://github.com/fade/asteroid asteroid-radio
|
||||
cd asteroid-radio/docker
|
||||
docker compose up -d
|
||||
#+END_SRC
|
||||
|
|
@ -100,7 +91,6 @@ services:
|
|||
volumes:
|
||||
- ./music:/app/music:ro
|
||||
- ./asteroid-radio-docker.liq:/app/asteroid-radio.liq:ro
|
||||
- ../stream-queue.m3u:/app/stream-queue.m3u:ro # Stream queue control
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- asteroid-network
|
||||
|
|
@ -222,42 +212,20 @@ settings.server.telnet.set(true)
|
|||
settings.server.telnet.port.set(1234)
|
||||
settings.server.telnet.bind_addr.set("0.0.0.0")
|
||||
|
||||
# Create playlist source from managed stream queue
|
||||
# Create playlist source from mounted music directory
|
||||
radio = playlist(
|
||||
mode="normal", # Play in order (not randomized)
|
||||
reload=5, # Check for playlist updates every 5 seconds
|
||||
reload_mode="watch", # Watch file for changes
|
||||
"/app/stream-queue.m3u"
|
||||
)
|
||||
|
||||
# Fallback to directory scan if queue is empty
|
||||
radio_fallback = playlist.safe(
|
||||
mode="randomize",
|
||||
reload=3600,
|
||||
mode="randomize",
|
||||
reload=3600,
|
||||
reload_mode="watch",
|
||||
"/app/music/"
|
||||
)
|
||||
|
||||
radio = fallback(track_sensitive=false, [radio, radio_fallback])
|
||||
# Add some audio processing
|
||||
radio = amplify(1.0, radio)
|
||||
radio = normalize(radio)
|
||||
|
||||
# Use ReplayGain for consistent volume without pumping
|
||||
radio = amplify(1.0, override="replaygain", radio)
|
||||
|
||||
# Add smooth crossfade between tracks (5 seconds)
|
||||
radio = crossfade(
|
||||
duration=5.0,
|
||||
fade_in=3.0,
|
||||
fade_out=3.0,
|
||||
radio
|
||||
)
|
||||
|
||||
# Add compressor to prevent clipping
|
||||
radio = compress(
|
||||
ratio=3.0,
|
||||
threshold=-15.0,
|
||||
attack=50.0,
|
||||
release=400.0,
|
||||
radio
|
||||
)
|
||||
# Add crossfade between tracks
|
||||
radio = crossfade(radio)
|
||||
|
||||
# Create a fallback with emergency content
|
||||
emergency = sine(440.0)
|
||||
|
|
@ -473,36 +441,6 @@ docker compose logs --tail=10 liquidsoap
|
|||
|
||||
#+END_SRC
|
||||
|
||||
* Stream Queue Control
|
||||
|
||||
** Overview
|
||||
The Docker setup integrates with Asteroid's stream queue control system, allowing you to curate exactly what plays on the broadcast stream.
|
||||
|
||||
** How It Works
|
||||
1. Asteroid web app manages =stream-queue.m3u= file in the project root
|
||||
2. File is mounted into Liquidsoap container at =/app/stream-queue.m3u=
|
||||
3. Liquidsoap watches the file and reloads every 5 seconds
|
||||
4. When queue is empty, falls back to random playback from music directory
|
||||
|
||||
** Managing the Queue
|
||||
Use the Asteroid web API or admin interface to control the stream queue:
|
||||
|
||||
#+BEGIN_SRC bash
|
||||
# Add track to queue (requires admin authentication)
|
||||
curl -X POST http://localhost:8080/api/asteroid/stream/queue/add \
|
||||
-d "track-id=42" \
|
||||
-b cookies.txt
|
||||
|
||||
# View current queue
|
||||
curl http://localhost:8080/api/asteroid/stream/queue -b cookies.txt
|
||||
|
||||
# Clear queue (falls back to random)
|
||||
curl -X POST http://localhost:8080/api/asteroid/stream/queue/clear \
|
||||
-b cookies.txt
|
||||
#+END_SRC
|
||||
|
||||
See =docs/STREAM-CONTROL.org= for complete queue management documentation.
|
||||
|
||||
* Volume Management
|
||||
|
||||
** Music Library Setup
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
#+TITLE: Asteroid Radio - Installation Guide
|
||||
#+AUTHOR: Installation Team
|
||||
#+DATE: 2025-10-03
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Installation Overview
|
||||
|
||||
This guide covers the complete installation and deployment of Asteroid Radio. The **recommended approach** is Docker-based installation for easy deployment and consistency. Native installation is also available for development or custom deployments.
|
||||
|
||||
#+BEGIN_QUOTE
|
||||
*Note on Package Managers*: Examples in this guide use =apt= (Debian/Ubuntu). Replace with your distribution's package manager:
|
||||
- Fedora/RHEL: =dnf= or =yum=
|
||||
- Arch Linux: =pacman=
|
||||
- openSUSE: =zypper=
|
||||
- Alpine: =apk=
|
||||
#+END_QUOTE
|
||||
|
||||
* Quick Start (Docker - Recommended)
|
||||
|
||||
** Prerequisites Check
|
||||
|
|
@ -18,8 +26,8 @@ docker info
|
|||
|
||||
** One-Command Setup
|
||||
#+BEGIN_SRC bash
|
||||
# Clone and setup (replace with actual repository URL)
|
||||
git clone <repository-url> asteroid-radio
|
||||
# Clone and setup
|
||||
git clone https://github.com/fade/asteroid.git asteroid-radio
|
||||
cd asteroid-radio/docker
|
||||
docker compose up -d
|
||||
#+END_SRC
|
||||
|
|
@ -201,8 +209,8 @@ sbcl --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --eval "(ql:a
|
|||
|
||||
*** Step 5: Clone and Setup Project
|
||||
#+BEGIN_SRC bash
|
||||
# Clone repository (replace with actual URL)
|
||||
git clone <repository-url> /opt/asteroid-radio
|
||||
# Clone repository
|
||||
git clone https://github.com/fade/asteroid /opt/asteroid-radio
|
||||
cd /opt/asteroid-radio
|
||||
|
||||
# Create required directories
|
||||
|
|
@ -380,7 +388,11 @@ sudo systemctl reload nginx
|
|||
|
||||
* Docker Management
|
||||
|
||||
** Container Management
|
||||
** Stream Services
|
||||
|
||||
The stream services can be managed using docker from inside the =docker= folder on this repository.
|
||||
|
||||
*** Container Management
|
||||
#+BEGIN_SRC bash
|
||||
# Start services
|
||||
docker compose up -d
|
||||
|
|
@ -395,11 +407,42 @@ docker compose logs -f
|
|||
docker compose restart
|
||||
#+END_SRC
|
||||
|
||||
** Docker Configuration
|
||||
*** Docker Configuration
|
||||
See =docker/docker-compose.yml= for complete Docker setup with Icecast2 and Liquidsoap containers. The setup includes:
|
||||
- **Icecast2**: Streaming server with three output formats
|
||||
- **Liquidsoap**: Audio processing and stream generation
|
||||
- **Music Volume**: Mounted from =./music/= directory
|
||||
- **Music Volume**: Mounted to the =./music/library= directory (can also be set with the =MUSIC_LIBRARY= environment variable)
|
||||
- *Queue Playlist*: Mounted to the =./stream-queue.m3u= file (can also be set with the =QUEUE_PLAYLIST= environment variable)
|
||||
|
||||
** Asteroid Radio Application
|
||||
|
||||
The asteroid radio application can also be served and managed using docker from inside the =docker= folder on this repository.
|
||||
|
||||
*** Container Management
|
||||
#+BEGIN_SRC bash
|
||||
# Build service
|
||||
docker compose -f docker-compose.asteroid.yml build
|
||||
|
||||
# Start service
|
||||
docker compose -f docker-compose.asteroid.yml up -d
|
||||
|
||||
# Stop service
|
||||
docker compose -f docker-compose.asteroid.yml down
|
||||
|
||||
# View logs
|
||||
docker compose -f docker-compose.asteroid.yml logs -f
|
||||
|
||||
# Restart service
|
||||
docker compose -f docker-compose.asteroid.yml restart
|
||||
#+END_SRC
|
||||
|
||||
*** Docker Configuration
|
||||
See =docker/docker-compose.asteroid.yml= for complete Docker setup, which includes:
|
||||
- Buils the application using the current cloned branch for the repository
|
||||
- Uses the host network for easy access to the stream endpoint
|
||||
- *Stream endpoint* mapped to =http://localhost:8000= (can also be set with the =ASTEROID_STREAM_URL= environment variable)
|
||||
- **Music Volume**: Mounted to the =./music/library= directory (can also be set with the =MUSIC_LIBRARY= environment variable)
|
||||
- *Queue Playlist*: Mounted to the =./stream-queue.m3u= file (can also be set with the =QUEUE_PLAYLIST= environment variable)
|
||||
|
||||
* Initial Configuration
|
||||
|
||||
|
|
@ -537,7 +580,7 @@ chmod +x ~/asteroid-radio/health-check.sh
|
|||
- Test stream connectivity from different networks
|
||||
|
||||
** Getting Support
|
||||
- Check project documentation and FAQ
|
||||
- Check project documentation
|
||||
- Review system logs for error messages
|
||||
- Submit issues with detailed system information
|
||||
- Join our IRC chat room: **#asteroid.music** on **irc.libera.chat**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#+TITLE: Playlist System - Complete (MVP)
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-04
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Overview
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#+TITLE: PostgreSQL Setup for Asteroid Radio
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-04
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* 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
|
||||
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade)
|
||||
#+DATE: 2025-10-16
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* 🎯 Mission
|
||||
|
||||
|
|
@ -38,7 +38,8 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack
|
|||
- **Common Lisp** (SBCL) - Core application language
|
||||
- **Radiance Framework** - Web framework and module system
|
||||
- **LASS** - CSS preprocessing in Lisp
|
||||
- **PostgreSQL** - Database backend for user accounts and metadata
|
||||
- **PostgreSQL** - Database backend (configured, ready for migration)
|
||||
- **Radiance DB** - Current database abstraction layer
|
||||
|
||||
**Frontend:**
|
||||
- **HTML5** with semantic templates
|
||||
|
|
@ -75,28 +76,32 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack
|
|||
## 🚀 Features
|
||||
|
||||
### Current Features
|
||||
- ✅ **User Authentication** - Registration, login, profiles, role-based access
|
||||
- ✅ **User Authentication** - Registration, login, profiles, role-based access (Admin/DJ/Listener)
|
||||
- ✅ **User Management** - Admin interface for user administration
|
||||
- ✅ **Music Library** - Track management with pagination and search
|
||||
- ✅ **Music Library** - Track management with pagination, search, and filtering
|
||||
- ✅ **User Playlists** - Create, manage, and play personal music collections
|
||||
- ✅ **Stream Queue Control** - Curated broadcast queue with admin controls
|
||||
- ✅ **Web Player** - Browser-based player with queue management
|
||||
- ✅ **REST API** - Comprehensive JSON API with 20+ endpoints
|
||||
- ✅ **Multi-Format Streaming** - AAC 96kbps, MP3 128kbps/64kbps
|
||||
- ✅ **ReplayGain Processing** - Consistent volume without pumping
|
||||
- ✅ **Multiple Player Modes** - Inline, pop-out, and persistent frameset players
|
||||
- ✅ **Stream Queue Control** - Admin control over broadcast stream queue (M3U-based)
|
||||
- ✅ **REST API** - Comprehensive JSON API with 15+ endpoints
|
||||
- ✅ **Music Streaming** - Multiple quality formats (128k MP3, 96k AAC, 64k MP3)
|
||||
- ✅ **Rate Limiting** - Anti-abuse protection
|
||||
- ✅ **Docker Integration** - Icecast2/Liquidsoap streaming infrastructure
|
||||
- ✅ **PostgreSQL Database** - Persistent data storage
|
||||
- ✅ **PostgreSQL Database** - Configured and ready for migration
|
||||
- ✅ **Liquidsoap DJ Controls** - Telnet interface for live control
|
||||
- ✅ **Dynamic Stream URLs** - Automatic host detection for multi-environment support
|
||||
- ✅ **ReplayGain Normalization** - Consistent audio volume across tracks
|
||||
- ✅ **Responsive Design** - Works on desktop and mobile
|
||||
- ✅ **Automated Testing** - Comprehensive test suite
|
||||
|
||||
### Planned Features
|
||||
- 🔄 **PostgreSQL Migration** - Full migration from Radiance DB to PostgreSQL
|
||||
- 🔄 **Enhanced Playlist Management** - Full CRUD operations with PostgreSQL
|
||||
- 🔄 **Social Features** - Playlist sharing and discovery
|
||||
- 🔄 **Advanced Search** - Full-text search and filtering
|
||||
- 🔄 **Mobile App** - Native mobile applications
|
||||
- 🔄 **WebSocket Support** - Real-time updates
|
||||
- 🔄 **Analytics** - Listening statistics and insights
|
||||
- 🔄 **Scheduled Programming** - Time-based queue switching
|
||||
|
||||
|
||||
## 🔮 Vision
|
||||
|
|
@ -111,10 +116,9 @@ Asteroid Radio is the premier streaming platform for **Asteroid Music** - the pe
|
|||
|
||||
**Platform Features:**
|
||||
- **Multi-Format Streaming** - High-quality AAC, MP3 128k, and MP3 64k streams
|
||||
- **Stream Queue Control** - Curate exactly what plays on the broadcast
|
||||
- **User Community** - Accounts, playlists, and sharing among fellow developers
|
||||
- **Developer-Friendly** - Built with Common Lisp, fully hackable and extensible
|
||||
- **Professional Quality** - ReplayGain, crossfading, compression, metadata, and telnet control
|
||||
- **Professional Quality** - Crossfading, normalization, metadata, and telnet control
|
||||
- **Always-On Broadcasting** - Continuous streams perfect for long coding sessions
|
||||
|
||||
Asteroid Radio isn't just another music platform - it's the soundtrack to the hacker lifestyle, designed by hackers for hackers who understand that the right music can make the difference between good code and great code.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#+TITLE: Asteroid Radio - Documentation Index
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-10
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Welcome to Asteroid Radio Documentation
|
||||
|
||||
|
|
@ -19,6 +19,9 @@ For immediate setup, see:
|
|||
*** [[file:PROJECT-OVERVIEW.org][Project Overview]]
|
||||
Complete overview of Asteroid Radio's architecture, technology stack, and vision. Start here to understand what Asteroid Radio is and how it works.
|
||||
|
||||
*** [[file:PROJECT-HISTORY.org][Project History]]
|
||||
Comprehensive development history from inception to present, including timeline, milestones, and contributor information.
|
||||
|
||||
*** [[file:INSTALLATION.org][Installation Guide]]
|
||||
Comprehensive installation instructions for multiple operating systems, including system requirements, dependencies, and production deployment considerations.
|
||||
|
||||
|
|
@ -58,16 +61,19 @@ Pagination system for efficient browsing of large music libraries.
|
|||
** What's Working Now
|
||||
- **Web Application**: Full-featured web interface with authentication
|
||||
- **REST API**: JSON API with 15+ endpoints for programmatic access
|
||||
- **User Management**: Registration, login, roles, and profiles
|
||||
- **Music Library**: Track management with pagination and search
|
||||
- **User Management**: Registration, login, roles (Admin/DJ/Listener), and profiles
|
||||
- **Music Library**: Track management with pagination, search, and filtering
|
||||
- **Playlists**: User playlists with creation and playback
|
||||
- **Web Player**: Browser-based audio player with queue management
|
||||
- **Multiple Player Modes**: Inline, pop-out, and persistent frameset players
|
||||
- **Stream Queue Control**: Admin control over broadcast stream queue
|
||||
- **Docker Streaming Infrastructure**: Icecast2 + Liquidsoap containers
|
||||
- **Three Quality Streams**: 128kbps MP3, 96kbps AAC, 64kbps MP3
|
||||
- **Admin Interface**: Icecast web admin at http://localhost:8000/admin/
|
||||
- **Liquidsoap DJ Controls**: Telnet control via localhost:1234
|
||||
- **Professional Features**: Crossfading, normalization, metadata support
|
||||
- **PostgreSQL Database**: Persistent data storage with full CRUD operations
|
||||
- **Professional Features**: Crossfading, ReplayGain normalization, metadata support
|
||||
- **PostgreSQL Database**: Configured and ready for migration
|
||||
- **Dynamic Stream URLs**: Automatic host detection for multi-environment support
|
||||
- **Responsive Design**: Works on desktop and mobile devices
|
||||
|
||||
** Stream URLs (when running)
|
||||
- **High Quality MP3**: http://localhost:8000/asteroid.mp3 (128kbps)
|
||||
|
|
@ -125,15 +131,15 @@ Asteroid Radio uses a modern, containerized architecture:
|
|||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Asteroid Radio Platform │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Streaming Infrastructure (Docker) │
|
||||
│ ├── Icecast2 (HTTP Streaming Server) │
|
||||
│ ├── Liquidsoap (Audio Processing Pipeline) │
|
||||
│ └── Multiple Format Support (AAC, MP3) │
|
||||
│ Streaming Infrastructure (Docker) │
|
||||
│ ├── Icecast2 (HTTP Streaming Server) │
|
||||
│ ├── Liquidsoap (Audio Processing Pipeline) │
|
||||
│ └── Multiple Format Support (AAC, MP3) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Control Interfaces │
|
||||
│ ├── Icecast Admin Web Interface │
|
||||
│ ├── Liquidsoap Telnet Control │
|
||||
│ └── Docker Container Management │
|
||||
│ Control Interfaces │
|
||||
│ ├── Icecast Admin Web Interface │
|
||||
│ ├── Liquidsoap Telnet Control │
|
||||
│ └── Docker Container Management │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
#+END_EXAMPLE
|
||||
|
||||
|
|
@ -141,5 +147,5 @@ For detailed technical information, see the **[[file:PROJECT-OVERVIEW.org][Proje
|
|||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-10-10*
|
||||
*Documentation Version: 2.0*
|
||||
*Last Updated: 2025-10-26*
|
||||
*Documentation Version: 3.0*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#+TITLE: Stream Queue Control System
|
||||
#+AUTHOR: Asteroid Radio Team
|
||||
#+DATE: 2025-10-14
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Overview
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ If you're working directly in the Lisp REPL:
|
|||
|
||||
* File Locations
|
||||
|
||||
- *Stream Queue File*: =/home/glenn/Projects/Code/asteroid/stream-queue.m3u=
|
||||
- *Stream Queue File*: =stream-queue.m3u= (in project root)
|
||||
- *Docker Mount*: =/app/stream-queue.m3u= (inside Liquidsoap container)
|
||||
- *Liquidsoap Config*: =docker/asteroid-radio-docker.liq=
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
#+TITLE: Asteroid Radio Testing Guide
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-08
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Overview
|
||||
|
||||
This document describes the automated testing system for Asteroid Radio.
|
||||
|
||||
#+BEGIN_QUOTE
|
||||
*Note on Package Managers*: Examples use =apt= (Debian/Ubuntu). Replace with your distribution's package manager (=dnf=, =pacman=, =zypper=, =apk=, etc.).
|
||||
#+END_QUOTE
|
||||
|
||||
* Test Script
|
||||
|
||||
The =test-server.sh= script provides comprehensive testing of all server endpoints and functionality.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#+TITLE: Track Pagination System - Complete
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-04
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Overview
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#+TITLE: User Management System - Complete
|
||||
#+AUTHOR: Asteroid Radio Development Team
|
||||
#+DATE: 2025-10-04
|
||||
#+DATE: 2025-10-26
|
||||
|
||||
* Overview
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
(in-package :asteroid)
|
||||
|
||||
(defun icecast-now-playing (icecast-base-url)
|
||||
(let* ((icecast-url (concatenate 'string icecast-base-url "/admin/stats.xml"))
|
||||
(response (drakma:http-request icecast-url
|
||||
:want-stream nil
|
||||
:basic-authorization '("admin" "asteroid_admin_2024"))))
|
||||
(when response
|
||||
(let ((xml-string (if (stringp response)
|
||||
response
|
||||
(babel:octets-to-string response :encoding :utf-8))))
|
||||
;; Simple XML parsing to extract source information
|
||||
;; Look for <source mount="/asteroid.mp3"> sections and extract title, listeners, etc.
|
||||
(multiple-value-bind (match-start match-end)
|
||||
(cl-ppcre:scan "<source mount=\"/asteroid\\.mp3\">" xml-string)
|
||||
|
||||
(if match-start
|
||||
(let* ((source-section (subseq xml-string match-start
|
||||
(or (cl-ppcre:scan "</source>" xml-string :start match-start)
|
||||
(length xml-string))))
|
||||
(titlep (cl-ppcre:all-matches "<title>" source-section))
|
||||
(listenersp (cl-ppcre:all-matches "<listeners>" source-section))
|
||||
(title (if titlep (cl-ppcre:regex-replace-all ".*<title>(.*?)</title>.*" source-section "\\1") "Unknown"))
|
||||
(listeners (if listenersp (cl-ppcre:regex-replace-all ".*<listeners>(.*?)</listeners>.*" source-section "\\1") "0")))
|
||||
`((:listenurl . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
|
||||
(:title . ,title)
|
||||
(:listeners . ,(parse-integer listeners :junk-allowed t))))
|
||||
`((:listenurl . ,(concatenate 'string *stream-base-url* "/asteroid.mp3"))
|
||||
(:title . "Unknown")
|
||||
(:listeners . "Unknown"))))))))
|
||||
|
||||
(define-api asteroid/partial/now-playing () ()
|
||||
"Get Partial HTML with live status from Icecast server"
|
||||
(handler-case
|
||||
(let ((now-playing-stats (icecast-now-playing *stream-base-url*))
|
||||
(template-path (merge-pathnames "template/partial/now-playing.chtml"
|
||||
(asdf:system-source-directory :asteroid))))
|
||||
(if now-playing-stats
|
||||
(progn
|
||||
;; TODO: it should be able to define a custom api-output for this
|
||||
;; (api-output <clip-parser> :format "html"))
|
||||
(setf (header "Content-Type") "text/html")
|
||||
(clip:process-to-string
|
||||
(plump:parse (alexandria:read-file-into-string template-path))
|
||||
:stats now-playing-stats))
|
||||
(progn
|
||||
(setf (header "Content-Type") "text/html")
|
||||
(clip:process-to-string
|
||||
(plump:parse (alexandria:read-file-into-string template-path))
|
||||
:connection-error t
|
||||
:stats nil))))
|
||||
(error (e)
|
||||
(api-output `(("status" . "error")
|
||||
("message" . ,(format nil "Error loading profile: ~a" e)))
|
||||
:status 500))))
|
||||
|
||||
(define-api asteroid/partial/now-playing-inline () ()
|
||||
"Get inline text with now playing info (for admin dashboard and widgets)"
|
||||
(handler-case
|
||||
(let ((now-playing-stats (icecast-now-playing *stream-base-url*)))
|
||||
(if now-playing-stats
|
||||
(progn
|
||||
(setf (header "Content-Type") "text/plain")
|
||||
(cdr (assoc :title now-playing-stats)))
|
||||
(progn
|
||||
(setf (header "Content-Type") "text/plain")
|
||||
"Stream Offline")))
|
||||
(error (e)
|
||||
(setf (header "Content-Type") "text/plain")
|
||||
"Error loading stream info")))
|
||||
|
|
@ -28,11 +28,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
// Queue controls
|
||||
const refreshQueueBtn = document.getElementById('refresh-queue');
|
||||
const loadFromM3uBtn = document.getElementById('load-from-m3u');
|
||||
const clearQueueBtn = document.getElementById('clear-queue-btn');
|
||||
const addRandomBtn = document.getElementById('add-random-tracks');
|
||||
const queueSearchInput = document.getElementById('queue-track-search');
|
||||
|
||||
if (refreshQueueBtn) refreshQueueBtn.addEventListener('click', loadStreamQueue);
|
||||
if (loadFromM3uBtn) loadFromM3uBtn.addEventListener('click', loadQueueFromM3U);
|
||||
if (clearQueueBtn) clearQueueBtn.addEventListener('click', clearStreamQueue);
|
||||
if (addRandomBtn) addRandomBtn.addEventListener('click', addRandomTracks);
|
||||
if (queueSearchInput) queueSearchInput.addEventListener('input', searchTracksForQueue);
|
||||
|
|
@ -103,8 +105,6 @@ function renderPage() {
|
|||
<div class="track-album">${track.album || 'Unknown Album'}</div>
|
||||
</div>
|
||||
<div class="track-actions">
|
||||
<button onclick="playTrack(${track.id})" class="btn btn-sm btn-success">▶️ Play</button>
|
||||
<button onclick="streamTrack(${track.id})" class="btn btn-sm btn-info">🎵 Stream</button>
|
||||
<button onclick="addToQueue(${track.id}, 'end')" class="btn btn-sm btn-primary">➕ Add to Queue</button>
|
||||
<button onclick="deleteTrack(${track.id})" class="btn btn-sm btn-danger">🗑️ Delete</button>
|
||||
</div>
|
||||
|
|
@ -185,6 +185,7 @@ async function scanLibrary() {
|
|||
|
||||
// Filter tracks based on search
|
||||
function filterTracks() {
|
||||
const query = document.getElementById('track-search').value.toLowerCase();
|
||||
const filtered = tracks.filter(track =>
|
||||
(track.title || '').toLowerCase().includes(query) ||
|
||||
(track.artist || '').toLowerCase().includes(query) ||
|
||||
|
|
@ -367,14 +368,20 @@ function displayStreamQueue() {
|
|||
let html = '<div class="queue-items">';
|
||||
streamQueue.forEach((item, index) => {
|
||||
if (item) {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === streamQueue.length - 1;
|
||||
html += `
|
||||
<div class="queue-item" data-track-id="${item.id}">
|
||||
<div class="queue-item" data-track-id="${item.id}" data-index="${index}">
|
||||
<span class="queue-position">${index + 1}</span>
|
||||
<div class="queue-track-info">
|
||||
<div class="track-title">${item.title || 'Unknown'}</div>
|
||||
<div class="track-artist">${item.artist || 'Unknown Artist'}</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" onclick="removeFromQueue(${item.id})">Remove</button>
|
||||
<div class="queue-actions">
|
||||
<button class="btn btn-sm btn-secondary" onclick="moveTrackUp(${index})" ${isFirst ? 'disabled' : ''}>⬆️</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="moveTrackDown(${index})" ${isLast ? 'disabled' : ''}>⬇️</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="removeFromQueue(${item.id})">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -409,6 +416,74 @@ async function clearStreamQueue() {
|
|||
}
|
||||
}
|
||||
|
||||
// Load queue from M3U file
|
||||
async function loadQueueFromM3U() {
|
||||
if (!confirm('Load queue from stream-queue.m3u file? This will replace the current queue.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/stream/queue/load-m3u', {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
const data = result.data || result;
|
||||
|
||||
if (data.status === 'success') {
|
||||
alert(`Successfully loaded ${data.count} tracks from M3U file!`);
|
||||
loadStreamQueue();
|
||||
} else {
|
||||
alert('Error loading from M3U: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading from M3U:', error);
|
||||
alert('Error loading from M3U: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Move track up in queue
|
||||
async function moveTrackUp(index) {
|
||||
if (index === 0) return;
|
||||
|
||||
// Swap with previous track
|
||||
const newQueue = [...streamQueue];
|
||||
[newQueue[index - 1], newQueue[index]] = [newQueue[index], newQueue[index - 1]];
|
||||
|
||||
await reorderQueue(newQueue);
|
||||
}
|
||||
|
||||
// Move track down in queue
|
||||
async function moveTrackDown(index) {
|
||||
if (index === streamQueue.length - 1) return;
|
||||
|
||||
// Swap with next track
|
||||
const newQueue = [...streamQueue];
|
||||
[newQueue[index], newQueue[index + 1]] = [newQueue[index + 1], newQueue[index]];
|
||||
|
||||
await reorderQueue(newQueue);
|
||||
}
|
||||
|
||||
// Reorder the queue
|
||||
async function reorderQueue(newQueue) {
|
||||
try {
|
||||
const trackIds = newQueue.map(track => track.id).join(',');
|
||||
const response = await fetch('/api/asteroid/stream/queue/reorder?track-ids=' + trackIds, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
const data = result.data || result;
|
||||
|
||||
if (data.status === 'success') {
|
||||
loadStreamQueue();
|
||||
} else {
|
||||
alert('Error reordering queue: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reordering queue:', error);
|
||||
alert('Error reordering queue');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove track from queue
|
||||
async function removeFromQueue(trackId) {
|
||||
try {
|
||||
|
|
@ -561,35 +636,25 @@ function displayQueueSearchResults(results) {
|
|||
// Live stream info update
|
||||
async function updateLiveStreamInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/icecast-status');
|
||||
if (!response.ok) {
|
||||
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
||||
const contentType = response.headers.get("content-type");
|
||||
|
||||
if (!contentType.includes('text/plain')) {
|
||||
console.error('Unexpected content type:', contentType);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const nowPlayingText = await response.text();
|
||||
const nowPlayingEl = document.getElementById('live-now-playing');
|
||||
|
||||
// Handle Radiance API response format
|
||||
const data = result.data || result;
|
||||
|
||||
// Sources are nested in icestats
|
||||
const sources = data.icestats?.source;
|
||||
|
||||
if (sources) {
|
||||
const mainStream = Array.isArray(sources)
|
||||
? sources.find(s => s.listenurl?.includes('/asteroid.aac') || s.listenurl?.includes('/asteroid.mp3'))
|
||||
: sources;
|
||||
|
||||
if (mainStream && mainStream.title) {
|
||||
const nowPlayingEl = document.getElementById('live-now-playing');
|
||||
if (nowPlayingEl) {
|
||||
const parts = mainStream.title.split(' - ');
|
||||
const artist = parts[0] || 'Unknown';
|
||||
const track = parts.slice(1).join(' - ') || 'Unknown';
|
||||
nowPlayingEl.textContent = `${artist} - ${track}`;
|
||||
}
|
||||
}
|
||||
if (nowPlayingEl) {
|
||||
nowPlayingEl.textContent = nowPlayingText;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not fetch stream info:', error);
|
||||
const nowPlayingEl = document.getElementById('live-now-playing');
|
||||
if (nowPlayingEl) {
|
||||
nowPlayingEl.textContent = 'Error loading stream info';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,33 +60,15 @@ function changeStreamQuality() {
|
|||
// Update now playing info from Icecast
|
||||
async function updateNowPlaying() {
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/icecast-status')
|
||||
const data = await response.json()
|
||||
// Handle RADIANCE API wrapper format
|
||||
const icecastData = data.data || data;
|
||||
if (icecastData.icestats && icecastData.icestats.source) {
|
||||
// Find the high quality stream (asteroid.mp3)
|
||||
const sources = Array.isArray(icecastData.icestats.source) ? icecastData.icestats.source : [icecastData.icestats.source];
|
||||
const mainStream = sources.find(s => s.listenurl && s.listenurl.includes('asteroid.mp3'));
|
||||
|
||||
if (mainStream && mainStream.title) {
|
||||
// Parse "Artist - Track" format
|
||||
const titleParts = mainStream.title.split(' - ');
|
||||
const artist = titleParts.length > 1 ? titleParts[0] : 'Unknown Artist';
|
||||
const track = titleParts.length > 1 ? titleParts.slice(1).join(' - ') : mainStream.title;
|
||||
|
||||
document.querySelector('[data-text="now-playing-artist"]').textContent = artist;
|
||||
document.querySelector('[data-text="now-playing-track"]').textContent = track;
|
||||
document.querySelector('[data-text="listeners"]').textContent = mainStream.listeners || '0';
|
||||
|
||||
// Update stream status
|
||||
const statusElement = document.querySelector('.live-stream p:nth-child(3) span');
|
||||
if (statusElement) {
|
||||
statusElement.textContent = '● LIVE - ' + track;
|
||||
statusElement.style.color = '#00ff00';
|
||||
}
|
||||
}
|
||||
const response = await fetch('/api/asteroid/partial/now-playing')
|
||||
const contentType = response.headers.get("content-type")
|
||||
if (!contentType.includes('text/html')) {
|
||||
throw new Error('Error connecting to stream')
|
||||
}
|
||||
|
||||
const data = await response.text()
|
||||
document.getElementById('now-playing').innerHTML = data
|
||||
|
||||
} catch(error) {
|
||||
console.log('Could not fetch stream status:', error);
|
||||
}
|
||||
|
|
@ -107,7 +89,110 @@ window.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
// Update playing information right after load
|
||||
updateNowPlaying();
|
||||
|
||||
// Auto-reconnect on stream errors
|
||||
const audioElement = document.getElementById('live-audio');
|
||||
if (audioElement) {
|
||||
audioElement.addEventListener('error', function(e) {
|
||||
console.log('Stream error, attempting reconnect in 3 seconds...');
|
||||
setTimeout(function() {
|
||||
audioElement.load();
|
||||
audioElement.play().catch(err => console.log('Reconnect failed:', err));
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
audioElement.addEventListener('stalled', function() {
|
||||
console.log('Stream stalled, reloading...');
|
||||
audioElement.load();
|
||||
audioElement.play().catch(err => console.log('Reload failed:', err));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update every 10 seconds
|
||||
setInterval(updateNowPlaying, 10000);
|
||||
|
||||
// Pop-out player functionality
|
||||
let popoutWindow = null;
|
||||
|
||||
function openPopoutPlayer() {
|
||||
// Check if popout is already open
|
||||
if (popoutWindow && !popoutWindow.closed) {
|
||||
popoutWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate centered position
|
||||
const width = 420;
|
||||
const height = 300;
|
||||
const left = (screen.width - width) / 2;
|
||||
const top = (screen.height - height) / 2;
|
||||
|
||||
// Open popout window
|
||||
const features = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no,status=no,menubar=no,toolbar=no,location=no`;
|
||||
|
||||
popoutWindow = window.open('/asteroid/popout-player', 'AsteroidPlayer', features);
|
||||
|
||||
// Update button state
|
||||
updatePopoutButton(true);
|
||||
}
|
||||
|
||||
function updatePopoutButton(isOpen) {
|
||||
const btn = document.getElementById('popout-btn');
|
||||
if (btn) {
|
||||
if (isOpen) {
|
||||
btn.textContent = '✓ Player Open';
|
||||
btn.classList.remove('btn-info');
|
||||
btn.classList.add('btn-success');
|
||||
} else {
|
||||
btn.textContent = '🗗 Pop Out Player';
|
||||
btn.classList.remove('btn-success');
|
||||
btn.classList.add('btn-info');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from popout window
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data.type === 'popout-opened') {
|
||||
updatePopoutButton(true);
|
||||
} else if (event.data.type === 'popout-closed') {
|
||||
updatePopoutButton(false);
|
||||
popoutWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if popout is still open periodically
|
||||
setInterval(function() {
|
||||
if (popoutWindow && popoutWindow.closed) {
|
||||
updatePopoutButton(false);
|
||||
popoutWindow = null;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Frameset mode functionality
|
||||
function enableFramesetMode() {
|
||||
// Save preference
|
||||
localStorage.setItem('useFrameset', 'true');
|
||||
// Redirect to frameset wrapper
|
||||
window.location.href = '/asteroid/frameset';
|
||||
}
|
||||
|
||||
function disableFramesetMode() {
|
||||
// Clear preference
|
||||
localStorage.removeItem('useFrameset');
|
||||
// Redirect to regular view
|
||||
window.location.href = '/asteroid/';
|
||||
}
|
||||
|
||||
// Check if user prefers frameset mode on page load
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const path = window.location.pathname;
|
||||
const isFramesetPage = path.includes('/frameset') || path.includes('/content') ||
|
||||
path.includes('/audio-player-frame') || path.includes('/player-content');
|
||||
|
||||
if (localStorage.getItem('useFrameset') === 'true' && !isFramesetPage && path === '/asteroid/') {
|
||||
// User wants frameset but is on regular front page, redirect
|
||||
window.location.href = '/asteroid/frameset';
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -566,41 +566,24 @@ function changeLiveStreamQuality() {
|
|||
}
|
||||
}
|
||||
|
||||
// Live stream functionality
|
||||
async function updateLiveStream() {
|
||||
// Live stream informatio update
|
||||
async function updateNowPlaying() {
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/icecast-status')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
const response = await fetch('/api/asteroid/partial/now-playing')
|
||||
const contentType = response.headers.get("content-type")
|
||||
if (!contentType.includes('text/html')) {
|
||||
throw new Error('Error connecting to stream')
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
// Handle RADIANCE API wrapper format
|
||||
const data = result.data || result;
|
||||
if (data.icestats && data.icestats.source) {
|
||||
const sources = Array.isArray(data.icestats.source) ? data.icestats.source : [data.icestats.source];
|
||||
const mainStream = sources.find(s => s.listenurl && s.listenurl.includes('asteroid.mp3'));
|
||||
|
||||
if (mainStream && mainStream.title) {
|
||||
const titleParts = mainStream.title.split(' - ');
|
||||
const artist = titleParts.length > 1 ? titleParts[0] : 'Unknown Artist';
|
||||
const track = titleParts.length > 1 ? titleParts.slice(1).join(' - ') : mainStream.title;
|
||||
const data = await response.text()
|
||||
document.getElementById('now-playing').innerHTML = data
|
||||
|
||||
const nowPlayingEl = document.getElementById('live-now-playing');
|
||||
const listenersEl = document.getElementById('live-listeners');
|
||||
|
||||
if (nowPlayingEl) nowPlayingEl.textContent = `${artist} - ${track}`;
|
||||
if (listenersEl) listenersEl.textContent = mainStream.listeners || '0';
|
||||
} else {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Live stream update error:', error);
|
||||
const nowPlayingEl = document.getElementById('live-now-playing');
|
||||
if (nowPlayingEl) nowPlayingEl.textContent = 'Stream unavailable';
|
||||
} catch(error) {
|
||||
console.log('Could not fetch stream status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial update after 1 second
|
||||
setTimeout(updateNowPlaying, 1000);
|
||||
// Update live stream info every 10 seconds
|
||||
setTimeout(updateLiveStream, 1000); // Initial update after 1 second
|
||||
setInterval(updateLiveStream, 10000);
|
||||
setInterval(updateNowPlaying, 10000);
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@
|
|||
|
||||
(defun convert-to-docker-path (host-path)
|
||||
"Convert host file path to Docker container path"
|
||||
;; Replace /home/glenn/Projects/Code/asteroid/music/library/ with /app/music/
|
||||
(let ((library-prefix "/home/glenn/Projects/Code/asteroid/music/library/"))
|
||||
;; Replace the music library path with /app/music/
|
||||
(let ((library-prefix (namestring *music-library-path*)))
|
||||
(if (and (stringp host-path)
|
||||
(>= (length host-path) (length library-prefix))
|
||||
(string= host-path library-prefix :end1 (length library-prefix)))
|
||||
|
|
@ -177,3 +177,46 @@
|
|||
(setf *stream-queue* (subseq track-ids 0 (min count (length track-ids))))
|
||||
(regenerate-stream-playlist)
|
||||
*stream-queue*))))
|
||||
|
||||
(defun convert-from-docker-path (docker-path)
|
||||
"Convert Docker container path back to host file path"
|
||||
(if (and (stringp docker-path)
|
||||
(>= (length docker-path) 11)
|
||||
(string= docker-path "/app/music/" :end1 11))
|
||||
(concatenate 'string
|
||||
(namestring *music-library-path*)
|
||||
(subseq docker-path 11))
|
||||
docker-path))
|
||||
|
||||
(defun load-queue-from-m3u-file ()
|
||||
"Load the stream queue from the stream-queue.m3u file"
|
||||
(let* ((m3u-path (merge-pathnames "stream-queue.m3u"
|
||||
(asdf:system-source-directory :asteroid)))
|
||||
(track-ids '())
|
||||
(all-tracks (db:select "tracks" (db:query :all))))
|
||||
|
||||
(when (probe-file m3u-path)
|
||||
(with-open-file (stream m3u-path :direction :input)
|
||||
(loop for line = (read-line stream nil)
|
||||
while line
|
||||
do (unless (or (string= line "")
|
||||
(char= (char line 0) #\#))
|
||||
;; This is a file path line
|
||||
(let* ((docker-path (string-trim '(#\Space #\Tab #\Return #\Newline) line))
|
||||
(host-path (convert-from-docker-path docker-path)))
|
||||
;; Find track by file path
|
||||
(let ((track (find-if
|
||||
(lambda (trk)
|
||||
(let ((fp (gethash "file-path" trk)))
|
||||
(let ((file-path (if (listp fp) (first fp) fp)))
|
||||
(string= file-path host-path))))
|
||||
all-tracks)))
|
||||
(when track
|
||||
(let ((id (gethash "_id" track)))
|
||||
(push (if (listp id) (first id) id) track-ids)))))))))
|
||||
|
||||
;; Reverse to maintain order from file
|
||||
(setf track-ids (nreverse track-ids))
|
||||
(setf *stream-queue* track-ids)
|
||||
(regenerate-stream-playlist)
|
||||
(length track-ids)))
|
||||
|
|
|
|||
|
|
@ -1,19 +1 @@
|
|||
#EXTM3U
|
||||
#EXTINF:0,
|
||||
/app/music/Model500/2015 - Digital Solutions/02-model_500-electric_night.flac
|
||||
#EXTINF:0,
|
||||
/app/music/Kraftwerk/1978 - The Man-Machine \[2009 Digital Remaster]/02 - Spacelab.flac
|
||||
#EXTINF:0,
|
||||
/app/music/Kraftwerk/1981 - Computer World \[2009 Digital Remaster]/03 - Numbers.flac
|
||||
#EXTINF:0,
|
||||
/app/music/Model500/2015 - Digital Solutions/08-model_500-digital_solutions.flac
|
||||
#EXTINF:0,
|
||||
/app/music/Model500/2015 - Digital Solutions/02-model_500-electric_night.flac
|
||||
#EXTINF:0,
|
||||
/app/music/This Mortal Coil/1984 - It'll End In Tears/09 - Barramundi.flac
|
||||
#EXTINF:0,
|
||||
/app/music/Underworld/1996 - Second Toughest In The Infants/04. Underworld - Rowla.flac
|
||||
#EXTINF:0,
|
||||
/app/music/This Mortal Coil/1984 - It'll End In Tears/10 - Dreams Made Flesh.flac
|
||||
#EXTINF:0,
|
||||
/app/music/Underworld/1996 - Second Toughest In The Infants/07. Underworld - Blueski.flac
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@
|
|||
<div class="container">
|
||||
<h1>🎛️ ADMIN DASHBOARD</h1>
|
||||
<div class="nav">
|
||||
<a href="/asteroid/">Home</a>
|
||||
<a href="/asteroid/player/">Player</a>
|
||||
<a href="/asteroid/profile">Profile</a>
|
||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/player-content" target="content-frame">Player</a>
|
||||
<a href="/asteroid/profile" target="content-frame">Profile</a>
|
||||
<a href="/asteroid/admin/users">👥 Users</a>
|
||||
<a href="/asteroid/logout" class="btn-logout">Logout</a>
|
||||
</div>
|
||||
|
|
@ -127,6 +127,7 @@
|
|||
|
||||
<div class="queue-controls">
|
||||
<button id="refresh-queue" class="btn btn-secondary">🔄 Refresh Queue</button>
|
||||
<button id="load-from-m3u" class="btn btn-success">📂 Load Queue from M3U</button>
|
||||
<button id="clear-queue-btn" class="btn btn-warning">🗑️ Clear Queue</button>
|
||||
<button id="add-random-tracks" class="btn btn-info">🎲 Add 10 Random Tracks</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background: #1a1a1a;
|
||||
font-family: 'VT323', monospace;
|
||||
}
|
||||
.persistent-player {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.player-label {
|
||||
color: #00ff00;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.quality-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.quality-selector label {
|
||||
color: #00ff00;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.quality-selector select {
|
||||
background: #2a2a2a;
|
||||
color: #00ff00;
|
||||
border: 1px solid #00ff00;
|
||||
padding: 3px 8px;
|
||||
font-family: 'VT323', monospace;
|
||||
}
|
||||
audio {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.now-playing-mini {
|
||||
color: #00ff00;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="persistent-player">
|
||||
<span class="player-label">🟢 LIVE:</span>
|
||||
|
||||
<div class="quality-selector">
|
||||
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
|
||||
<label for="stream-quality">Quality:</label>
|
||||
<select id="stream-quality" onchange="changeStreamQuality()">
|
||||
<option value="aac">AAC 96k</option>
|
||||
<option value="mp3">MP3 128k</option>
|
||||
<option value="low">MP3 64k</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<audio id="persistent-audio" controls preload="metadata">
|
||||
<source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)">
|
||||
</audio>
|
||||
|
||||
<span class="now-playing-mini" id="mini-now-playing">Loading...</span>
|
||||
|
||||
<button onclick="disableFramesetMode()" style="background: #2a2a2a; color: #00ff00; border: 1px solid #00ff00; padding: 5px 10px; cursor: pointer; font-family: 'VT323', monospace; font-size: 0.85em; white-space: nowrap;">
|
||||
✕ Disable
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Configure audio element for better streaming
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const audioElement = document.getElementById('persistent-audio');
|
||||
|
||||
// Try to enable low-latency mode if supported
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: 'Asteroid Radio Live Stream',
|
||||
artist: 'Asteroid Radio',
|
||||
album: 'Live Broadcast'
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listeners for debugging
|
||||
audioElement.addEventListener('waiting', function() {
|
||||
console.log('Audio buffering...');
|
||||
});
|
||||
|
||||
audioElement.addEventListener('playing', function() {
|
||||
console.log('Audio playing');
|
||||
});
|
||||
|
||||
audioElement.addEventListener('error', function(e) {
|
||||
console.error('Audio error:', e);
|
||||
});
|
||||
});
|
||||
|
||||
// Stream quality configuration
|
||||
function getStreamConfig(streamBaseUrl, encoding) {
|
||||
const config = {
|
||||
aac: {
|
||||
url: streamBaseUrl + '/asteroid.aac',
|
||||
type: 'audio/aac'
|
||||
},
|
||||
mp3: {
|
||||
url: streamBaseUrl + '/asteroid.mp3',
|
||||
type: 'audio/mpeg'
|
||||
},
|
||||
low: {
|
||||
url: streamBaseUrl + '/asteroid-low.mp3',
|
||||
type: 'audio/mpeg'
|
||||
}
|
||||
};
|
||||
return config[encoding];
|
||||
}
|
||||
|
||||
// Change stream quality
|
||||
function changeStreamQuality() {
|
||||
const selector = document.getElementById('stream-quality');
|
||||
const streamBaseUrl = document.getElementById('stream-base-url').value;
|
||||
const config = getStreamConfig(streamBaseUrl, selector.value);
|
||||
|
||||
const audioElement = document.getElementById('persistent-audio');
|
||||
const sourceElement = document.getElementById('audio-source');
|
||||
|
||||
const wasPlaying = !audioElement.paused;
|
||||
|
||||
sourceElement.src = config.url;
|
||||
sourceElement.type = config.type;
|
||||
audioElement.load();
|
||||
|
||||
if (wasPlaying) {
|
||||
audioElement.play().catch(e => console.log('Autoplay prevented:', e));
|
||||
}
|
||||
}
|
||||
|
||||
// Update mini now playing display
|
||||
async function updateMiniNowPlaying() {
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
||||
if (response.ok) {
|
||||
const text = await response.text();
|
||||
document.getElementById('mini-now-playing').textContent = text;
|
||||
}
|
||||
} catch(error) {
|
||||
console.log('Could not fetch now playing:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update every 10 seconds
|
||||
setTimeout(updateMiniNowPlaying, 1000);
|
||||
setInterval(updateMiniNowPlaying, 10000);
|
||||
|
||||
// Disable frameset mode function
|
||||
function disableFramesetMode() {
|
||||
// Clear preference
|
||||
localStorage.removeItem('useFrameset');
|
||||
// Redirect parent window to regular view
|
||||
window.parent.location.href = '/asteroid/';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title lquery="(text title)">🎵 ASTEROID RADIO 🎵</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script>
|
||||
// Prevent nested framesets - break out if we're already in a frame
|
||||
if (window.self !== window.top) {
|
||||
window.top.location.href = window.self.location.href;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<frameset rows="*,80" frameborder="0" border="0" framespacing="0">
|
||||
<frame src="/asteroid/content" name="content-frame" noresize>
|
||||
<frame src="/asteroid/audio-player-frame" name="player-frame" noresize scrolling="no">
|
||||
<noframes>
|
||||
<body>
|
||||
<p>Your browser does not support frames. Please use a modern browser or visit <a href="/asteroid/content">the main site</a>.</p>
|
||||
</body>
|
||||
</noframes>
|
||||
</frameset>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title data-text="title">🎵 ASTEROID RADIO 🎵</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||
<script src="/asteroid/static/js/front-page.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1 data-text="station-name">🎵 ASTEROID RADIO 🎵</h1>
|
||||
<nav class="nav">
|
||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/player-content" target="content-frame">Player</a>
|
||||
<a href="/asteroid/status" target="content-frame">Status</a>
|
||||
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
|
||||
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
|
||||
<a href="/asteroid/login" target="content-frame" data-show-if-logged-out>Login</a>
|
||||
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
|
||||
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="status">
|
||||
<h2>Station Status</h2>
|
||||
<p data-text="status-message">🟢 LIVE - Broadcasting asteroid music for hackers</p>
|
||||
<p>Current listeners: <span data-text="listeners">0</span></p>
|
||||
<p>Stream quality: <span data-text="stream-quality">AAC 96kbps Stereo</span></p>
|
||||
</div>
|
||||
|
||||
<div class="live-stream">
|
||||
<h2 style="color: #00ff00;">🟢 LIVE STREAM</h2>
|
||||
<p><em>The live stream player is now in the persistent bar at the bottom of the page.</em></p>
|
||||
<p><strong>Stream URL:</strong> <code id="stream-url" lquery="(text default-stream-url)"></code></p>
|
||||
<p><strong>Format:</strong> <span id="stream-format" lquery="(text default-stream-encoding-desc)"></span></p>
|
||||
<p><strong>Status:</strong> <span id="stream-status" style="color: #00ff00;">● BROADCASTING</span></p>
|
||||
</div>
|
||||
|
||||
<div id="now-playing" class="now-playing"></div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -33,7 +33,17 @@
|
|||
</div>
|
||||
|
||||
<div class="live-stream">
|
||||
<h2 style="color: #00ff00;">🟢 LIVE STREAM</h2>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<h2 style="color: #00ff00; margin: 0;">🟢 LIVE STREAM</h2>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button id="popout-btn" class="btn btn-info" onclick="openPopoutPlayer()" style="font-size: 0.9em;">
|
||||
🗗 Pop Out Player
|
||||
</button>
|
||||
<button id="frameset-btn" class="btn btn-secondary" onclick="enableFramesetMode()" style="font-size: 0.9em;">
|
||||
🖼️ Enable Persistent Player
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stream Quality Selector -->
|
||||
<div class="live-stream-quality">
|
||||
|
|
@ -56,12 +66,7 @@
|
|||
</audio>
|
||||
</div>
|
||||
|
||||
<div class="now-playing">
|
||||
<h2>Now Playing</h2>
|
||||
<p>Artist: <span data-text="now-playing-artist">The Void</span></p>
|
||||
<p>Track: <span data-text="now-playing-track">Silence</span></p>
|
||||
<p>Listeners: <span data-text="listeners">0</span></p>
|
||||
</div>
|
||||
<div id="now-playing" class="now-playing"></div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
<header>
|
||||
<h1>🎵 ASTEROID RADIO - LOGIN</h1>
|
||||
<nav class="nav">
|
||||
<a href="/asteroid/">Home</a>
|
||||
<a href="/asteroid/player">Player</a>
|
||||
<a href="/asteroid/status">Status</a>
|
||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/player-content" target="content-frame">Player</a>
|
||||
<a href="/asteroid/status" target="content-frame">Status</a>
|
||||
<a href="/asteroid/register">Register</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<h2>Now Playing</h2>
|
||||
<c:if test="stats">
|
||||
<c:then>
|
||||
<c:using value="stats">
|
||||
<!--<p>Artist: <span>The Void</span></p>-->
|
||||
<p>Track: <span lquery="(text title)">The Void - Silence</span></p>
|
||||
<p>Listeners: <span lquery="(text listeners)">1</span></p>
|
||||
</c:using>
|
||||
</c:then>
|
||||
<c:else>
|
||||
<c:if test="connection-error">
|
||||
<c:then>
|
||||
<div class="message error">
|
||||
<span>There was an error trying to get information from stream.</span>
|
||||
</div>
|
||||
</c:then>
|
||||
</c:if>
|
||||
<p>Track: <span>NA</span></p>
|
||||
<p>Listeners: <span>NA</span></p>
|
||||
</c:else>
|
||||
</c:if>
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title data-text="title">Asteroid Radio - Web Player</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||
<script src="/asteroid/static/js/player.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎵 WEB PLAYER</h1>
|
||||
<div class="nav">
|
||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
|
||||
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
|
||||
<a href="/asteroid/login" target="content-frame" data-show-if-logged-out>Login</a>
|
||||
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
|
||||
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||
</div>
|
||||
|
||||
<!-- Live Stream Section - Note about persistent player -->
|
||||
<div class="player-section">
|
||||
<h2 style="color: #00ff00;">🟢 Live Radio Stream</h2>
|
||||
<p><em>The live stream player is now in the persistent bar at the bottom of the page. It will continue playing as you navigate between pages!</em></p>
|
||||
</div>
|
||||
|
||||
<div id="now-playing" class="now-playing"></div>
|
||||
|
||||
<!-- Track Browser -->
|
||||
<div class="player-section">
|
||||
<h2>Personal Track Library</h2>
|
||||
<div class="track-browser">
|
||||
<input type="text" id="search-tracks" placeholder="Search tracks..." class="search-input">
|
||||
<select id="library-tracks-per-page" class="sort-select" onchange="changeLibraryTracksPerPage()" style="margin: 10px 0px;">
|
||||
<option value="10">10 per page</option>
|
||||
<option value="20" selected>20 per page</option>
|
||||
<option value="50">50 per page</option>
|
||||
</select>
|
||||
<div id="track-list" class="track-list">
|
||||
<div class="loading">Loading tracks...</div>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<div id="library-pagination-controls" style="display: none; margin-top: 20px; text-align: center;">
|
||||
<button onclick="libraryGoToPage(1)" class="btn btn-secondary">« First</button>
|
||||
<button onclick="libraryPreviousPage()" class="btn btn-secondary">‹ Prev</button>
|
||||
<span id="library-page-info" style="margin: 0 15px; font-weight: bold;">Page 1 of 1</span>
|
||||
<button onclick="libraryNextPage()" class="btn btn-secondary">Next ›</button>
|
||||
<button onclick="libraryGoToLastPage()" class="btn btn-secondary">Last »</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio Player Widget -->
|
||||
<div class="player-section">
|
||||
<h2>Audio Player</h2>
|
||||
<div class="audio-player">
|
||||
<div class="now-playing">
|
||||
<div class="track-art">🎵</div>
|
||||
<div class="track-details">
|
||||
<div class="track-title" id="current-title">No track selected</div>
|
||||
<div class="track-artist" id="current-artist">Unknown Artist</div>
|
||||
<div class="track-album" id="current-album">Unknown Album</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<audio id="audio-player" controls preload="none" style="width: 100%; margin: 20px 0;">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
<div class="player-controls">
|
||||
<button id="prev-btn" class="btn btn-secondary">⏮️ Previous</button>
|
||||
<button id="play-pause-btn" class="btn btn-primary">▶️ Play</button>
|
||||
<button id="next-btn" class="btn btn-secondary">⏭️ Next</button>
|
||||
<button id="shuffle-btn" class="btn btn-info">🔀 Shuffle</button>
|
||||
<button id="repeat-btn" class="btn btn-warning">🔁 Repeat</button>
|
||||
</div>
|
||||
|
||||
<div class="player-info">
|
||||
<div class="time-display">
|
||||
<span id="current-time">0:00</span> / <span id="total-time">0:00</span>
|
||||
</div>
|
||||
<div class="volume-control">
|
||||
<label for="volume-slider">🔊</label>
|
||||
<input type="range" id="volume-slider" min="0" max="100" value="50" class="volume-slider">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playlist Management -->
|
||||
<div class="player-section">
|
||||
<h2>Playlists</h2>
|
||||
<div class="playlist-controls">
|
||||
<input type="text" id="new-playlist-name" placeholder="New playlist name..." class="playlist-input">
|
||||
<button id="create-playlist" class="btn btn-success">➕ Create Playlist</button>
|
||||
</div>
|
||||
|
||||
<div class="playlist-list">
|
||||
<div id="playlists-container">
|
||||
<div class="no-playlists">No playlists created yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue -->
|
||||
<div class="player-section">
|
||||
<h2>Play Queue</h2>
|
||||
<div class="queue-controls">
|
||||
<button id="clear-queue" class="btn btn-danger">🗑️ Clear Queue</button>
|
||||
<button id="save-queue" class="btn btn-info">💾 Save as Playlist</button>
|
||||
</div>
|
||||
<div id="play-queue" class="play-queue">
|
||||
<div class="empty-queue">Queue is empty</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -12,9 +12,9 @@
|
|||
<div class="container">
|
||||
<h1>🎵 WEB PLAYER</h1>
|
||||
<div class="nav">
|
||||
<a href="/asteroid/">Home</a>
|
||||
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
|
||||
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
|
||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
|
||||
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
|
||||
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
|
||||
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
|
||||
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||
|
|
@ -25,8 +25,6 @@
|
|||
<h2 style="color: #00ff00;">🟢 Live Radio Stream</h2>
|
||||
<div class="live-stream">
|
||||
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
|
||||
<p><strong>Now Playing:</strong> <span id="live-now-playing">Loading...</span></p>
|
||||
<p><strong>Listeners:</strong> <span id="live-listeners">0</span></p>
|
||||
<!-- Stream Quality Selector -->
|
||||
<div class="live-stream-quality">
|
||||
<label for="live-stream-quality"><strong>Quality:</strong></label>
|
||||
|
|
@ -45,6 +43,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="now-playing" class="now-playing"></div>
|
||||
|
||||
<!-- Track Browser -->
|
||||
<div class="player-section">
|
||||
<h2>Personal Track Library</h2>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,234 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>🎵 Asteroid Radio - Player</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background: #0a0a0a;
|
||||
overflow: hidden;
|
||||
}
|
||||
.popout-container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.popout-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #2a3441;
|
||||
}
|
||||
.popout-title {
|
||||
font-size: 1.2em;
|
||||
color: #00ff00;
|
||||
}
|
||||
.close-btn {
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: #ff6666;
|
||||
}
|
||||
.now-playing-mini {
|
||||
background: #1a1a1a;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #2a3441;
|
||||
}
|
||||
.track-info-mini {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.track-title-mini {
|
||||
color: #00ff00;
|
||||
font-weight: bold;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.track-artist-mini {
|
||||
color: #4488ff;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.quality-selector {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #2a3441;
|
||||
}
|
||||
.quality-selector label {
|
||||
color: #00ff00;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.quality-selector select {
|
||||
background: #0a0a0a;
|
||||
color: #00ff00;
|
||||
border: 1px solid #2a3441;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
audio {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.status-mini {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-size: 0.85em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
<script src="/asteroid/static/js/front-page.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="popout-container">
|
||||
<div class="popout-header">
|
||||
<div class="popout-title">🎵 Asteroid Radio</div>
|
||||
<button class="close-btn" onclick="window.close()">✖ Close</button>
|
||||
</div>
|
||||
|
||||
<div class="now-playing-mini">
|
||||
<div class="track-info-mini">
|
||||
<div class="track-title-mini" id="popout-track-title">Loading...</div>
|
||||
<div class="track-artist-mini" id="popout-track-artist">Please wait</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quality-selector">
|
||||
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
|
||||
<label for="popout-stream-quality"><strong>Quality:</strong></label>
|
||||
<select id="popout-stream-quality" onchange="changeStreamQuality()">
|
||||
<option value="aac">AAC 96kbps</option>
|
||||
<option value="mp3">MP3 128kbps</option>
|
||||
<option value="low">MP3 64kbps</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<audio id="live-audio" controls autoplay style="width: 100%;">
|
||||
<source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
<div class="status-mini">
|
||||
<span style="color: #00ff00;">● LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Stream quality configuration for popout
|
||||
function getStreamConfig(streamBaseUrl, encoding) {
|
||||
const config = {
|
||||
aac: {
|
||||
url: `${streamBaseUrl}/asteroid.aac`,
|
||||
format: 'AAC 96kbps Stereo',
|
||||
type: 'audio/aac',
|
||||
mount: 'asteroid.aac'
|
||||
},
|
||||
mp3: {
|
||||
url: `${streamBaseUrl}/asteroid.mp3`,
|
||||
format: 'MP3 128kbps Stereo',
|
||||
type: 'audio/mpeg',
|
||||
mount: 'asteroid.mp3'
|
||||
},
|
||||
low: {
|
||||
url: `${streamBaseUrl}/asteroid-low.mp3`,
|
||||
format: 'MP3 64kbps Stereo',
|
||||
type: 'audio/mpeg',
|
||||
mount: 'asteroid-low.mp3'
|
||||
}
|
||||
};
|
||||
return config[encoding];
|
||||
}
|
||||
|
||||
// Change stream quality in popout
|
||||
function changeStreamQuality() {
|
||||
const selector = document.getElementById('popout-stream-quality');
|
||||
const streamBaseUrl = document.getElementById('stream-base-url');
|
||||
const config = getStreamConfig(streamBaseUrl.value, selector.value);
|
||||
|
||||
// Update audio player
|
||||
const audioElement = document.getElementById('live-audio');
|
||||
const sourceElement = document.getElementById('audio-source');
|
||||
|
||||
const wasPlaying = !audioElement.paused;
|
||||
|
||||
sourceElement.src = config.url;
|
||||
sourceElement.type = config.type;
|
||||
audioElement.load();
|
||||
|
||||
// Resume playback if it was playing
|
||||
if (wasPlaying) {
|
||||
audioElement.play().catch(e => console.log('Autoplay prevented:', e));
|
||||
}
|
||||
}
|
||||
|
||||
// Update now playing info for popout
|
||||
async function updatePopoutNowPlaying() {
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
||||
const html = await response.text();
|
||||
|
||||
// Parse the HTML to extract track info
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const trackText = doc.body.textContent || doc.body.innerText || '';
|
||||
|
||||
// Try to split artist - title format
|
||||
const parts = trackText.split(' - ');
|
||||
if (parts.length >= 2) {
|
||||
document.getElementById('popout-track-artist').textContent = parts[0].trim();
|
||||
document.getElementById('popout-track-title').textContent = parts.slice(1).join(' - ').trim();
|
||||
} else {
|
||||
document.getElementById('popout-track-title').textContent = trackText.trim();
|
||||
document.getElementById('popout-track-artist').textContent = 'Asteroid Radio';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating now playing:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update every 10 seconds
|
||||
setInterval(updatePopoutNowPlaying, 10000);
|
||||
// Initial update
|
||||
updatePopoutNowPlaying();
|
||||
|
||||
// Auto-reconnect on stream errors
|
||||
const audioElement = document.getElementById('live-audio');
|
||||
audioElement.addEventListener('error', function(e) {
|
||||
console.log('Stream error, attempting reconnect in 3 seconds...');
|
||||
setTimeout(function() {
|
||||
audioElement.load();
|
||||
audioElement.play().catch(err => console.log('Reconnect failed:', err));
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
audioElement.addEventListener('stalled', function() {
|
||||
console.log('Stream stalled, reloading...');
|
||||
audioElement.load();
|
||||
audioElement.play().catch(err => console.log('Reload failed:', err));
|
||||
});
|
||||
|
||||
// Notify parent window that popout is open
|
||||
if (window.opener && !window.opener.closed) {
|
||||
window.opener.postMessage({ type: 'popout-opened' }, '*');
|
||||
}
|
||||
|
||||
// Notify parent when closing
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (window.opener && !window.opener.closed) {
|
||||
window.opener.postMessage({ type: 'popout-closed' }, '*');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -12,9 +12,9 @@
|
|||
<div class="container">
|
||||
<h1>👤 USER PROFILE</h1>
|
||||
<div class="nav">
|
||||
<a href="/asteroid/">Home</a>
|
||||
<a href="/asteroid/player/">Player</a>
|
||||
<a href="/asteroid/admin/" data-show-if-admin>Admin</a>
|
||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/player-content" target="content-frame">Player</a>
|
||||
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
|
||||
<a href="/asteroid/logout" class="btn-logout">Logout</a>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
<header>
|
||||
<h1>🎵 ASTEROID RADIO - REGISTER</h1>
|
||||
<nav class="nav">
|
||||
<a href="/asteroid/">Home</a>
|
||||
<a href="/asteroid/player">Player</a>
|
||||
<a href="/asteroid/status">Status</a>
|
||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/player-content" target="content-frame">Player</a>
|
||||
<a href="/asteroid/status" target="content-frame">Status</a>
|
||||
<a href="/asteroid/login">Login</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
<div class="container">
|
||||
<h1>👥 USER MANAGEMENT</h1>
|
||||
<div class="nav">
|
||||
<a href="/asteroid/">Home</a>
|
||||
<a href="/asteroid/admin">Admin</a>
|
||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/admin" target="content-frame">Admin</a>
|
||||
<a href="/asteroid/logout" class="btn-logout">Logout</a>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -282,12 +282,15 @@
|
|||
;; Fallback to delayed initialization
|
||||
(bt:make-thread
|
||||
(lambda ()
|
||||
(sleep 3) ; Give database more time to initialize
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Retrying user management setup...~%")
|
||||
(create-default-admin)
|
||||
(format t "User management initialization complete.~%"))
|
||||
(error (e)
|
||||
(format t "Error initializing user system: ~a~%" e))))
|
||||
(dotimes (a 5)
|
||||
(unless (db:connected-p)
|
||||
(sleep 3)) ; Give database more time to initialize
|
||||
(handler-case
|
||||
(progn
|
||||
(format t "Retrying user management setup...~%")
|
||||
(create-default-admin)
|
||||
(format t "User management initialization complete.~%")
|
||||
(return))
|
||||
(error (e)
|
||||
(format t "Error initializing user system: ~a~%" e)))))
|
||||
:name "user-init"))))
|
||||
|
|
|
|||
Loading…
Reference in New Issue