From 4bb1b1697a9737163d1b168fde87f66545ede3fb Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Wed, 20 Aug 2025 08:38:04 +0300 Subject: [PATCH] Implement Hunchentoot-based web server - Replace RADIANCE with Hunchentoot web framework - Remove unavailable dependencies (MITO, MITO-AUTH, STR, PZMQ) - Add working web interface with main, admin, and player pages - Implement JSON API endpoint at /api/status - Fix HTML generation issues with proper Spinneret usage - Add hacker-themed styling (green terminal aesthetic) - Include comprehensive project documentation Features: - Web server on localhost:8080 - Admin dashboard with placeholder controls - Web player interface (UI ready for streaming integration) - Station status display - Server management functions (start/stop/run) - Upstream git remote configuration Technical improvements: - Direct Spinneret macro usage for reliable HTML generation - Proper error handling and clean server shutdown - Modular route handlers with consistent styling - Cross-platform compatibility via app-utils --- asteroid.asd | 7 +- asteroid.lisp | 231 +++++++++++++++++++++++++++++++++++++++++++- project-summary.org | 211 ++++++++++++++++++++++++++++++++++++++++ test-server.lisp | 28 ++++++ 4 files changed, 469 insertions(+), 8 deletions(-) create mode 100644 project-summary.org create mode 100644 test-server.lisp diff --git a/asteroid.asd b/asteroid.asd index 5762ddc..d0ebe38 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -6,12 +6,9 @@ :author "Brian O'Reilly " :license "GNU AFFERO GENERAL PUBLIC LICENSE V.3" :serial t - :depends-on (:RADIANCE - :MITO - :MITO-AUTH - :STR - :PZMQ + :depends-on (:HUNCHENTOOT :SPINNERET + :CL-JSON ) :pathname "./" :components ((:file "app-utils") diff --git a/asteroid.lisp b/asteroid.lisp index e2a341c..fe9c9bc 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -2,10 +2,235 @@ (defpackage :asteroid (:use :cl) (:use :asteroid.app-utils) - (:export :-main)) + (:export :-main + :start-server + :stop-server + :run-server)) (in-package :asteroid) -(defun -main (&optional args) - (format t "~a~%" "I don't do much yet")) +;; 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" + (spinneret:with-html-string + (:doctype) + (:html + (:head + (:title title) + (: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; } + .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:hover { background: #333; } + ")) + (: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")) + (: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")))))))) + +(defun handle-admin () + "Admin dashboard handler" + (spinneret:with-html-string + (:doctype) + (:html + (:head + (:title "Asteroid Radio - Admin Dashboard") + (:meta :charset "utf-8") + (: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; } + .panel { background: #1a1a1a; padding: 20px; border: 1px solid #333; margin: 20px 0; } + button { background: #333; color: #00ff00; border: 1px solid #555; padding: 10px 20px; margin: 5px; cursor: pointer; } + button:hover { background: #555; } + .back { color: #00ff00; text-decoration: none; } + ")) + (:body + (:div.container + (:a.back :href "/" "← Back to Main") + (:h1 "Admin Dashboard") + (:div.panel + (:h2 "Playback Control") + (:button "Play") + (:button "Pause") + (:button "Skip") + (:button "Stop")) + (:div.panel + (:h2 "Library Management") + (:button "Upload Music") + (:button "Manage Playlists") + (:button "Scan Library")) + (:div.panel + (:h2 "Live DJ") + (:button "Go Live") + (:button "End Session") + (:button "Mic Check")) + (:div.panel + (:h2 "System Status") + (:p "Server: Running") + (:p "Database: Not Connected") + (:p "Liquidsoap: Not Running") + (:p "Icecast: Not Running"))))))) + +(defun handle-player () + "Web player handler" + (spinneret:with-html-string + (:doctype) + (:html + (:head + (:title "Asteroid Radio - Web Player") + (:meta :charset "utf-8") + (:style " + body { font-family: 'Courier New', monospace; background: #0a0a0a; color: #00ff00; margin: 0; padding: 20px; text-align: center; } + .player { background: #1a1a1a; padding: 40px; border: 1px solid #333; margin: 40px auto; max-width: 600px; } + .now-playing { font-size: 1.5em; margin: 20px 0; color: #ff6600; } + .controls button { background: #333; color: #00ff00; border: 1px solid #555; padding: 15px 30px; margin: 10px; font-size: 1.2em; cursor: pointer; } + .controls button:hover { background: #555; } + .back { color: #00ff00; text-decoration: none; } + ")) + (:body + (:a.back :href "/" "← Back to Main") + (:div.player + (:h1 "🎵 ASTEROID RADIO PLAYER 🎵") + (:div.now-playing + (:div "Now Playing:") + (:div "Silence - The Sound of Startup")) + (:div.controls + (:button "▶ Play Stream") + (:button "⏸ Pause") + (:button "🔊 Volume")) + (:div + (:p "Stream URL: http://localhost:8000/asteroid") + (:p "Bitrate: 128kbps MP3") + (:p "Status: Offline"))))))) + +(defun handle-api-status () + "API status endpoint handler" + (setf (hunchentoot:content-type*) "application/json") + (cl-json:encode-json-to-string + `(("status" . "running") + ("server" . "asteroid-radio") + ("version" . "0.1.0") + ("uptime" . ,(get-universal-time)) + ("now-playing" . (("title" . "Silence") + ("artist" . "The Void") + ("album" . "Startup Sounds"))) + ("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)) + +(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.~%"))) + +(defun run-server (&key (port *server-port*) (host *server-host*)) + "Start the server and keep it running (blocking)" + (start-server :port port :host host) + (format t "Server running. Press Ctrl+C to stop.~%") + ;; Keep the server running + (handler-case + (loop (sleep 1)) + (sb-sys:interactive-interrupt () + (format t "~%Received interrupt, stopping server...~%") + (stop-server)))) + +(defun -main (&optional args) + (declare (ignore args)) + (format t "~%🎵 ASTEROID RADIO - Music for Hackers 🎵~%") + (format t "Starting web server...~%") + (run-server)) diff --git a/project-summary.org b/project-summary.org new file mode 100644 index 0000000..93ff315 --- /dev/null +++ b/project-summary.org @@ -0,0 +1,211 @@ +#+TITLE: Asteroid Radio Web Server Implementation Summary +#+DATE: 2025-08-20 +#+AUTHOR: Development Session Summary + +* Project Overview + +This document summarizes the implementation of a basic web server for the Asteroid Radio project, a Common Lisp-based streaming radio station for "asteroid music for hackers." + +** Project Context +- Fork of https://github.com/fade/asteroid +- Goal: Create streaming radio station with web interface +- Started with basic Common Lisp framework skeleton +- Implemented web server as foundation for future streaming components + +* Initial State Analysis + +** Original Dependencies (asteroid.asd) +- :RADIANCE (web framework) +- :MITO (database ORM) +- :MITO-AUTH (authentication) +- :STR (string utilities) +- :PZMQ (ZeroMQ bindings) +- :SPINNERET (HTML generation) + +** Original Code Structure +- =asteroid.lisp= - Main package with placeholder =-main= function +- =app-utils.lisp= - Utility functions for debugger control and cross-platform quit +- =design.org= - Comprehensive feature specification and MVP roadmap + +* Dependency Changes and Rationale + +** 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 | +| :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 | +| :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 + +* 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 + +** 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 | + +** HTML Generation Strategy +- Direct use of =spinneret:with-html-string= in each handler +- Consistent hacker-themed styling (green text, black background) +- Responsive design with CSS embedded in each page + +* Errors Encountered and Solutions + +** Error 1: Missing MITO Dependency +*** Problem +#+BEGIN_EXAMPLE +debugger invoked on a ASDF/FIND-COMPONENT:MISSING-DEPENDENCY +Component :MITO not found, required by # +#+END_EXAMPLE + +*** Root Cause +MITO and related database dependencies not available in Quicklisp distribution. + +*** Solution +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 +*** Problem +Original design assumed RADIANCE web framework, but not available in Quicklisp. + +*** Solution +- Replaced RADIANCE with Hunchentoot +- Rewrote web server initialization and route handling +- Used =hunchentoot:define-easy-handler= for route definitions + +** Error 3: HTML Generation Function Signature Mismatch +*** Problem +#+BEGIN_EXAMPLE +The function GENERATE-PAGE-HTML is called with five arguments, but wants exactly two. +#+END_EXAMPLE + +*** Root Cause +Initial =generate-page-html= helper function designed for single body argument, but called with multiple arguments. + +*** Solution +Attempted fix with =&rest= parameter, but Spinneret macro expansion issues persisted. + +** Error 4: Spinneret Macro Expansion Issues +*** Problem +#+BEGIN_EXAMPLE +[ERROR] The function :H1 is undefined. +Internal Server Error in browser +#+END_EXAMPLE + +*** Root Cause +Complex helper function approach interfered with Spinneret's macro expansion system. + +*** 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 + +** Error 5: Shell History Expansion Issues +*** Problem +#+BEGIN_EXAMPLE +zsh: event not found: ~ +zsh: event not found: \ +#+END_EXAMPLE + +*** Root Cause +Zsh history expansion interfering with command-line arguments containing special characters. + +*** Solution +Used single quotes instead of double quotes for SBCL command-line arguments to prevent shell interpretation. + +* Current Project Status + +** ✅ Completed Features +- [X] Basic web server running on localhost:8080 +- [X] Main page with station status display +- [X] Admin dashboard with placeholder controls +- [X] Web player interface (UI only) +- [X] JSON API endpoint (/api/status) +- [X] Hacker-themed consistent styling +- [X] Proper error handling and server management +- [X] Git upstream remote configuration + +** 🎯 Current Capabilities +- Web server starts/stops cleanly +- All routes functional and accessible +- HTML generation working correctly +- JSON API returning structured data +- Responsive web interface +- Server management functions exported + +** 📋 Next Steps (Not Implemented) +- Database integration (when MITO alternative chosen) +- Audio streaming backend (Liquidsoap integration) +- Icecast server integration +- File upload functionality +- Authentication system +- Real-time now-playing updates +- WebSocket integration for live updates + +* Technical Lessons Learned + +** Dependency Management +- Always verify dependency availability in target package manager +- Prefer widely-adopted, stable libraries over newer alternatives +- Keep dependency list minimal for initial implementation + +** Common Lisp Web Development +- Hunchentoot provides robust foundation for web applications +- Spinneret works best with direct macro usage, not through helper functions +- HTML generation should be kept simple and direct + +** Error Handling Strategy +- Compilation warnings often indicate runtime issues +- Test each component incrementally +- Use REPL for interactive debugging and testing + +** Development Workflow +- Start with minimal working version +- Add complexity incrementally +- Test each change immediately +- Keep fallback options for critical dependencies + +* File Structure Summary + +#+BEGIN_EXAMPLE +asteroid/ +├── asteroid.asd # System definition (minimal dependencies) +├── asteroid.lisp # Main web server implementation +├── app-utils.lisp # Utility functions +├── design.org # Original project specification +├── test-server.lisp # Server testing script +├── project-summary.org # This document +├── Makefile # Build configuration +└── LICENSE # AGPL v3 license +#+END_EXAMPLE + +* 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. + +The implementation is ready for the next development phase: integrating audio streaming components and database functionality. diff --git a/test-server.lisp b/test-server.lisp new file mode 100644 index 0000000..0e21c3d --- /dev/null +++ b/test-server.lisp @@ -0,0 +1,28 @@ +;; Test script for Asteroid Radio server +(format t "Loading dependencies...~%") +(ql:quickload '(:hunchentoot :spinneret :cl-json)) + +(format t "Loading Asteroid Radio...~%") +(load "asteroid.asd") +(asdf:load-system :asteroid) + +;; Start server in non-blocking mode +(format t "Starting server...~%") +(asteroid:start-server) + +(format t "Testing API endpoint...~%") +;; Give server a moment to start +(sleep 2) + +(format t "Server should now be running on http://localhost:8080~%") +(format t "Try visiting:~%") +(format t " - http://localhost:8080/ (main page)~%") +(format t " - http://localhost:8080/admin (admin dashboard)~%") +(format t " - http://localhost:8080/player (web player)~%") +(format t " - http://localhost:8080/api/status (API status)~%") + +(format t "~%Press Enter to stop the server...~%") +(read-line) + +(asteroid:stop-server) +(format t "Test complete.~%")