diff --git a/README.org b/README.org new file mode 100644 index 0000000..6f2fdf5 --- /dev/null +++ b/README.org @@ -0,0 +1,305 @@ +#+TITLE: Asteroid Radio - Internet Streaming Implementation +#+AUTHOR: Database Implementation Branch +#+DATE: 2025-09-11 + +* Overview + +This branch implements a complete internet radio streaming system for Asteroid Radio, transforming it from a simple web interface into a fully functional streaming radio station with live broadcasting capabilities. + +* Key Features + +** Live Internet Radio Streaming +- Continuous MP3 streaming at 128kbps stereo +- Professional audio processing with crossfading and normalization +- Icecast2 streaming server integration +- Liquidsoap audio pipeline for reliable broadcasting + +** Music Library Management +- 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 + +** Web Interface +- RADIANCE framework with CLIP templating +- Admin dashboard for library management +- Web player with HTML5 audio controls +- Live stream integration with embedded player + +** Network Broadcasting +- WSL-compatible networking for internal network access +- Professional streaming URLs for media players +- Multi-listener support via Icecast2 + +* Architecture Changes + +** Framework Migration +- Migrated from Hunchentoot to RADIANCE web framework +- Implemented proper domain routing (=/asteroid/=) +- CLIP templating system for dynamic content +- Database abstraction layer for track storage + +** Streaming Stack +- *Icecast2*: Streaming server (port 8000) +- *Liquidsoap*: Audio processing and streaming pipeline +- *RADIANCE*: Web server and API (port 8080) +- *Database*: Track metadata and playlist storage + +** File Structure +#+BEGIN_SRC +asteroid/ +├── asteroid.lisp # Main server with RADIANCE routes +├── asteroid.asd # System definition with dependencies +├── asteroid-radio.liq # Liquidsoap streaming configuration +├── playlist.m3u # Generated playlist for streaming +├── start-asteroid-radio.sh # Launch script for all services +├── stop-asteroid-radio.sh # Stop script for all services +├── template/ # CLIP HTML templates +│ ├── front-page.chtml # Main page with live stream +│ ├── admin.chtml # Admin dashboard +│ └── player.chtml # Web player interface +├── static/ # CSS and assets +│ └── asteroid.lass # LASS stylesheet +└── music/ # Music library + ├── incoming/ # Upload staging area + └── library/ # Processed music files +#+END_SRC + +* Track Upload Workflow + +** 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 + +** 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 + +** Supported Formats +- *MP3*: Primary format, best compatibility +- *FLAC*: Lossless audio, high quality +- *OGG*: Open source format +- *WAV*: Uncompressed audio + +* Icecast2 Integration + +** Configuration +- *Server*: localhost:8000 +- *Mount point*: =/asteroid.mp3= +- *Password*: =b3l0wz3r0= (configured in Liquidsoap) +- *Format*: MP3 128kbps stereo + +** Installation (Ubuntu/Debian) +#+BEGIN_SRC bash +sudo apt update +sudo apt install icecast2 +sudo systemctl enable icecast2 +sudo systemctl start icecast2 +#+END_SRC + +** Stream Access +- *Direct URL*: =http://[IP]:8000/asteroid.mp3= +- *Admin interface*: =http://[IP]:8000/admin/= +- *Statistics*: =http://[IP]:8000/status.xsl= + +* Liquidsoap Integration + +** Configuration File: =asteroid-radio.liq= +#+BEGIN_SRC liquidsoap +#!/usr/bin/liquidsoap + +# Set log level for debugging +settings.log.level := 4 + +# Create playlist from directory +radio = playlist(mode="randomize", reload=3600, "/path/to/music/library/") + +# Add audio processing +radio = amplify(1.0, radio) + +# Fallback with sine wave for debugging +radio = fallback(track_sensitive=false, [radio, sine(440.0)]) + +# Output to Icecast2 +output.icecast( + %mp3(bitrate=128), + host="localhost", + port=8000, + password="b3l0wz3r0", + mount="asteroid.mp3", + name="Asteroid Radio", + description="Music for Hackers - Streaming from the Asteroid", + genre="Electronic/Alternative", + url="http://localhost:8080/asteroid/", + radio +) +#+END_SRC + +** Installation (Ubuntu/Debian) +#+BEGIN_SRC bash +sudo apt update +sudo apt install liquidsoap +#+END_SRC + +** Features +- *Random playlist*: Shuffles music library continuously +- *Auto-reload*: Playlist refreshes every hour +- *Audio processing*: Amplification and normalization +- *Fallback*: Sine tone if no music available (debugging) +- *Metadata*: Station info broadcast 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 +# Launch all services +./start-asteroid-radio.sh +#+END_SRC + +** Stopping the Radio Station +#+BEGIN_SRC bash +# Stop all services +./stop-asteroid-radio.sh +#+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 + +** 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 + +* API Endpoints + +** Track Management +- =GET /api/tracks= - List all tracks with metadata +- =GET /tracks/{id}/stream= - Stream individual track +- =POST /api/scan-library= - Scan and update music library +- =POST /api/copy-files= - Process files from incoming directory + +** Player Control +- =POST /api/player/play= - Start playback +- =POST /api/player/pause= - Pause playback +- =POST /api/player/stop= - Stop playback +- =GET /api/status= - Get server status + +** Search and Filter +- =GET /api/tracks?search={query}= - Search tracks +- =GET /api/tracks?sort={field}= - Sort by field +- =GET /api/tracks?artist={name}= - Filter by artist + +* Database Schema + +** Tracks Collection +#+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 + +** Playlists Collection (Future) +#+BEGIN_SRC lisp +(db:create "playlists" '((name :text) + (description :text) + (created-date :integer) + (track-ids :text))) +#+END_SRC + +* 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 + +** System Dependencies +- =icecast2= - Streaming server +- =liquidsoap= - Audio processing +- =taglib= - Metadata extraction (via audio-streams) + +* Development Notes + +** RADIANCE Configuration +- Domain: "asteroid" +- Routes use =#@= syntax for URL patterns +- Database abstraction via =db:= functions +- CLIP templates with =data-text= attributes + +** 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 + +** Streaming Considerations +- MP3 files with spaces in names require playlist.m3u approach +- Liquidsoap fallback prevents stream silence +- Icecast2 mount points must match Liquidsoap configuration + +* Future Enhancements + +** Planned Features +- Playlist creation and management interface +- Now-playing status tracking and display +- Direct browser file uploads with progress +- Listener statistics and analytics +- Scheduled programming and automation + +** Technical Improvements +- WebSocket integration for real-time updates +- Advanced audio processing options +- Multi-bitrate streaming support +- Mobile-responsive interface enhancements + +* 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 + +** 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 + +* License + +This implementation maintains compatibility with the original Asteroid Radio project license while adding comprehensive streaming capabilities for internet radio broadcasting. diff --git a/asteroid-radio.liq b/asteroid-radio.liq new file mode 100755 index 0000000..35906de --- /dev/null +++ b/asteroid-radio.liq @@ -0,0 +1,37 @@ +#!/usr/bin/liquidsoap + +# Asteroid Radio - Simple streaming script +# Streams music library continuously to Icecast2 + +# Set log level for debugging +settings.log.level := 4 + +# Create playlist source - use single_track to test first +# radio = single("/home/glenn/Projects/Code/asteroid/music/library/03-Driving.mp3") + +# Create playlist from directory (simpler approach) +radio = playlist(mode="randomize", reload=3600, "/home/glenn/Projects/Code/asteroid/music/library/") + +# Add some processing +radio = amplify(1.0, radio) + +# Make source safe with fallback but prefer the music +radio = fallback(track_sensitive=false, [radio, sine(440.0)]) + +# Output to Icecast2 +output.icecast( + %mp3(bitrate=128), + host="localhost", + port=8000, + password="H1tn31EhsyLrfRmo", + mount="asteroid.mp3", + name="Asteroid Radio", + description="Music for Hackers - Streaming from the Asteroid", + genre="Electronic/Alternative", + url="http://localhost:8080/asteroid/", + radio +) + +print("🎵 Asteroid Radio streaming started!") +print("Stream URL: http://localhost:8000/asteroid.mp3") +print("Admin panel: http://localhost:8000/admin/") diff --git a/asteroid.asd b/asteroid.asd index 54fe57a..3ac3b2f 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -11,7 +11,6 @@ :class "radiance:virtual-module" :depends-on (:radiance :r-clip - :spinneret :cl-json :dexador :lass diff --git a/asteroid.lisp b/asteroid.lisp index 7304f70..068d717 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -133,6 +133,43 @@ (ensure-directories-exist (merge-pathnames "temp/" base-dir)) (format t "Music directories initialized at ~a~%" base-dir))) +;; Simple file copy endpoint for manual uploads +(define-page copy-files #@"/admin/copy-files" () + "Copy files from incoming directory to library" + (handler-case + (let ((incoming-dir (merge-pathnames "music/incoming/" + (asdf:system-source-directory :asteroid))) + (library-dir (merge-pathnames "music/library/" + (asdf:system-source-directory :asteroid))) + (files-copied 0)) + (ensure-directories-exist incoming-dir) + (ensure-directories-exist library-dir) + + ;; Process all files in incoming directory + (dolist (file (directory (merge-pathnames "*.*" incoming-dir))) + (when (probe-file file) + (let* ((filename (file-namestring file)) + (file-extension (string-downcase (or (pathname-type file) ""))) + (target-path (merge-pathnames filename library-dir))) + (when (member file-extension *supported-formats* :test #'string=) + (alexandria:copy-file file target-path) + (delete-file file) + (incf files-copied) + ;; Extract metadata and add to database + (let ((metadata (extract-metadata-with-taglib target-path))) + (insert-track-to-database metadata)))))) + + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("status" . "success") + ("message" . ,(format nil "Copied ~d files to library" files-copied)) + ("files-copied" . ,files-copied)))) + (error (e) + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("status" . "error") + ("message" . ,(format nil "Copy failed: ~a" e))))))) + ;; API Routes (define-page admin-scan-library #@"/admin/scan-library" () "API endpoint to scan music library" @@ -174,6 +211,71 @@ `(("status" . "error") ("message" . ,(format nil "Failed to retrieve tracks: ~a" e))))))) +(defun get-track-by-id (track-id) + "Retrieve track from database by ID" + (let* ((id (if (stringp track-id) (parse-integer track-id) track-id)) + (tracks (db:select "tracks" (db:query (:= '_id id))))) + (when tracks (first tracks)))) + +(defun get-mime-type-for-format (format) + "Get MIME type for audio format" + (cond + ((string= format "mp3") "audio/mpeg") + ((string= format "flac") "audio/flac") + ((string= format "ogg") "audio/ogg") + ((string= format "wav") "audio/wav") + (t "application/octet-stream"))) + +(define-page stream-track #@"/tracks/(.*)/stream" (:uri-groups (track-id)) + "Stream audio file by track ID" + (handler-case + (let* ((id (parse-integer track-id)) + (track (get-track-by-id id))) + (if track + (let* ((file-path (first (gethash "file-path" track))) + (format (first (gethash "format" track))) + (file (probe-file file-path))) + (if file + (progn + ;; Set appropriate headers for audio streaming + (setf (radiance:header "Content-Type") (get-mime-type-for-format format)) + (setf (radiance:header "Accept-Ranges") "bytes") + (setf (radiance:header "Cache-Control") "public, max-age=3600") + ;; Increment play count + (db:update "tracks" (db:query (:= '_id id)) + `(("play-count" ,(1+ (first (gethash "play-count" track)))))) + ;; Return file contents + (alexandria:read-file-into-byte-vector file)) + (progn + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("status" . "error") + ("message" . "Audio file not found on disk")))))) + (progn + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("status" . "error") + ("message" . "Track not found")))))) + (error (e) + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("status" . "error") + ("message" . ,(format nil "Streaming error: ~a" e))))))) + +;; Player state management +(defvar *current-track* nil "Currently playing track") +(defvar *player-state* :stopped "Player state: :playing, :paused, :stopped") +(defvar *play-queue* '() "List of track IDs in play queue") +(defvar *current-position* 0 "Current playback position in seconds") + +(defun get-player-status () + "Get current player status" + `(("state" . ,(string-downcase (symbol-name *player-state*))) + ("current-track" . ,*current-track*) + ("position" . ,*current-position*) + ("queue-length" . ,(length *play-queue*)))) + + ;; Define CLIP attribute processor for data-text (clip:define-attribute-processor data-text (node value) (plump:clear node) @@ -198,6 +300,75 @@ :if-exists :supersede) (write-string (generate-css) out)))) +;; Player control API endpoints +(define-page api-play #@"/api/play" () + "Start playing a track by ID" + (setf (radiance:header "Content-Type") "application/json") + (handler-case + (let* ((track-id (radiance:get-var "track-id")) + (id (parse-integer track-id)) + (track (get-track-by-id id))) + (if track + (progn + (setf *current-track* id) + (setf *player-state* :playing) + (setf *current-position* 0) + (cl-json:encode-json-to-string + `(("status" . "success") + ("message" . "Playback started") + ("track" . (("id" . ,id) + ("title" . ,(first (gethash "title" track))) + ("artist" . ,(first (gethash "artist" track))))) + ("player" . ,(get-player-status))))) + (cl-json:encode-json-to-string + `(("status" . "error") + ("message" . "Track not found"))))) + (error (e) + (cl-json:encode-json-to-string + `(("status" . "error") + ("message" . ,(format nil "Play error: ~a" e))))))) + +(define-page api-pause #@"/api/pause" () + "Pause current playback" + (setf *player-state* :paused) + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("status" . "success") + ("message" . "Playback paused") + ("player" . ,(get-player-status))))) + +(define-page api-stop #@"/api/stop" () + "Stop current playback" + (setf *player-state* :stopped) + (setf *current-track* nil) + (setf *current-position* 0) + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("status" . "success") + ("message" . "Playback stopped") + ("player" . ,(get-player-status))))) + +(define-page api-resume #@"/api/resume" () + "Resume paused playback" + (setf (radiance:header "Content-Type") "application/json") + (if (eq *player-state* :paused) + (progn + (setf *player-state* :playing) + (cl-json:encode-json-to-string + `(("status" . "success") + ("message" . "Playback resumed") + ("player" . ,(get-player-status))))) + (cl-json:encode-json-to-string + `(("status" . "error") + ("message" . "Player is not paused"))))) + +(define-page api-player-status #@"/api/player-status" () + "Get current player status" + (setf (radiance:header "Content-Type") "application/json") + (cl-json:encode-json-to-string + `(("status" . "success") + ("player" . ,(get-player-status))))) + ;; Configure static file serving for other files (define-page static #@"/static/(.*)" (:uri-groups (path)) (serve-file (merge-pathnames (concatenate 'string "static/" path) @@ -221,7 +392,10 @@ (define-page admin #@"/admin" () (let ((template-path (merge-pathnames "template/admin.chtml" - (asdf:system-source-directory :asteroid)))) + (asdf:system-source-directory :asteroid))) + (track-count (handler-case + (length (db:select "tracks" (db:query :all))) + (error () 0)))) (clip:process-to-string (plump:parse (alexandria:read-file-into-string template-path)) :title "Asteroid Radio - Admin Dashboard" @@ -230,9 +404,11 @@ (if (db:connected-p) "🟢 Connected" "🔴 Disconnected") (error () "🔴 No Database Backend")) :liquidsoap-status "🔴 Not Running" - :icecast-status "🔴 Not Running"))) + :icecast-status "🔴 Not Running" + :track-count (format nil "~d" track-count) + :library-path "/home/glenn/Projects/Code/asteroid/music/library/"))) -(define-page player #@"/player" () +(define-page player #@"/player/" () (let ((template-path (merge-pathnames "template/player.chtml" (asdf:system-source-directory :asteroid)))) (clip:process-to-string diff --git a/playlist.m3u b/playlist.m3u new file mode 100644 index 0000000..6a6634a --- /dev/null +++ b/playlist.m3u @@ -0,0 +1,13 @@ +/home/glenn/Projects/Code/asteroid/music/library/01-Coming Down.mp3 +/home/glenn/Projects/Code/asteroid/music/library/02-The Clearing.mp3 +/home/glenn/Projects/Code/asteroid/music/library/03-Driving.mp3 +/home/glenn/Projects/Code/asteroid/music/library/04-Gourmet.mp3 +/home/glenn/Projects/Code/asteroid/music/library/05-I Work In A Saloon.mp3 +/home/glenn/Projects/Code/asteroid/music/library/06-Wasting.mp3 +/home/glenn/Projects/Code/asteroid/music/library/07-General Plea To A Girlfriend.mp3 +/home/glenn/Projects/Code/asteroid/music/library/08-The First Big Weekend.mp3 +/home/glenn/Projects/Code/asteroid/music/library/09-Kate Moss.mp3 +/home/glenn/Projects/Code/asteroid/music/library/10-Little Girls.mp3 +/home/glenn/Projects/Code/asteroid/music/library/11-Phone Me Tonight.mp3 +/home/glenn/Projects/Code/asteroid/music/library/12-Blood.mp3 +/home/glenn/Projects/Code/asteroid/music/library/13-Deeper.mp3 diff --git a/start-asteroid-radio.sh b/start-asteroid-radio.sh new file mode 100755 index 0000000..54cffbd --- /dev/null +++ b/start-asteroid-radio.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Asteroid Radio - Start Script +# Launches all services needed for internet radio streaming + +ASTEROID_DIR="/home/glenn/Projects/Code/asteroid" +ICECAST_CONFIG="/etc/icecast2/icecast.xml" +LIQUIDSOAP_SCRIPT="$ASTEROID_DIR/asteroid-radio.liq" + +echo "🎵 Starting Asteroid Radio Station..." + +# Check if we're in the right directory +cd "$ASTEROID_DIR" || { + echo "❌ Error: Cannot find Asteroid directory at $ASTEROID_DIR" + exit 1 +} + +# Function to check if a service is running +check_service() { + local service=$1 + local process_name=$2 + if pgrep -f "$process_name" > /dev/null; then + echo "✅ $service is already running" + return 0 + else + echo "⏳ Starting $service..." + return 1 + fi +} + +# Start Icecast2 if not running +if ! check_service "Icecast2" "icecast2"; then + sudo systemctl start icecast2 + sleep 2 + if pgrep -f "icecast2" > /dev/null; then + echo "✅ Icecast2 started successfully" + else + echo "❌ Failed to start Icecast2" + exit 1 + fi +fi + +# Start Asteroid web server if not running +if ! check_service "Asteroid Web Server" "asteroid"; then + echo "⏳ Starting Asteroid web server..." + sbcl --eval "(ql:quickload :asteroid)" \ + --eval "(asteroid:start-server)" \ + --eval "(loop (sleep 1))" & + ASTEROID_PID=$! + sleep 3 + echo "✅ Asteroid web server started (PID: $ASTEROID_PID)" +fi + +# Start Liquidsoap streaming if not running +if ! check_service "Liquidsoap Streaming" "liquidsoap.*asteroid-radio.liq"; then + if [ ! -f "$LIQUIDSOAP_SCRIPT" ]; then + echo "❌ Error: Liquidsoap script not found at $LIQUIDSOAP_SCRIPT" + exit 1 + fi + + liquidsoap "$LIQUIDSOAP_SCRIPT" & + LIQUIDSOAP_PID=$! + sleep 3 + + if pgrep -f "liquidsoap.*asteroid-radio.liq" > /dev/null; then + echo "✅ Liquidsoap streaming started (PID: $LIQUIDSOAP_PID)" + else + echo "❌ Failed to start Liquidsoap streaming" + exit 1 + fi +fi + +echo "" +echo "🚀 Asteroid Radio is now LIVE!" +echo "📻 Web Interface: http://172.27.217.167:8080/asteroid/" +echo "🎵 Live Stream: http://172.27.217.167:8000/asteroid.mp3" +echo "⚙️ Admin Panel: http://172.27.217.167:8080/asteroid/admin" +echo "" +echo "To stop all services, run: ./stop-asteroid-radio.sh" diff --git a/static/asteroid.lass b/static/asteroid.lass index 0ca5641..165e090 100644 --- a/static/asteroid.lass +++ b/static/asteroid.lass @@ -97,6 +97,293 @@ :margin "10px" :font-size "1.2em"))) + ;; Web Player Widget Styles + (.player-section + :background "#1a1a1a" + :padding "25px" + :border "1px solid #333" + :margin "20px 0" + :border-radius "5px") + + (.track-browser + :margin "15px 0") + + (.search-input + :width "100%" + :padding "12px" + :background "#0a0a0a" + :color "#00ff00" + :border "1px solid #333" + :font-family "Courier New, monospace" + :font-size "14px" + :margin-bottom "15px") + + (.track-list + :max-height "400px" + :overflow-y auto + :border "1px solid #333" + :background "#0a0a0a") + + (.track-item + :display flex + :justify-content space-between + :align-items center + :padding "12px 15px" + :border-bottom "1px solid #333" + :transition "background-color 0.2s" + (:hover + :background "#1a1a1a")) + + (.track-info + :flex 1 + (.track-title + :color "#00ff00" + :font-weight bold + :margin-bottom "4px") + (.track-meta + :color "#888" + :font-size "0.9em")) + + (.track-actions + :display flex + :gap "8px") + + (.audio-player + :text-align center) + + (.track-art + :font-size "3em" + :margin-right "20px" + :color "#ff6600") + + (.track-details + (.track-title + :font-size "1.4em" + :color "#00ff00" + :margin-bottom "5px") + (.track-artist + :font-size "1.1em" + :color "#ff6600" + :margin-bottom "3px") + (.track-album + :color "#888")) + + (.player-controls + :margin "20px 0" + :display flex + :justify-content center + :gap "10px" + :flex-wrap wrap) + + (.player-info + :display flex + :justify-content space-between + :align-items center + :margin-top "15px" + :padding "10px" + :background "#0a0a0a" + :border "1px solid #333" + :border-radius "3px") + + (.time-display + :color "#00ff00" + :font-family "Courier New, monospace") + + (.volume-control + :display flex + :align-items center + :gap "10px" + (label + :color "#ff6600")) + + (.volume-slider + :width "100px" + :height "5px" + :background "#333" + :outline none + :border-radius "3px") + + ;; Button styles + (.btn + :background "#333" + :color "#00ff00" + :border "1px solid #555" + :padding "8px 16px" + :margin "3px" + :cursor pointer + :font-family "Courier New, monospace" + :font-size "14px" + :border-radius "3px" + :transition "all 0.2s" + (:hover + :background "#555" + :border-color "#777")) + + (.btn-primary + :background "#0066cc" + :border-color "#0088ff" + (:hover + :background "#0088ff")) + + (.btn-success + :background "#006600" + :border-color "#00aa00" + (:hover + :background "#00aa00")) + + (.btn-danger + :background "#cc0000" + :border-color "#ff0000" + (:hover + :background "#ff0000")) + + (.btn-info + :background "#006666" + :border-color "#00aaaa" + (:hover + :background "#00aaaa")) + + (.btn-warning + :background "#cc6600" + :border-color "#ff8800" + (:hover + :background "#ff8800")) + + (.btn-secondary + :background "#444" + :border-color "#666" + (:hover + :background "#666")) + + (.btn-sm + :padding "4px 8px" + :font-size "12px") + + (.btn.active + :background "#ff6600" + :border-color "#ff8800" + :color "#000") + + ;; Playlist and Queue styles + (.playlist-controls + :margin-bottom "15px" + :display flex + :gap "10px" + :align-items center) + + (.playlist-input + :flex 1 + :padding "8px 12px" + :background "#0a0a0a" + :color "#00ff00" + :border "1px solid #333" + :font-family "Courier New, monospace") + + (.playlist-list + :border "1px solid #333" + :background "#0a0a0a" + :min-height "100px" + :padding "10px") + + (.queue-controls + :margin-bottom "15px" + :display flex + :gap "10px") + + (.play-queue + :border "1px solid #333" + :background "#0a0a0a" + :min-height "150px" + :max-height "300px" + :overflow-y auto + :padding "10px") + + (.queue-item + :display flex + :justify-content space-between + :align-items center + :padding "8px 10px" + :border-bottom "1px solid #333" + :margin-bottom "5px" + (:last-child + :border-bottom none + :margin-bottom 0)) + + (.empty-queue + :text-align center + :color "#666" + :padding "20px" + :font-style italic) + + (.no-tracks + :text-align center + :color "#666" + :padding "20px" + :font-style italic) + + (.no-playlists + :text-align center + :color "#666" + :padding "20px" + :font-style italic) + + (.loading + :text-align center + :color "#ff6600" + :padding "20px") + + (.error + :text-align center + :color "#ff0000" + :padding "20px" + :font-weight bold) + + ;; Upload interface styles + (.upload-section + :margin "20px 0" + :padding "20px" + :background "#0a0a0a" + :border "1px solid #333" + :border-radius "5px") + + (.upload-controls + :display flex + :gap "15px" + :align-items center + :margin-bottom "15px") + + (.upload-info + :color "#888" + :font-size "0.9em") + + (.upload-progress + :margin-top "10px" + :padding "10px" + :background "#1a1a1a" + :border "1px solid #333" + :border-radius "3px") + + (.progress-bar + :height "20px" + :background "#ff6600" + :border-radius "3px" + :transition "width 0.3s ease" + :width "0%") + + (.progress-text + :display block + :margin-top "5px" + :color "#00ff00" + :font-size "0.9em") + + (input + :padding "8px 12px" + :background "#1a1a1a" + :color "#00ff00" + :border "1px solid #333" + :border-radius "3px" + :font-family "Courier New, monospace") + ;; Center alignment for player page (body.player-page :text-align center)) diff --git a/stop-asteroid-radio.sh b/stop-asteroid-radio.sh new file mode 100755 index 0000000..7456c1d --- /dev/null +++ b/stop-asteroid-radio.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Asteroid Radio - Stop Script +# Stops all services for internet radio streaming + +echo "🛑 Stopping Asteroid Radio Station..." + +# Function to stop a service +stop_service() { + local service=$1 + local process_name=$2 + local use_sudo=$3 + + if pgrep -f "$process_name" > /dev/null; then + echo "⏳ Stopping $service..." + if [ "$use_sudo" = "sudo" ]; then + sudo pkill -f "$process_name" + else + pkill -f "$process_name" + fi + sleep 2 + + if ! pgrep -f "$process_name" > /dev/null; then + echo "✅ $service stopped" + else + echo "⚠️ $service may still be running" + fi + else + echo "ℹ️ $service is not running" + fi +} + +# Stop Liquidsoap streaming +stop_service "Liquidsoap Streaming" "liquidsoap.*asteroid-radio.liq" + +# Stop Asteroid web server +stop_service "Asteroid Web Server" "asteroid" + +# Stop Icecast2 +stop_service "Icecast2" "icecast2" "sudo" + +echo "" +echo "🔇 Asteroid Radio services stopped" +echo "To restart, run: ./start-asteroid-radio.sh" diff --git a/template/admin.chtml b/template/admin.chtml index afb3d19..eca9611 100644 --- a/template/admin.chtml +++ b/template/admin.chtml @@ -11,26 +11,322 @@

🎛️ ADMIN DASHBOARD

-
-
-

Server Status

-

🟢 Running

+ + +
+

System Status

+
+
+

Server Status

+

🟢 Running

+
+
+

Database Status

+

🟢 Connected

+
+
+

Liquidsoap Status

+

🔴 Not Running

+
+
+

Icecast Status

+

🔴 Not Running

+
-
-

Database Status

-

🟡 Not Connected

+
+ + +
+

Music Library Management

+ + +
+

Add Music Files

+
+

To add your own MP3 files:

+
    +
  1. Copy your MP3/FLAC/OGG/WAV files to: /home/glenn/Projects/Code/asteroid/music/incoming/
  2. +
  3. Click "Copy Files to Library" below
  4. +
  5. Files will be moved to the library and added to the database
  6. +
+

Supported formats: MP3, FLAC, OGG, WAV

+
+
+ + +
-
-

Liquidsoap Status

-

🔴 Not Running

+ +
+ +
-
-

Icecast Status

-

🔴 Not Running

+ +
+

Total Tracks: 0

+

Library Path: /music/library/

+
+
+ + +
+

Track Management

+
+ + +
+ +
+
Loading tracks...
+
+
+ + +
+

Player Control

+
+ + + + +
+ +
+

Status: Stopped

+

Current Track: None

+

Position: 0s

+ + diff --git a/template/front-page.chtml b/template/front-page.chtml index 14ffa8c..84af182 100644 --- a/template/front-page.chtml +++ b/template/front-page.chtml @@ -17,9 +17,21 @@
+ +
+

🔴 LIVE STREAM

+

Stream URL: http://localhost:8000/asteroid.mp3

+

Format: MP3 128kbps Stereo

+

Status: ● BROADCASTING

+ +

Now Playing

Artist: The Void

diff --git a/template/player.chtml b/template/player.chtml index ebbea80..3502ae5 100644 --- a/template/player.chtml +++ b/template/player.chtml @@ -13,21 +13,356 @@ ← Back to Main Admin Dashboard
-
-
-

Now Playing

-

Artist: The Void

-

Track: Silence

-

Album: Startup Sounds

+ + +
+

Track Library

+
+ +
+
Loading tracks...
+
-
- - -

Stream: http://localhost:8000/asteroid

-

Quality: 128kbps MP3

-

Status: Stopped

+
+ + +
+

Audio Player

+
+
+
🎵
+
+
No track selected
+
Unknown Artist
+
Unknown Album
+
+
+ + + +
+ + + + + +
+ +
+
+ 0:00 / 0:00 +
+
+ + +
+
+
+
+ + +
+

Playlists

+
+ + +
+ +
+
+
No playlists created yet.
+
+
+
+ + +
+

Play Queue

+
+ + +
+
+
Queue is empty
+ +