From 8298dfed4c4a8463883ff57a220048350d32c6ae Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Wed, 20 Aug 2025 10:13:34 +0300 Subject: [PATCH] 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 --- asteroid.asd | 2 +- asteroid.lisp | 137 +++++++++++++------------------------------- project-summary.org | 113 +++++++++++++++++++++++------------- 3 files changed, 114 insertions(+), 138 deletions(-) diff --git a/asteroid.asd b/asteroid.asd index d0ebe38..9c6d1bc 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -6,7 +6,7 @@ :author "Brian O'Reilly " :license "GNU AFFERO GENERAL PUBLIC LICENSE V.3" :serial t - :depends-on (:HUNCHENTOOT + :depends-on (:RADIANCE :SPINNERET :CL-JSON ) diff --git a/asteroid.lisp b/asteroid.lisp index fe9c9bc..a71a534 100644 --- a/asteroid.lisp +++ b/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)) diff --git a/project-summary.org b/project-summary.org index 2c49e27..f29aeb5 100644 --- a/project-summary.org +++ b/project-summary.org @@ -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 # 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.