Migrate from Hunchentoot to RADIANCE framework

Major Changes:
- Replace Hunchentoot with RADIANCE web framework
- Add Shirakumo distribution for RADIANCE access
- Implement proper RADIANCE module with define-module declaration
- Convert all route handlers to use define-page syntax
- Update route paths for subdomain routing (asteroid.localhost:8080)
- Fix API endpoint to use define-page instead of define-api
- Update server management to use radiance:startup/shutdown

Technical Improvements:
- Modular architecture with subdomain routing
- Proper RADIANCE module integration
- Updated documentation with migration details
- Fixed route syntax and parentheses issues
- Added comprehensive server startup commands

Routes now accessible at:
- Main: http://asteroid.localhost:8080/
- Admin: http://asteroid.localhost:8080/admin
- Player: http://asteroid.localhost:8080/player
- API: http://asteroid.localhost:8080/api/status
This commit is contained in:
Glenn Thompson 2025-08-20 10:13:34 +03:00 committed by Brian O'Reilly
parent 4f399b95fa
commit 8298dfed4c
3 changed files with 114 additions and 138 deletions

View File

@ -6,7 +6,7 @@
:author "Brian O'Reilly <fade@deepsky.com>"
:license "GNU AFFERO GENERAL PUBLIC LICENSE V.3"
:serial t
:depends-on (:HUNCHENTOOT
:depends-on (:RADIANCE
:SPINNERET
:CL-JSON
)

View File

@ -1,6 +1,6 @@
;; -*-lisp-*-
(defpackage :asteroid
(:use :cl)
(:use :cl :radiance)
(:use :asteroid.app-utils)
(:export :-main
:start-server
@ -9,19 +9,21 @@
(in-package :asteroid)
;; Define as RADIANCE module
(define-module asteroid
(:use #:cl #:radiance)
(:domain "asteroid"))
;; Configuration
(defparameter *server-port* 8080)
(defparameter *server-host* "localhost")
(defparameter *acceptor* nil)
;; HTML generation helpers
(defun generate-page-html (title &rest body-content)
"Generate a complete HTML page with consistent styling"
;; RADIANCE route handlers
(define-page index #@"/" ()
(spinneret:with-html-string
(:doctype)
(:html
(:head
(:title title)
(:title "🎵 ASTEROID RADIO 🎵")
(:meta :charset "utf-8")
(:meta :name "viewport" :content "width=device-width, initial-scale=1")
(:style "
@ -32,64 +34,35 @@
.status { background: #1a1a1a; padding: 20px; border: 1px solid #333; margin: 20px 0; }
.panel { background: #1a1a1a; padding: 20px; border: 1px solid #333; margin: 20px 0; }
.nav { margin: 20px 0; }
.nav a { color: #00ff00; text-decoration: none; margin-right: 20px; padding: 10px; border: 1px solid #333; }
.nav a:hover { background: #333; }
.back { color: #00ff00; text-decoration: none; }
button { background: #333; color: #00ff00; border: 1px solid #555; padding: 10px 20px; margin: 5px; cursor: pointer; }
button:hover { background: #555; }
.player { background: #1a1a1a; padding: 40px; border: 1px solid #333; margin: 40px auto; max-width: 600px; text-align: center; }
.now-playing { font-size: 1.5em; margin: 20px 0; color: #ff6600; }
.controls button { padding: 15px 30px; margin: 10px; font-size: 1.2em; }
"))
(:body
(:div.container
(mapcar (lambda (element) element) body-content))))))
;; Route handlers
(defun handle-index ()
"Main page handler"
(spinneret:with-html-string
(:doctype)
(:html
(:head
(:title "Asteroid Radio - Music for Hackers")
(:meta :charset "utf-8")
(:meta :name "viewport" :content "width=device-width, initial-scale=1")
(:style "
body { font-family: 'Courier New', monospace; background: #0a0a0a; color: #00ff00; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #ff6600; text-align: center; font-size: 2.5em; margin-bottom: 30px; }
h2 { color: #ff6600; }
.status { background: #1a1a1a; padding: 20px; border: 1px solid #333; margin: 20px 0; }
.nav { margin: 20px 0; }
.nav a { color: #00ff00; text-decoration: none; margin-right: 20px; padding: 10px; border: 1px solid #333; }
.nav a { color: #00ff00; text-decoration: none; margin: 0 15px; padding: 10px 20px; border: 1px solid #333; background: #1a1a1a; display: inline-block; }
.nav a:hover { background: #333; }
.controls { margin: 20px 0; }
.controls button { background: #1a1a1a; color: #00ff00; border: 1px solid #333; padding: 10px 20px; margin: 5px; cursor: pointer; }
.controls button:hover { background: #333; }
.now-playing { background: #1a1a1a; padding: 20px; border: 1px solid #333; margin: 20px 0; }
.back { color: #00ff00; text-decoration: none; margin-bottom: 20px; display: inline-block; }
.back:hover { text-decoration: underline; }
"))
(:body
(:div.container
(:h1 "🎵 ASTEROID RADIO 🎵")
(:div.status
(:h2 "Station Status")
(:p "Status: " (:strong "Running"))
(:p "Now Playing: " (:em "Silence (for now)"))
(:p "Listeners: 0"))
(:p "🟢 LIVE - Broadcasting asteroid music for hackers")
(:p "Current listeners: 0")
(:p "Stream quality: 128kbps MP3"))
(:div.nav
(:a :href "/admin" "Admin Dashboard")
(:a :href "/player" "Web Player")
(:a :href "/api/status" "API Status"))
(:div
(:h2 "Welcome to Asteroid Radio")
(:p "A streaming radio station for hackers, built with Common Lisp.")
(:p "Features coming soon:")
(:ul
(:li "Auto-DJ with crossfading")
(:li "Live DJ handoff")
(:li "Song requests")
(:li "Admin dashboard")
(:li "Music library management"))))))))
(:h2 "Now Playing")
(:p "Artist: The Void")
(:p "Track: Silence")
(:p "Album: Startup Sounds")
(:p "Duration: ∞")))))))
(defun handle-admin ()
"Admin dashboard handler"
(define-page admin #@"/admin" ()
(spinneret:with-html-string
(:doctype)
(:html
@ -132,8 +105,7 @@
(:p "Liquidsoap: Not Running")
(:p "Icecast: Not Running")))))))
(defun handle-player ()
"Web player handler"
(define-page player #@"/player" ()
(spinneret:with-html-string
(:doctype)
(:html
@ -164,9 +136,8 @@
(:p "Bitrate: 128kbps MP3")
(:p "Status: Offline")))))))
(defun handle-api-status ()
"API status endpoint handler"
(setf (hunchentoot:content-type*) "application/json")
(define-page api/status #@"/api/status" ()
(setf (radiance:header "Content-Type") "application/json")
(cl-json:encode-json-to-string
`(("status" . "running")
("server" . "asteroid-radio")
@ -178,48 +149,22 @@
("listeners" . 0)
("stream-url" . "http://localhost:8000/asteroid"))))
;; Route setup
(defun setup-routes ()
"Set up all HTTP routes"
(hunchentoot:define-easy-handler (index :uri "/") ()
(handle-index))
(hunchentoot:define-easy-handler (admin :uri "/admin") ()
(handle-admin))
(hunchentoot:define-easy-handler (player :uri "/player") ()
(handle-player))
(hunchentoot:define-easy-handler (api-status :uri "/api/status") ()
(handle-api-status)))
;; Server management functions
(defun start-server (&key (port *server-port*) (host *server-host*))
"Start the Asteroid Radio web server"
(when *acceptor*
(hunchentoot:stop *acceptor*))
(format t "Setting up routes...~%")
(setup-routes)
(format t "Starting Asteroid Radio server on ~a:~a~%" host port)
(setf *acceptor* (make-instance 'hunchentoot:easy-acceptor
:port port
:address host))
(hunchentoot:start *acceptor*)
(format t "Server started! Visit http://~a:~a~%" host port))
;; RADIANCE server management functions
(defun start-server (&key (port *server-port*))
"Start the Asteroid Radio RADIANCE server"
(format t "Starting Asteroid Radio RADIANCE server on port ~a~%" port)
(radiance:startup)
(format t "Server started! Visit http://localhost:~a/asteroid/~%" port))
(defun stop-server ()
"Stop the Asteroid Radio web server"
(when *acceptor*
(format t "Stopping Asteroid Radio server...~%")
(hunchentoot:stop *acceptor*)
(setf *acceptor* nil)
(format t "Server stopped.~%")))
"Stop the Asteroid Radio RADIANCE server"
(format t "Stopping Asteroid Radio server...~%")
(radiance:shutdown)
(format t "Server stopped.~%"))
(defun run-server (&key (port *server-port*) (host *server-host*))
(defun run-server (&key (port *server-port*))
"Start the server and keep it running (blocking)"
(start-server :port port :host host)
(start-server :port port)
(format t "Server running. Press Ctrl+C to stop.~%")
;; Keep the server running
(handler-case
@ -231,6 +176,6 @@
(defun -main (&optional args)
(declare (ignore args))
(format t "~%🎵 ASTEROID RADIO - Music for Hackers 🎵~%")
(format t "Starting web server...~%")
(format t "Starting RADIANCE web server...~%")
(run-server))

View File

@ -32,39 +32,42 @@ This document summarizes the implementation of a basic web server for the Astero
** Removed Dependencies
| Dependency | Reason for Removal |
|------------|-------------------|
| :RADIANCE | Not available in Quicklisp distribution |
| :MITO | Not available in Quicklisp, not needed for basic web server |
| :MITO-AUTH | Not available in Quicklisp, authentication not needed for MVP |
| :RADIANCE | Requires separate Shirakumo dist installation, switched to simpler Hunchentoot for MVP |
| :MITO | Not available in default Quicklisp, not needed for basic web server |
| :MITO-AUTH | Not available in default Quicklisp, authentication not needed for MVP |
| :STR | Not used in current implementation |
| :PZMQ | Not needed for basic web server functionality |
** Final Dependencies
| Dependency | Purpose | Justification |
|------------|---------|---------------|
| :HUNCHENTOOT | Web server framework | Widely available, stable, well-documented Common Lisp web server |
| :RADIANCE | Web framework | Modular web framework with subdomain routing and integrated module system |
| :SPINNERET | HTML generation | Clean DSL for generating HTML, integrates well with Common Lisp |
| :CL-JSON | JSON encoding/decoding | Standard library for API endpoints |
** Why Hunchentoot Over RADIANCE
- **Availability**: RADIANCE not found in Quicklisp distribution
- **Stability**: Hunchentoot is mature, battle-tested web server
- **Documentation**: Extensive documentation and community support
- **Simplicity**: Easier to set up for basic web server needs
** Migration from Hunchentoot to RADIANCE
- **Initial Choice**: Started with Hunchentoot for simpler MVP setup
- **Discovery**: RADIANCE available via Shirakumo dist: `(ql-dist:install-dist "http://dist.shirakumo.org/shirakumo.txt")`
- **Migration Completed**: Successfully migrated to RADIANCE framework
- **Benefits**: RADIANCE provides modular architecture, subdomain routing, and integrated module system
- **Result**: More scalable foundation for future radio station features
* Implementation Details
** Web Server Architecture
- Port: 8080 (configurable via =*server-port*=)
- Host: localhost (configurable via =*server-host*=)
- Server management: =*acceptor*= global variable tracks server instance
- Framework: RADIANCE modular web framework
- Port: 8080 (default RADIANCE configuration)
- Module: asteroid (domain: "asteroid")
- Subdomain routing: asteroid.localhost:8080
- Server management: =radiance:startup= / =radiance:shutdown=
** Route Structure
| Route | Handler | Purpose |
|-------|---------|---------|
| / | =handle-index= | Main page with station status |
| /admin | =handle-admin= | Admin dashboard with controls |
| /player | =handle-player= | Web player interface |
| /api/status | =handle-api-status= | JSON API endpoint |
** Route Structure (RADIANCE)
| Route | Handler | Purpose | URL |
|-------|---------|---------|-----|
| / | =index= | Main page with station status | http://asteroid.localhost:8080/ |
| /admin | =admin= | Admin dashboard with controls | http://asteroid.localhost:8080/admin |
| /player | =player= | Web player interface | http://asteroid.localhost:8080/player |
| /api/status | =api/status= | JSON API endpoint | http://asteroid.localhost:8080/api/status |
** HTML Generation Strategy
- Direct use of =spinneret:with-html-string= in each handler
@ -88,14 +91,19 @@ Removed unused dependencies from =asteroid.asd=:
- Removed :MITO, :MITO-AUTH, :STR, :PZMQ
- Kept only essential dependencies: :HUNCHENTOOT, :SPINNERET, :CL-JSON
** Error 2: RADIANCE Framework Unavailable
** Error 2: RADIANCE Framework Migration
*** Problem
Original design assumed RADIANCE web framework, but not available in Quicklisp.
Initially avoided RADIANCE due to perceived unavailability, implemented with Hunchentoot instead.
*** Root Cause
RADIANCE available via Shirakumo dist: `(ql-dist:install-dist "http://dist.shirakumo.org/shirakumo.txt")` but required additional setup step.
*** Solution
- Replaced RADIANCE with Hunchentoot
- Rewrote web server initialization and route handling
- Used =hunchentoot:define-easy-handler= for route definitions
- Successfully migrated from Hunchentoot to RADIANCE
- Installed Shirakumo distribution for RADIANCE access
- Rewrote route handlers using =define-page= syntax
- Added =define-module= declaration for proper RADIANCE integration
- Updated server management to use =radiance:startup= / =radiance:shutdown=
** Error 3: HTML Generation Function Signature Mismatch
*** Problem
@ -109,21 +117,23 @@ Initial =generate-page-html= helper function designed for single body argument,
*** Solution
Attempted fix with =&rest= parameter, but Spinneret macro expansion issues persisted.
** Error 4: Spinneret Macro Expansion Issues
** Error 4: RADIANCE Route Syntax Issues
*** Problem
#+BEGIN_EXAMPLE
[ERROR] The function :H1 is undefined.
Internal Server Error in browser
Module #<PACKAGE "ASTEROID"> requested but while the package exists, it is not a module.
The value #@"asteroid/api/status" is not of type LIST
#+END_EXAMPLE
*** Root Cause
Complex helper function approach interfered with Spinneret's macro expansion system.
- Missing =define-module= declaration for RADIANCE integration
- Incorrect route path syntax using =asteroid/= prefix instead of module-relative paths
- Wrong API endpoint definition syntax
*** Solution
- Abandoned helper function approach
- Rewrote each handler to use =spinneret:with-html-string= directly
- Embedded CSS styling directly in each page
- Simplified HTML generation to work within Spinneret's macro system
- Added =define-module= declaration with proper domain specification
- Fixed route paths: =#@"asteroid/"==#@"/"=, =#@"asteroid/admin"==#@"/admin"=
- Updated API endpoint to use =define-page= instead of =define-api=
- Fixed parentheses syntax errors in HTML generation
** Error 5: Shell History Expansion Issues
*** Problem
@ -160,31 +170,43 @@ Used single quotes instead of double quotes for SBCL command-line arguments to p
** 🚀 Running the Server
*** RADIANCE Setup (One-time)
#+BEGIN_EXAMPLE
sbcl --eval '(ql-dist:install-dist "http://dist.shirakumo.org/shirakumo.txt")'
#+END_EXAMPLE
*** Command Line (One-shot execution)
#+BEGIN_EXAMPLE
sbcl --eval '(ql:quickload (quote (:hunchentoot :spinneret :cl-json)))' \
sbcl --eval '(ql:quickload (quote (:radiance :spinneret :cl-json)))' \
--eval '(load "asteroid.asd")' \
--eval '(asdf:load-system :asteroid)' \
--eval '(asteroid:start-server)' \
--eval '(format t "Server running at http://localhost:8080 - Press Ctrl+C to stop")'
--eval '(format t "Server running at http://asteroid.localhost:8080/ - Press Ctrl+C to stop")'
#+END_EXAMPLE
*** Interactive REPL
#+BEGIN_EXAMPLE
sbcl
(ql:quickload '(:hunchentoot :spinneret :cl-json))
(ql:quickload '(:radiance :spinneret :cl-json))
(load "asteroid.asd")
(asdf:load-system :asteroid)
(asteroid:start-server)
;; Server now running at http://localhost:8080
;; Server now running at http://asteroid.localhost:8080/
;; To stop: (asteroid:stop-server)
#+END_EXAMPLE
*** Available Functions
- =(asteroid:start-server)= - Start web server (non-blocking)
- =(asteroid:stop-server)= - Stop web server cleanly
- =(asteroid:start-server)= - Start RADIANCE server (non-blocking)
- =(asteroid:stop-server)= - Stop RADIANCE server cleanly
- =(asteroid:run-server)= - Start server and keep running (blocking, with Ctrl+C handler)
*** Access URLs
- **Main page**: http://asteroid.localhost:8080/
- **Admin dashboard**: http://asteroid.localhost:8080/admin
- **Web player**: http://asteroid.localhost:8080/player
- **API endpoint**: http://asteroid.localhost:8080/api/status
- **RADIANCE welcome**: http://localhost:8080/
** 📋 Next Steps (Not Implemented)
- Database integration (when MITO alternative chosen)
- Audio streaming backend (Liquidsoap integration)
@ -193,6 +215,7 @@ sbcl
- Authentication system
- Real-time now-playing updates
- WebSocket integration for live updates
- **Completed**: ✅ Successfully migrated to RADIANCE framework
* Technical Lessons Learned
@ -202,9 +225,11 @@ sbcl
- Keep dependency list minimal for initial implementation
** Common Lisp Web Development
- Hunchentoot provides robust foundation for web applications
- RADIANCE provides modular architecture with subdomain routing
- Spinneret works best with direct macro usage, not through helper functions
- HTML generation should be kept simple and direct
- RADIANCE modules require proper =define-module= declarations
- Route paths in RADIANCE are module-relative (use =#@"/"= not =#@"asteroid/"=)
** Error Handling Strategy
- Compilation warnings often indicate runtime issues
@ -233,6 +258,12 @@ asteroid/
* Conclusion
Successfully implemented a functional web server foundation for the Asteroid Radio project. The server provides a complete web interface with admin controls, player interface, and API endpoints. Key success factors included pragmatic dependency choices, incremental development approach, and thorough error resolution.
Successfully implemented and migrated a functional web server foundation for the Asteroid Radio project using RADIANCE framework. The server provides a complete web interface with admin controls, player interface, and API endpoints accessible via subdomain routing at asteroid.localhost:8080.
The implementation is ready for the next development phase: integrating audio streaming components and database functionality.
Key achievements:
- **Framework Migration**: Successfully migrated from Hunchentoot to RADIANCE
- **Modular Architecture**: Implemented proper RADIANCE module with subdomain routing
- **Complete Web Interface**: Main page, admin dashboard, web player, and JSON API
- **Scalable Foundation**: RADIANCE provides better architecture for future radio features
The implementation demonstrates the value of exploring framework alternatives and provides a robust, modular foundation ready for the next development phase: integrating audio streaming components (Liquidsoap/Icecast) and database functionality.