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 "parenscript-utils")
(:module :parenscript
:components ((:file "parenscript-utils")
(:file "recently-played")
:components ((:file "recently-played")
(:file "auth-ui")
(:file "front-page")
(:file "profile")

View File

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

View File

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

View File

@ -224,7 +224,7 @@
nil)))))
;; Update now playing info from API
(defun update-now-playing()
(defun update-now-playing ()
(let ((mount (get-current-mount)))
(ps:chain
(fetch (+ "/api/asteroid/partial/now-playing?mount=" mount))
@ -250,25 +250,19 @@
;; Check if this track is in user's favorites
(check-favorite-status)
;; Update favorite count display
(update-favorite-information)
(update-media-session new-title)))))))))
(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"))))
(when (and count-el count-val-el)
(let ((fav-count (parse-int (or (ps:@ count-val-el value) "0") 10)))
(if (> fav-count 0)
(setf (ps:@ count-el text-content)
(if (= fav-count 1)
"1 person loves this track ❤️"
(+ fav-count " people love this track ❤️")))
(setf (ps:@ count-el text-content) "")))))))))))))
(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")))
(count-val-el (ps:chain document (get-element-by-id "favorite-count-value"))))
(when (and count-el count-val-el)
(let ((fav-count (parse-int (or (ps:@ count-val-el value) "0") 10)))
(if (> fav-count 0)
(setf (ps:@ count-el text-content)
(if (= fav-count 1)
"1 person loves this track ❤️"
(+ fav-count " people love this track ❤️")))
(setf (ps:@ count-el text-content) ""))))))
;; Update stream information
(defun update-stream-information ()
(let* ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel"))
@ -641,7 +635,6 @@
;; Load user's favorites for highlight feature
(load-favorites-cache)
(update-favorite-information)
;; Update now playing
(update-now-playing)
@ -871,6 +864,4 @@
(defun generate-front-page-js ()
"Return the pre-compiled JavaScript for front page"
(ps-join
*common-player-js*
*front-page-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)))

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -50,6 +50,8 @@
;; Step 1: Reload the playlist file in Liquidsoap
(dotimes (attempt max-retries)
(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)
(setf reload-ok t)
(return)))
@ -60,6 +62,8 @@
(sleep 1)) ; Brief pause after reload before skipping
(dotimes (attempt max-retries)
(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)
(setf skip-ok t)
(return)))
@ -72,23 +76,30 @@
(let ((playlist-path (merge-pathnames playlist-name (get-playlists-directory))))
(if (probe-file playlist-path)
(progn
(format t "~&[SCHEDULER] Loading playlist: ~a~%" playlist-name)
(copy-playlist-to-stream-queue playlist-path)
(load-queue-from-m3u-file)
(multiple-value-bind (skip-ok reload-ok)
(liquidsoap-reload-and-skip)
(if (and reload-ok skip-ok)
(log:info "Scheduler loaded ~a" playlist-name)
(log:error "Scheduler failed to switch to ~a (reload:~a skip:~a)"
playlist-name reload-ok skip-ok)))
(cond
((and reload-ok skip-ok)
(format t "~&[SCHEDULER] Playlist ~a loaded and crossfade triggered successfully~%" playlist-name))
(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)
(progn
(log:error "Scheduler playlist not found: ~a" playlist-name)
(format t "~&[SCHEDULER] Error: Playlist not found: ~a~%" playlist-name)
nil))))
(defun scheduled-playlist-loader (hour playlist-name)
"Create a function that loads a specific playlist. Used by cl-cron jobs."
(lambda ()
(when *scheduler-enabled*
(format t "~&[SCHEDULER] Triggered at hour ~a UTC - loading ~a~%" hour playlist-name)
(load-scheduled-playlist playlist-name))))
;;; Cron Job Management
@ -96,25 +107,30 @@
(defun setup-playlist-cron-jobs ()
"Set up cl-cron jobs for all scheduled playlists."
(unless *scheduler-running*
(format t "~&[SCHEDULER] Setting up playlist schedule:~%")
(dolist (entry *playlist-schedule*)
(let ((hour (car entry))
(playlist (cdr entry)))
(format t "~&[SCHEDULER] ~2,'0d:00 UTC -> ~a~%" hour playlist)
(cl-cron:make-cron-job
(scheduled-playlist-loader hour playlist)
:minute 0
:hour hour)))
(setf *scheduler-running* t)))
(setf *scheduler-running* t)
(format t "~&[SCHEDULER] Playlist schedule configured~%")))
(defun start-playlist-scheduler ()
"Start the playlist scheduler. Sets up cron jobs and starts cl-cron."
(setup-playlist-cron-jobs)
(cl-cron:start-cron)
(format t "~&[SCHEDULER] Playlist scheduler started~%")
t)
(defun stop-playlist-scheduler ()
"Stop the playlist scheduler."
(cl-cron:stop-cron)
(setf *scheduler-running* nil)
(format t "~&[SCHEDULER] Playlist scheduler stopped~%")
t)
(defun restart-playlist-scheduler ()
@ -134,9 +150,10 @@
(mapcar (lambda (row)
(cons (first row) (second row)))
rows))
(log:info "Scheduler loaded ~a entries from database" (length rows)))))
(format t "~&[SCHEDULER] Loaded ~a schedule entries from database~%" (length rows)))))
(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)
"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()"
hour playlist-name playlist-name)))
(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)
"Delete a schedule entry from the database."
@ -163,7 +180,7 @@
(with-db
(postmodern:query (:delete-from 'playlist_schedule :where (:= 'hour hour))))
(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)
"Add or update a playlist in the schedule (persists to database)."
@ -335,13 +352,17 @@
(define-trigger db:connected ()
"Start the playlist scheduler after database connection is established"
(format t "~&[SCHEDULER] Database connected, starting playlist scheduler...~%")
(handler-case
(progn
;; Load schedule from database first
(load-schedule-from-db)
(start-playlist-scheduler)
;; Load the current scheduled playlist on startup
(let ((current-playlist (get-current-scheduled-playlist)))
(when current-playlist
(format t "~&[SCHEDULER] Loading current scheduled playlist: ~a~%" current-playlist)
(load-scheduled-playlist current-playlist)))
(log:info "Playlist scheduler started"))
(format t "~&[SCHEDULER] Scheduler auto-started successfully~%"))
(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>
</div>
<div id="now-playing" class="now-playing">
<c:h>(asteroid::load-template "partial/now-playing")</c:h>
</div>
<div id="now-playing" class="now-playing"></div>
<!-- Recently Played Tracks -->
<div id="recently-played-panel" class="recently-played-panel">

View File

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