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:
parent
4f399b95fa
commit
8298dfed4c
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
137
asteroid.lisp
137
asteroid.lisp
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue