Compare commits
No commits in common. "5f9dc80ac8cbc016b53c292d572f660a8bc61f8f" and "0da8101f6383b81419d960d281d637092b7255ae" have entirely different histories.
5f9dc80ac8
...
0da8101f63
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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" ()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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*)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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*)
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue