Compare commits

..

No commits in common. "5f9dc80ac8cbc016b53c292d572f660a8bc61f8f" and "0da8101f6383b81419d960d281d637092b7255ae" have entirely different histories.

11 changed files with 849 additions and 878 deletions

View File

@ -49,8 +49,7 @@
(:file "template-utils") (:file "template-utils")
(:file "parenscript-utils") (:file "parenscript-utils")
(:module :parenscript (:module :parenscript
:components ((:file "parenscript-utils") :components ((:file "recently-played")
(:file "recently-played")
(:file "auth-ui") (:file "auth-ui")
(:file "front-page") (:file "front-page")
(:file "profile") (:file "profile")

View File

@ -825,14 +825,12 @@
"Main front page" "Main front page"
;; Register this visitor for geo stats (captures real IP from X-Forwarded-For) ;; Register this visitor for geo stats (captures real IP from X-Forwarded-For)
(register-web-listener) (register-web-listener)
(let ((now-playing-stats (icecast-now-playing *stream-base-url*)))
(clip:process-to-string (clip:process-to-string
(load-template "front-page") (load-template "front-page")
:title "ASTEROID RADIO" :title "ASTEROID RADIO"
:station-name "ASTEROID RADIO" :station-name "ASTEROID RADIO"
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers" :status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
:listeners "0" :listeners "0"
:connection-error (not now-playing-stats)
:stream-quality "128kbps MP3" :stream-quality "128kbps MP3"
:stream-base-url *stream-base-url* :stream-base-url *stream-base-url*
:curated-channel-name (get-curated-channel-name) :curated-channel-name (get-curated-channel-name)
@ -842,7 +840,7 @@
:now-playing-artist "The Void" :now-playing-artist "The Void"
:now-playing-track "Silence" :now-playing-track "Silence"
:now-playing-album "Startup Sounds" :now-playing-album "Startup Sounds"
:now-playing-duration "∞"))) :now-playing-duration "∞"))
;; Frameset wrapper for persistent player mode ;; Frameset wrapper for persistent player mode
(define-page frameset-wrapper #@"/frameset" () (define-page frameset-wrapper #@"/frameset" ()

View File

@ -34,7 +34,6 @@
(error (e) (error (e)
(declare (ignore e)) (declare (ignore e))
nil)))) nil))))
(defun icecast-now-playing (icecast-base-url &optional (mount "asteroid.mp3")) (defun icecast-now-playing (icecast-base-url &optional (mount "asteroid.mp3"))
"Fetch now-playing information from Icecast server. "Fetch now-playing information from Icecast server.
@ -90,8 +89,7 @@
`((:listenurl . ,(format nil "~a/~a" *stream-base-url* mount)) `((:listenurl . ,(format nil "~a/~a" *stream-base-url* mount))
(:title . ,title) (:title . ,title)
(:listeners . ,total-listeners) (:listeners . ,total-listeners)
(:track-id . ,(find-track-by-title title)) (:track-id . ,(find-track-by-title title))))))))
(:favorite-count . ,(or (get-track-favorite-count title) 1))))))))
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 10 :timeout 1) (define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 10 :timeout 1)
"Get Partial HTML with live status from Icecast server. "Get Partial HTML with live status from Icecast server.

View File

@ -224,7 +224,7 @@
nil))))) nil)))))
;; Update now playing info from API ;; Update now playing info from API
(defun update-now-playing() (defun update-now-playing ()
(let ((mount (get-current-mount))) (let ((mount (get-current-mount)))
(ps:chain (ps:chain
(fetch (+ "/api/asteroid/partial/now-playing?mount=" mount)) (fetch (+ "/api/asteroid/partial/now-playing?mount=" mount))
@ -250,13 +250,6 @@
;; Check if this track is in user's favorites ;; Check if this track is in user's favorites
(check-favorite-status) (check-favorite-status)
;; Update favorite count display ;; Update favorite count display
(update-favorite-information)
(update-media-session new-title)))))))))
(catch (lambda (error)
(ps:chain console (log "Could not fetch stream status:" error)))))))
;; Update favorite count display
(defun update-favorite-information ()
(let ((count-el (ps:chain document (get-element-by-id "favorite-count-display"))) (let ((count-el (ps:chain document (get-element-by-id "favorite-count-display")))
(count-val-el (ps:chain document (get-element-by-id "favorite-count-value")))) (count-val-el (ps:chain document (get-element-by-id "favorite-count-value"))))
(when (and count-el count-val-el) (when (and count-el count-val-el)
@ -266,8 +259,9 @@
(if (= fav-count 1) (if (= fav-count 1)
"1 person loves this track ❤️" "1 person loves this track ❤️"
(+ fav-count " people love this track ❤️"))) (+ fav-count " people love this track ❤️")))
(setf (ps:@ count-el text-content) "")))))) (setf (ps:@ count-el text-content) "")))))))))))))
(catch (lambda (error)
(ps:chain console (log "Could not fetch stream status:" error)))))))
;; Update stream information ;; Update stream information
(defun update-stream-information () (defun update-stream-information ()
@ -641,7 +635,6 @@
;; Load user's favorites for highlight feature ;; Load user's favorites for highlight feature
(load-favorites-cache) (load-favorites-cache)
(update-favorite-information)
;; Update now playing ;; Update now playing
(update-now-playing) (update-now-playing)
@ -871,6 +864,4 @@
(defun generate-front-page-js () (defun generate-front-page-js ()
"Return the pre-compiled JavaScript for front page" "Return the pre-compiled JavaScript for front page"
(ps-join *front-page-js*)
*common-player-js*
*front-page-js*))

View File

@ -1,6 +0,0 @@
;;;; parenscript-utils.lisp - ParenScript utility functions
(in-package #:asteroid)
(defmacro ps-join (&body forms)
`(format nil "~{~A~^~%~%~}" (list ,@forms)))

View File

@ -3,30 +3,9 @@
(in-package #:asteroid) (in-package #:asteroid)
(defparameter *common-player-js*
(ps:ps*
'(progn
(defun update-media-session (title)
(let ((media-session (ps:@ navigator media-session)))
(when media-session
(let ((track-title "Unknown")
(now-playing-title-el (ps:chain document (query-selector "#current-track-title"))))
(when title
(setf track-title title))
(when (and now-playing-title-el (not title))
(let ((now-playing-title (ps:@ now-playing-title-el text-content)))
(when now-playing-title
(setf track-title now-playing-title))))
(let* ((media-info (ps:create :title track-title
:artwork (list (ps:create :src "/asteroid/static/asteroid-squared.png"
:type "image/png"
:sizes "256x256"))))
(metadata (ps:new (-media-metadata media-info))))
(setf (ps:@ media-session metadata) metadata)))))))))
(defparameter *player-js* (defparameter *player-js*
(ps:ps* (ps:ps*
`(progn '(progn
;; Global variables ;; Global variables
(defvar *tracks* (array)) (defvar *tracks* (array))
@ -802,8 +781,7 @@
(ps:chain console (log "Error connecting to stream")) (ps:chain console (log "Error connecting to stream"))
""))))) "")))))
(then (lambda (data) (then (lambda (data)
(setf (ps:chain document (get-element-by-id "now-playing") inner-h-t-m-l) data) (setf (ps:chain document (get-element-by-id "now-playing") inner-h-t-m-l) data)))
(update-media-session)))
(catch (lambda (error) (catch (lambda (error)
(ps:chain console (log "Could not fetch stream status:" error)))))) (ps:chain console (log "Could not fetch stream status:" error))))))
@ -832,6 +810,4 @@
(defun generate-player-js () (defun generate-player-js ()
"Generate JavaScript code for the web player" "Generate JavaScript code for the web player"
(ps-join *player-js*)
*common-player-js*
*player-js*))

View File

@ -524,7 +524,6 @@
(setf (ps:@ el text-content) title) (setf (ps:@ el text-content) title)
;; Check if this track is in user's favorites ;; Check if this track is in user's favorites
(check-favorite-status-mini)) (check-favorite-status-mini))
(update-media-session title)
(when track-id-el (when track-id-el
(let ((track-id (or (ps:@ data data track_id) (ps:@ data track_id)))) (let ((track-id (or (ps:@ data data track_id) (ps:@ data track_id))))
(setf (ps:@ track-id-el value) (or track-id "")))) (setf (ps:@ track-id-el value) (or track-id ""))))
@ -635,8 +634,7 @@
(when title-el (when title-el
(setf (ps:@ title-el text-content) (ps:chain track-text (trim)))) (setf (ps:@ title-el text-content) (ps:chain track-text (trim))))
(when artist-el (when artist-el
(setf (ps:@ artist-el text-content) "Asteroid Radio"))))) (setf (ps:@ artist-el text-content) "Asteroid Radio"))))))))
(update-media-session track-text))))
(catch (lambda (error) (catch (lambda (error)
(ps:chain console (error "Error updating now playing:" error))))))) (ps:chain console (error "Error updating now playing:" error)))))))
@ -1084,6 +1082,4 @@
(defun generate-stream-player-js () (defun generate-stream-player-js ()
"Generate JavaScript code for the stream player" "Generate JavaScript code for the stream player"
(ps-join *stream-player-js*)
*common-player-js*
*stream-player-js*))

View File

@ -50,6 +50,8 @@
;; Step 1: Reload the playlist file in Liquidsoap ;; Step 1: Reload the playlist file in Liquidsoap
(dotimes (attempt max-retries) (dotimes (attempt max-retries)
(let ((result (liquidsoap-command "stream-queue_m3u.reload"))) (let ((result (liquidsoap-command "stream-queue_m3u.reload")))
(format t "~&[SCHEDULER] Reload attempt ~a/~a: ~a~%"
(1+ attempt) max-retries (string-trim '(#\Space #\Newline #\Return) result))
(when (liquidsoap-command-succeeded-p result) (when (liquidsoap-command-succeeded-p result)
(setf reload-ok t) (setf reload-ok t)
(return))) (return)))
@ -60,6 +62,8 @@
(sleep 1)) ; Brief pause after reload before skipping (sleep 1)) ; Brief pause after reload before skipping
(dotimes (attempt max-retries) (dotimes (attempt max-retries)
(let ((result (liquidsoap-command "stream-queue_m3u.skip"))) (let ((result (liquidsoap-command "stream-queue_m3u.skip")))
(format t "~&[SCHEDULER] Skip attempt ~a/~a: ~a~%"
(1+ attempt) max-retries (string-trim '(#\Space #\Newline #\Return) result))
(when (liquidsoap-command-succeeded-p result) (when (liquidsoap-command-succeeded-p result)
(setf skip-ok t) (setf skip-ok t)
(return))) (return)))
@ -72,23 +76,30 @@
(let ((playlist-path (merge-pathnames playlist-name (get-playlists-directory)))) (let ((playlist-path (merge-pathnames playlist-name (get-playlists-directory))))
(if (probe-file playlist-path) (if (probe-file playlist-path)
(progn (progn
(format t "~&[SCHEDULER] Loading playlist: ~a~%" playlist-name)
(copy-playlist-to-stream-queue playlist-path) (copy-playlist-to-stream-queue playlist-path)
(load-queue-from-m3u-file) (load-queue-from-m3u-file)
(multiple-value-bind (skip-ok reload-ok) (multiple-value-bind (skip-ok reload-ok)
(liquidsoap-reload-and-skip) (liquidsoap-reload-and-skip)
(if (and reload-ok skip-ok) (cond
(log:info "Scheduler loaded ~a" playlist-name) ((and reload-ok skip-ok)
(log:error "Scheduler failed to switch to ~a (reload:~a skip:~a)" (format t "~&[SCHEDULER] Playlist ~a loaded and crossfade triggered successfully~%" playlist-name))
playlist-name reload-ok skip-ok))) (skip-ok
(format t "~&[SCHEDULER] WARNING: Reload failed but skip succeeded for ~a~%" playlist-name))
(reload-ok
(format t "~&[SCHEDULER] WARNING: Reload OK but skip failed for ~a - track may not change immediately~%" playlist-name))
(t
(format t "~&[SCHEDULER] ERROR: Both reload and skip failed for ~a - Liquidsoap may be unresponsive~%" playlist-name))))
t) t)
(progn (progn
(log:error "Scheduler playlist not found: ~a" playlist-name) (format t "~&[SCHEDULER] Error: Playlist not found: ~a~%" playlist-name)
nil)))) nil))))
(defun scheduled-playlist-loader (hour playlist-name) (defun scheduled-playlist-loader (hour playlist-name)
"Create a function that loads a specific playlist. Used by cl-cron jobs." "Create a function that loads a specific playlist. Used by cl-cron jobs."
(lambda () (lambda ()
(when *scheduler-enabled* (when *scheduler-enabled*
(format t "~&[SCHEDULER] Triggered at hour ~a UTC - loading ~a~%" hour playlist-name)
(load-scheduled-playlist playlist-name)))) (load-scheduled-playlist playlist-name))))
;;; Cron Job Management ;;; Cron Job Management
@ -96,25 +107,30 @@
(defun setup-playlist-cron-jobs () (defun setup-playlist-cron-jobs ()
"Set up cl-cron jobs for all scheduled playlists." "Set up cl-cron jobs for all scheduled playlists."
(unless *scheduler-running* (unless *scheduler-running*
(format t "~&[SCHEDULER] Setting up playlist schedule:~%")
(dolist (entry *playlist-schedule*) (dolist (entry *playlist-schedule*)
(let ((hour (car entry)) (let ((hour (car entry))
(playlist (cdr entry))) (playlist (cdr entry)))
(format t "~&[SCHEDULER] ~2,'0d:00 UTC -> ~a~%" hour playlist)
(cl-cron:make-cron-job (cl-cron:make-cron-job
(scheduled-playlist-loader hour playlist) (scheduled-playlist-loader hour playlist)
:minute 0 :minute 0
:hour hour))) :hour hour)))
(setf *scheduler-running* t))) (setf *scheduler-running* t)
(format t "~&[SCHEDULER] Playlist schedule configured~%")))
(defun start-playlist-scheduler () (defun start-playlist-scheduler ()
"Start the playlist scheduler. Sets up cron jobs and starts cl-cron." "Start the playlist scheduler. Sets up cron jobs and starts cl-cron."
(setup-playlist-cron-jobs) (setup-playlist-cron-jobs)
(cl-cron:start-cron) (cl-cron:start-cron)
(format t "~&[SCHEDULER] Playlist scheduler started~%")
t) t)
(defun stop-playlist-scheduler () (defun stop-playlist-scheduler ()
"Stop the playlist scheduler." "Stop the playlist scheduler."
(cl-cron:stop-cron) (cl-cron:stop-cron)
(setf *scheduler-running* nil) (setf *scheduler-running* nil)
(format t "~&[SCHEDULER] Playlist scheduler stopped~%")
t) t)
(defun restart-playlist-scheduler () (defun restart-playlist-scheduler ()
@ -134,9 +150,10 @@
(mapcar (lambda (row) (mapcar (lambda (row)
(cons (first row) (second row))) (cons (first row) (second row)))
rows)) rows))
(log:info "Scheduler loaded ~a entries from database" (length rows))))) (format t "~&[SCHEDULER] Loaded ~a schedule entries from database~%" (length rows)))))
(error (e) (error (e)
(log:warn "Scheduler DB load failed, using defaults: ~a" e)))) (format t "~&[SCHEDULER] Warning: Could not load schedule from DB: ~a~%" e)
(format t "~&[SCHEDULER] Using default schedule~%"))))
(defun save-schedule-entry-to-db (hour playlist-name) (defun save-schedule-entry-to-db (hour playlist-name)
"Save or update a schedule entry in the database." "Save or update a schedule entry in the database."
@ -155,7 +172,7 @@
(format nil "INSERT INTO playlist_schedule (hour, playlist, updated_at) VALUES (~a, '~a', NOW()) ON CONFLICT (hour) DO UPDATE SET playlist = '~a', updated_at = NOW()" (format nil "INSERT INTO playlist_schedule (hour, playlist, updated_at) VALUES (~a, '~a', NOW()) ON CONFLICT (hour) DO UPDATE SET playlist = '~a', updated_at = NOW()"
hour playlist-name playlist-name))) hour playlist-name playlist-name)))
(error (e2) (error (e2)
(log:warn "Scheduler could not save schedule entry: ~a" e2)))))) (format t "~&[SCHEDULER] Warning: Could not save schedule entry: ~a~%" e2))))))
(defun delete-schedule-entry-from-db (hour) (defun delete-schedule-entry-from-db (hour)
"Delete a schedule entry from the database." "Delete a schedule entry from the database."
@ -163,7 +180,7 @@
(with-db (with-db
(postmodern:query (:delete-from 'playlist_schedule :where (:= 'hour hour)))) (postmodern:query (:delete-from 'playlist_schedule :where (:= 'hour hour))))
(error (e) (error (e)
(log:warn "Scheduler could not delete schedule entry: ~a" e)))) (format t "~&[SCHEDULER] Warning: Could not delete schedule entry: ~a~%" e))))
(defun add-scheduled-playlist (hour playlist-name) (defun add-scheduled-playlist (hour playlist-name)
"Add or update a playlist in the schedule (persists to database)." "Add or update a playlist in the schedule (persists to database)."
@ -335,13 +352,17 @@
(define-trigger db:connected () (define-trigger db:connected ()
"Start the playlist scheduler after database connection is established" "Start the playlist scheduler after database connection is established"
(format t "~&[SCHEDULER] Database connected, starting playlist scheduler...~%")
(handler-case (handler-case
(progn (progn
;; Load schedule from database first
(load-schedule-from-db) (load-schedule-from-db)
(start-playlist-scheduler) (start-playlist-scheduler)
;; Load the current scheduled playlist on startup
(let ((current-playlist (get-current-scheduled-playlist))) (let ((current-playlist (get-current-scheduled-playlist)))
(when current-playlist (when current-playlist
(format t "~&[SCHEDULER] Loading current scheduled playlist: ~a~%" current-playlist)
(load-scheduled-playlist current-playlist))) (load-scheduled-playlist current-playlist)))
(log:info "Playlist scheduler started")) (format t "~&[SCHEDULER] Scheduler auto-started successfully~%"))
(error (e) (error (e)
(log:error "Scheduler failed to start: ~a" e)))) (format t "~&[SCHEDULER] Warning: Could not auto-start scheduler: ~a~%" e))))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -117,9 +117,7 @@
</c:if> </c:if>
</div> </div>
<div id="now-playing" class="now-playing"> <div id="now-playing" class="now-playing"></div>
<c:h>(asteroid::load-template "partial/now-playing")</c:h>
</div>
<!-- Recently Played Tracks --> <!-- Recently Played Tracks -->
<div id="recently-played-panel" class="recently-played-panel"> <div id="recently-played-panel" class="recently-played-panel">

View File

@ -8,11 +8,11 @@
<span class="star-icon">☆</span> <span class="star-icon">☆</span>
</button> </button>
</div> </div>
<p>Listeners: <span id="current-listeners" lquery="(text listeners)">1</span></p> <p>Listeners: <span lquery="(text listeners)">1</span></p>
</c:using>
<input type="hidden" id="current-track-id" lquery="(val track-id)" value=""> <input type="hidden" id="current-track-id" lquery="(val track-id)" value="">
<input type="hidden" id="favorite-count-value" lquery="(val favorite-count)" value="0"> <input type="hidden" id="favorite-count-value" lquery="(val favorite-count)" value="0">
<p class="favorite-count" id="favorite-count-display"></p> <p class="favorite-count" id="favorite-count-display"></p>
</c:using>
</c:then> </c:then>
<c:else> <c:else>
<c:if test="connection-error"> <c:if test="connection-error">