Add Liquidsoap DJ controls via telnet integration
- Add usocket and cl-ppcre dependencies to asteroid.asd - Implement telnet communication functions for Liquidsoap control - Add 5 new DJ control API endpoints: * /api/asteroid/dj/skip - Skip to next track (admin only) * /api/asteroid/dj/reload-playlist - Reload music library (admin only) * /api/asteroid/dj/queue - Queue specific track (admin only) * /api/asteroid/dj/queue-status - View request queue (admin only) * /api/asteroid/dj/metadata - Enhanced metadata (authenticated users) - Use correct Liquidsoap commands (music.skip, music.reload) - Implement proper role-based access control and error handling - All endpoints return JSON responses with appropriate HTTP status codes Features tested and working: - Real-time track skipping with natural streaming delay - Enhanced metadata parsing from Liquidsoap - Queue monitoring and management - Secure admin-only access to DJ controls
This commit is contained in:
parent
e432484424
commit
69b6a9b681
|
|
@ -0,0 +1,53 @@
|
|||
# Refactor API endpoints to use Radiance's define-api macro
|
||||
|
||||
## Summary
|
||||
Refactors all API endpoints from manual `define-page` implementations to use Radiance's built-in `define-api` macro, following framework best practices and conventions.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### API Endpoint Refactoring
|
||||
- **Converted 15+ API endpoints** from `define-page` to `define-api` format
|
||||
- **Standardized URL patterns** to `/api/<module>/<endpoint>` structure
|
||||
- **Implemented proper JSON responses** using `api-output` instead of manual JSON encoding
|
||||
- **Added comprehensive error handling** with appropriate HTTP status codes (404, 500, 403)
|
||||
|
||||
### Key Technical Improvements
|
||||
- **Automatic JSON formatting**: Leverages Radiance's built-in JSON API format
|
||||
- **Parameter handling**: Uses lambda-list parameters instead of manual POST/GET parsing
|
||||
- **Consistent error responses**: Standardized error format across all endpoints
|
||||
- **Framework compliance**: Follows Radiance's recommended API patterns
|
||||
|
||||
### Endpoints Converted
|
||||
- **Admin APIs**: `asteroid/admin/scan-library`, `asteroid/admin/tracks`
|
||||
- **Playlist APIs**: `asteroid/playlists`, `asteroid/playlists/create`, `asteroid/playlists/add-track`, `asteroid/playlists/get`
|
||||
- **Track APIs**: `asteroid/tracks`
|
||||
- **Player APIs**: `asteroid/player/play`, `asteroid/player/pause`, `asteroid/player/stop`, `asteroid/player/resume`, `asteroid/player/status`
|
||||
- **Status APIs**: `asteroid/status`, `asteroid/icecast-status`, `asteroid/auth-status`
|
||||
|
||||
### Added Comprehensive Test Suite
|
||||
- **18 automated tests** covering all API endpoints
|
||||
- **Response format validation** ensuring proper JSON output
|
||||
- **Authentication testing** for protected endpoints
|
||||
- **Static file serving verification**
|
||||
- **HTML page loading tests**
|
||||
|
||||
## Testing
|
||||
- ✅ **All 18 tests pass** via `./test-server.sh`
|
||||
- ✅ **Proper JSON responses** instead of S-expressions
|
||||
- ✅ **Authentication integration** working correctly
|
||||
- ✅ **Error handling** with appropriate HTTP status codes
|
||||
- ✅ **Backward compatibility** maintained for existing functionality
|
||||
|
||||
## Files Changed
|
||||
- `asteroid.lisp` - Main API endpoint refactoring
|
||||
- `auth-routes.lisp` - Authentication API endpoints (if applicable)
|
||||
- Added comprehensive test suite
|
||||
|
||||
## Impact
|
||||
- **Cleaner codebase** following Radiance framework conventions
|
||||
- **Better maintainability** with standardized API patterns
|
||||
- **Improved error handling** and status code management
|
||||
- **Enhanced testing coverage** for all API functionality
|
||||
- **Foundation for frontend integration** with consistent JSON responses
|
||||
|
||||
This refactoring establishes a solid foundation for frontend development and maintains full backward compatibility while improving code quality and framework compliance.
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# Fix frontend JavaScript to work with define-api endpoints
|
||||
|
||||
## Summary
|
||||
Updates all frontend JavaScript files to properly work with the refactored `define-api` endpoints, including API path corrections and RADIANCE response format handling.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### API Endpoint Path Updates
|
||||
- **Fixed API paths** in `users.js` and `profile.js` from `/asteroid/api/` to `/api/asteroid/`
|
||||
- **Corrected endpoint references** to match new `define-api` URL structure
|
||||
- **Updated all fetch calls** to use consistent API path format
|
||||
|
||||
### RADIANCE API Response Handling
|
||||
- **Added response wrapper handling** for RADIANCE's `{status: 200, data: {...}}` format
|
||||
- **Implemented fallback logic** with `result.data || result` pattern
|
||||
- **Fixed icecast-status parsing** in both `front-page.js` and `player.js`
|
||||
- **Enhanced error handling** for API response processing
|
||||
|
||||
### JavaScript Fixes
|
||||
- **Fixed missing function declaration** in `player.js` (`loadTracks` function)
|
||||
- **Improved error handling** in track loading with proper user feedback
|
||||
- **Added debug logging** for API response troubleshooting
|
||||
- **Enhanced live metadata updates** for real-time track information
|
||||
|
||||
### Files Updated
|
||||
- **`static/js/front-page.js`** - Fixed icecast-status response parsing for live metadata
|
||||
- **`static/js/player.js`** - Fixed function declaration and RADIANCE response handling
|
||||
- **`static/js/users.js`** - Updated all API paths from `/asteroid/api/` to `/api/asteroid/`
|
||||
- **`static/js/profile.js`** - Updated all API paths from `/asteroid/api/` to `/api/asteroid/`
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### Live Metadata Integration
|
||||
- **Real-time track updates** now working correctly on front page
|
||||
- **Automatic refresh** every 10 seconds showing current playing track
|
||||
- **Proper parsing** of "Artist - Track" format from Icecast
|
||||
- **Fallback handling** for missing or malformed metadata
|
||||
|
||||
### Enhanced User Experience
|
||||
- **Consistent API communication** across all frontend components
|
||||
- **Better error messaging** when API calls fail
|
||||
- **Improved loading states** and user feedback
|
||||
- **Seamless integration** with authentication system
|
||||
|
||||
## Testing
|
||||
- ✅ **All 18 tests pass** via `./test-server.sh`
|
||||
- ✅ **Live metadata working** - Shows actual track names instead of "The Void - Silence"
|
||||
- ✅ **Frontend API calls successful** - All JavaScript fetch operations working
|
||||
- ✅ **User interface responsive** - All interactive elements functioning
|
||||
- ✅ **Authentication integration** - Proper handling of protected endpoints
|
||||
|
||||
## Technical Details
|
||||
- **RADIANCE API wrapper**: Properly handles `{status: 200, message: "Ok", data: {...}}` format
|
||||
- **Error resilience**: Graceful fallback when API responses are malformed
|
||||
- **Consistent patterns**: Standardized approach to API response handling across all files
|
||||
- **Debug support**: Added logging for troubleshooting API communication
|
||||
|
||||
## Impact
|
||||
- **Fully functional frontend** working with refactored backend APIs
|
||||
- **Live streaming metadata** displaying real track information
|
||||
- **Enhanced user experience** with proper error handling and feedback
|
||||
- **Maintainable codebase** with consistent API communication patterns
|
||||
|
||||
This update completes the frontend integration with the `define-api` refactoring, ensuring all user-facing functionality works seamlessly with the new backend API structure.
|
||||
|
|
@ -27,6 +27,8 @@
|
|||
:cl-fad
|
||||
:bordeaux-threads
|
||||
:drakma
|
||||
:usocket
|
||||
:cl-ppcre
|
||||
(:interface :auth)
|
||||
(:interface :database)
|
||||
(:interface :user))
|
||||
|
|
|
|||
158
asteroid.lisp
158
asteroid.lisp
|
|
@ -278,6 +278,68 @@
|
|||
("position" . ,*current-position*)
|
||||
("queue-length" . ,(length *play-queue*))))
|
||||
|
||||
;; Liquidsoap Telnet Control Functions
|
||||
(defun liquidsoap-telnet-command (command)
|
||||
"Send a command to Liquidsoap telnet server and return response"
|
||||
(handler-case
|
||||
(let ((socket (usocket:socket-connect "127.0.0.1" 1234)))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(format (usocket:socket-stream socket) "~A~%" command)
|
||||
(force-output (usocket:socket-stream socket))
|
||||
(let ((response-lines '())
|
||||
(line nil))
|
||||
;; Read all lines until "END"
|
||||
(loop while (and (setf line (read-line (usocket:socket-stream socket) nil))
|
||||
(not (string= line "END")))
|
||||
do (push line response-lines))
|
||||
(format nil "~{~A~%~}" (reverse response-lines))))
|
||||
(usocket:socket-close socket)))
|
||||
(error (e)
|
||||
(format t "Error communicating with Liquidsoap: ~A~%" e)
|
||||
nil)))
|
||||
|
||||
(defun liquidsoap-skip-track ()
|
||||
"Skip to the next track in Liquidsoap"
|
||||
(liquidsoap-telnet-command "music.skip"))
|
||||
|
||||
(defun liquidsoap-reload-playlist ()
|
||||
"Reload the playlist in Liquidsoap"
|
||||
(liquidsoap-telnet-command "music.reload"))
|
||||
|
||||
(defun liquidsoap-get-metadata ()
|
||||
"Get current track metadata from Liquidsoap"
|
||||
(let ((response (liquidsoap-telnet-command "output.icecast.metadata")))
|
||||
(when response
|
||||
(parse-liquidsoap-metadata response))))
|
||||
|
||||
(defun liquidsoap-get-queue ()
|
||||
"Get the current request queue from Liquidsoap"
|
||||
(liquidsoap-telnet-command "request.all"))
|
||||
|
||||
(defun liquidsoap-queue-track (file-path)
|
||||
"Queue a specific track in Liquidsoap"
|
||||
(liquidsoap-telnet-command (format nil "request.push ~A" file-path)))
|
||||
|
||||
(defun parse-liquidsoap-metadata (metadata-string)
|
||||
"Parse Liquidsoap metadata string into artist/title/album"
|
||||
(let ((artist "Unknown Artist")
|
||||
(title "Unknown Track")
|
||||
(album "Unknown Album"))
|
||||
(when metadata-string
|
||||
;; Parse the first track (most recent) from the metadata
|
||||
;; Format: --- 2 ---\nkey="value"\nkey="value"...
|
||||
(let ((lines (cl-ppcre:split "\\n" metadata-string)))
|
||||
(dolist (line lines)
|
||||
(cond
|
||||
((cl-ppcre:scan "^artist=" line)
|
||||
(setf artist (cl-ppcre:regex-replace "^artist=\"([^\"]+)\"" line "\\1")))
|
||||
((cl-ppcre:scan "^title=" line)
|
||||
(setf title (cl-ppcre:regex-replace "^title=\"([^\"]+)\"" line "\\1")))
|
||||
((cl-ppcre:scan "^album=" line)
|
||||
(setf album (cl-ppcre:regex-replace "^album=\"([^\"]+)\"" line "\\1")))))))
|
||||
(list :artist artist :title title :album album)))
|
||||
|
||||
|
||||
;; Define CLIP attribute processor for data-text
|
||||
(clip:define-attribute-processor data-text (node value)
|
||||
|
|
@ -361,6 +423,102 @@
|
|||
(api-output `(("status" . "success")
|
||||
("player" . ,(get-player-status)))))
|
||||
|
||||
;; DJ Control API Endpoints
|
||||
(define-api asteroid/dj/skip () ()
|
||||
"Skip to the next track via Liquidsoap telnet"
|
||||
(require-role :admin) ; Only admins can use DJ controls
|
||||
(handler-case
|
||||
(let ((result (liquidsoap-skip-track)))
|
||||
(if result
|
||||
(api-output `(("status" . "success")
|
||||
("message" . "Track skipped successfully")
|
||||
("liquidsoap-response" . ,result)))
|
||||
(api-output `(("status" . "error")
|
||||
("message" . "Failed to communicate with Liquidsoap"))
|
||||
:status 503)))
|
||||
(error (e)
|
||||
(api-output `(("status" . "error")
|
||||
("message" . ,(format nil "Skip error: ~a" e)))
|
||||
:status 500))))
|
||||
|
||||
(define-api asteroid/dj/reload-playlist () ()
|
||||
"Reload the playlist in Liquidsoap"
|
||||
(require-role :admin)
|
||||
(handler-case
|
||||
(let ((result (liquidsoap-reload-playlist)))
|
||||
(if result
|
||||
(api-output `(("status" . "success")
|
||||
("message" . "Playlist reloaded successfully")
|
||||
("liquidsoap-response" . ,result)))
|
||||
(api-output `(("status" . "error")
|
||||
("message" . "Failed to communicate with Liquidsoap"))
|
||||
:status 503)))
|
||||
(error (e)
|
||||
(api-output `(("status" . "error")
|
||||
("message" . ,(format nil "Reload error: ~a" e)))
|
||||
:status 500))))
|
||||
|
||||
(define-api asteroid/dj/queue (track-id) ()
|
||||
"Queue a specific track by ID"
|
||||
(require-role :admin)
|
||||
(handler-case
|
||||
(let* ((id (parse-integer track-id :junk-allowed t))
|
||||
(track (get-track-by-id id)))
|
||||
(if track
|
||||
(let* ((file-path (first (gethash "file-path" track)))
|
||||
(result (liquidsoap-queue-track file-path)))
|
||||
(if result
|
||||
(api-output `(("status" . "success")
|
||||
("message" . "Track queued successfully")
|
||||
("track" . (("id" . ,id)
|
||||
("title" . ,(first (gethash "title" track)))
|
||||
("artist" . ,(first (gethash "artist" track)))))
|
||||
("liquidsoap-response" . ,result)))
|
||||
(api-output `(("status" . "error")
|
||||
("message" . "Failed to queue track in Liquidsoap"))
|
||||
:status 503)))
|
||||
(api-output `(("status" . "error")
|
||||
("message" . "Track not found"))
|
||||
:status 404)))
|
||||
(error (e)
|
||||
(api-output `(("status" . "error")
|
||||
("message" . ,(format nil "Queue error: ~a" e)))
|
||||
:status 500))))
|
||||
|
||||
(define-api asteroid/dj/queue-status () ()
|
||||
"Get the current Liquidsoap request queue"
|
||||
(require-role :admin)
|
||||
(handler-case
|
||||
(let ((result (liquidsoap-get-queue)))
|
||||
(if result
|
||||
(api-output `(("status" . "success")
|
||||
("queue" . ,result)))
|
||||
(api-output `(("status" . "error")
|
||||
("message" . "Failed to get queue from Liquidsoap"))
|
||||
:status 503)))
|
||||
(error (e)
|
||||
(api-output `(("status" . "error")
|
||||
("message" . ,(format nil "Queue status error: ~a" e)))
|
||||
:status 500))))
|
||||
|
||||
(define-api asteroid/dj/metadata () ()
|
||||
"Get enhanced metadata directly from Liquidsoap"
|
||||
(require-authentication) ; Any authenticated user can see metadata
|
||||
(handler-case
|
||||
(let ((metadata (liquidsoap-get-metadata)))
|
||||
(if metadata
|
||||
(api-output `(("status" . "success")
|
||||
("metadata" . (("artist" . ,(getf metadata :artist))
|
||||
("title" . ,(getf metadata :title))
|
||||
("album" . ,(getf metadata :album))))))
|
||||
(api-output `(("status" . "error")
|
||||
("message" . "Failed to get metadata from Liquidsoap"))
|
||||
:status 503)))
|
||||
(error (e)
|
||||
(api-output `(("status" . "error")
|
||||
("message" . ,(format nil "Metadata error: ~a" e)))
|
||||
:status 500))))
|
||||
|
||||
;; Profile API Routes - TEMPORARILY COMMENTED OUT
|
||||
#|
|
||||
(define-page api-user-profile #@"/api/user/profile" ()
|
||||
|
|
|
|||
Loading…
Reference in New Issue