Compare commits

..

No commits in common. "a9c48e59c951b5e5251d1b61fdcd2e33c597a174" and "7ed11ae2f4086d5cda1d8fd6efff429eda64c520" have entirely different histories.

6 changed files with 20 additions and 167 deletions

1
.gitignore vendored
View File

@ -65,4 +65,3 @@ playlists/stream-queue.m3u
/radiance-bootstrap.lisp /radiance-bootstrap.lisp
/test-postgres-db.lisp /test-postgres-db.lisp
/userdump.csv /userdump.csv
.envrc

View File

@ -91,7 +91,7 @@
(:listeners . ,total-listeners) (:listeners . ,total-listeners)
(:track-id . ,(find-track-by-title title)))))))) (:track-id . ,(find-track-by-title title))))))))
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 180 :timeout 60) (define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 3 :timeout 1)
"Get Partial HTML with live status from Icecast server. "Get Partial HTML with live status from Icecast server.
Optional MOUNT parameter specifies which stream to get metadata from. Optional MOUNT parameter specifies which stream to get metadata from.
Always polls both streams to keep recently played lists updated." Always polls both streams to keep recently played lists updated."
@ -121,7 +121,7 @@
:connection-error t :connection-error t
:stats nil)))))) :stats nil))))))
(define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) (:limit 180 :timeout 60) (define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) (:limit 3 :timeout 1)
"Get inline text with now playing info (for admin dashboard and widgets). "Get inline text with now playing info (for admin dashboard and widgets).
Optional MOUNT parameter specifies which stream to get metadata from." Optional MOUNT parameter specifies which stream to get metadata from."
(with-error-handling (with-error-handling
@ -135,7 +135,7 @@
(setf (header "Content-Type") "text/plain") (setf (header "Content-Type") "text/plain")
"Stream Offline"))))) "Stream Offline")))))
(define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 180 :timeout 60) (define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 2 :timeout 1)
"Get JSON with now playing info including track ID for favorites. "Get JSON with now playing info including track ID for favorites.
Optional MOUNT parameter specifies which stream to get metadata from." Optional MOUNT parameter specifies which stream to get metadata from."
;; Register web listener for geo stats (keeps listener active during playback) ;; Register web listener for geo stats (keeps listener active during playback)
@ -160,7 +160,7 @@
("title" . "Stream Offline") ("title" . "Stream Offline")
("track_id" . nil))))))) ("track_id" . nil)))))))
(define-api-with-limit asteroid/channel-name () (:limit 180 :timeout 60) (define-api-with-limit asteroid/channel-name () (:limit 2 :timeout 1)
"Get the current curated channel name for live updates. "Get the current curated channel name for live updates.
Returns JSON with the channel name from the current playlist's PHASE header." Returns JSON with the channel name from the current playlist's PHASE header."
(with-error-handling (with-error-handling

View File

@ -338,114 +338,6 @@
;; Track the last recorded title to avoid duplicate history entries ;; Track the last recorded title to avoid duplicate history entries
(defvar *last-recorded-title* nil) (defvar *last-recorded-title* nil)
;; Track last notified title to avoid duplicate notifications
(defvar *last-notified-title* nil)
;; Check if notifications are enabled in localStorage
(defun notifications-enabled-p ()
(= (ps:chain local-storage (get-item "notifications-enabled")) "true"))
;; Check if browser supports notifications
(defun notifications-supported-p ()
(not (= (typeof (ps:@ window -notification)) "undefined")))
;; Get notification permission status
(defun get-notification-permission ()
(if (notifications-supported-p)
(ps:@ -notification permission)
"denied"))
;; Request notification permission from user
(defun request-notification-permission ()
(when (notifications-supported-p)
(ps:chain -notification (request-permission)
(then (lambda (permission)
(if (= permission "granted")
(progn
(ps:chain local-storage (set-item "notifications-enabled" "true"))
(update-notification-toggle-ui)
(show-track-notification "Notifications Enabled" "You'll now receive track change notifications"))
(progn
(ps:chain local-storage (set-item "notifications-enabled" "false"))
(update-notification-toggle-ui))))))))
;; Toggle notifications on/off
(defun toggle-notifications ()
(let ((permission (get-notification-permission)))
(cond
;; Not supported
((not (notifications-supported-p))
(alert "Your browser does not support notifications"))
;; Permission denied - can't do anything
((= permission "denied")
(alert "Notifications are blocked. Please enable them in your browser settings."))
;; Permission not yet requested
((= permission "default")
(request-notification-permission))
;; Permission granted - toggle the setting
((= permission "granted")
(if (notifications-enabled-p)
(progn
(ps:chain local-storage (set-item "notifications-enabled" "false"))
(update-notification-toggle-ui))
(progn
(ps:chain local-storage (set-item "notifications-enabled" "true"))
(update-notification-toggle-ui)
(show-track-notification "Notifications Enabled" "You'll now receive track change notifications")))))))
;; Update the notification toggle button UI
(defun update-notification-toggle-ui ()
(let ((btn (ps:chain document (get-element-by-id "notification-toggle"))))
(when btn
(let ((permission (get-notification-permission))
(enabled (notifications-enabled-p)))
(cond
((not (notifications-supported-p))
(setf (ps:@ btn text-content) "🔕")
(setf (ps:@ btn title) "Notifications not supported"))
((= permission "denied")
(setf (ps:@ btn text-content) "🔕")
(setf (ps:@ btn title) "Notifications blocked - enable in browser settings"))
((and (= permission "granted") enabled)
(setf (ps:@ btn text-content) "🔔")
(setf (ps:@ btn title) "Track notifications ON - click to disable"))
(t
(setf (ps:@ btn text-content) "🔕")
(setf (ps:@ btn title) "Track notifications OFF - click to enable")))))))
;; Show a system notification for track change
(defun show-track-notification (title body)
(when (and (notifications-supported-p)
(= (get-notification-permission) "granted")
(notifications-enabled-p)
(not (= title *last-notified-title*)))
(setf *last-notified-title* title)
(ps:try
(let ((notification (ps:new (-notification title
(ps:create :body body
:icon "/asteroid/static/asteroid.png"
:tag "asteroid-track-change"
:renotify true
:silent false)))))
;; Auto-close after 5 seconds
(set-timeout (lambda () (ps:chain notification (close))) 5000)
;; Click to focus the window
(setf (ps:@ notification onclick)
(lambda ()
(ps:chain window (focus))
(ps:chain notification (close)))))
(:catch (e)
(ps:chain console (log "Notification error:" e))))))
;; Notify track change (called from update-mini-now-playing)
(defun notify-track-change (title)
(when (and title
(not (= title ""))
(not (= title "Loading..."))
(not (= title *last-notified-title*)))
;; Show full "Artist - Track" in title, "Now Playing" as body
(show-track-notification title "Now Playing on Asteroid Radio")))
;; Cache of user's favorite track titles for quick lookup (mini player) ;; Cache of user's favorite track titles for quick lookup (mini player)
(defvar *user-favorites-cache-mini* (array)) (defvar *user-favorites-cache-mini* (array))
@ -517,10 +409,9 @@
(track-id-el (ps:chain document (get-element-by-id "current-track-id-mini"))) (track-id-el (ps:chain document (get-element-by-id "current-track-id-mini")))
(title (or (ps:@ data data title) (ps:@ data title) "Loading..."))) (title (or (ps:@ data data title) (ps:@ data title) "Loading...")))
(when el (when el
;; Check if track changed and record to history + notify ;; Check if track changed and record to history
(when (not (= (ps:@ el text-content) title)) (when (not (= (ps:@ el text-content) title))
(record-track-listen title) (record-track-listen title))
(notify-track-change title))
(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))
@ -965,9 +856,6 @@
;; Update quality selector state based on channel ;; Update quality selector state based on channel
(update-quality-selector-state) (update-quality-selector-state)
;; Initialize notification toggle UI
(update-notification-toggle-ui)
;; Poll server for channel name changes (works across all listeners) ;; Poll server for channel name changes (works across all listeners)
(let ((last-channel-name nil)) (let ((last-channel-name nil))
(set-interval (set-interval
@ -1054,10 +942,6 @@
(setf (ps:@ window update-mini-now-playing) update-mini-now-playing) (setf (ps:@ window update-mini-now-playing) update-mini-now-playing)
(setf (ps:@ window update-popout-now-playing) update-popout-now-playing) (setf (ps:@ window update-popout-now-playing) update-popout-now-playing)
(setf (ps:@ window toggle-favorite-mini) toggle-favorite-mini) (setf (ps:@ window toggle-favorite-mini) toggle-favorite-mini)
(setf (ps:@ window toggle-notifications) toggle-notifications)
(setf (ps:@ window update-notification-toggle-ui) update-notification-toggle-ui)
(setf (ps:@ window notify-popout-opened) notify-popout-opened)
(setf (ps:@ window notify-popout-closing) notify-popout-closing)
;; Auto-initialize on DOMContentLoaded based on which elements exist ;; Auto-initialize on DOMContentLoaded based on which elements exist
(ps:chain document (ps:chain document

View File

@ -1388,21 +1388,6 @@ body.persistent-player-container .persistent-reconnect-btn:hover{
background: #2a3441; background: #2a3441;
} }
body.persistent-player-container .persistent-notification-btn{
background: transparent;
color: #00ff00;
border: 1px solid #00ff00;
padding: 5px 8px;
cursor: pointer;
font-size: 1em;
white-space: nowrap;
margin-right: 5px;
}
body.persistent-player-container .persistent-notification-btn:hover{
background: #2a3441;
}
body.persistent-player-container .persistent-disable-btn{ body.persistent-player-container .persistent-disable-btn{
background: transparent; background: transparent;
color: #00ff00; color: #00ff00;

View File

@ -1122,19 +1122,6 @@
((:and .persistent-reconnect-btn :hover) ((:and .persistent-reconnect-btn :hover)
:background "#2a3441") :background "#2a3441")
(.persistent-notification-btn
:background transparent
:color "#00ff00"
:border "1px solid #00ff00"
:padding "5px 8px"
:cursor "pointer"
:font-size "1em"
:white-space nowrap
:margin-right "5px")
((:and .persistent-notification-btn :hover)
:background "#2a3441")
(.persistent-disable-btn (.persistent-disable-btn
:background transparent :background transparent
:color "#00ff00" :color "#00ff00"

View File

@ -46,8 +46,6 @@
</button> </button>
<input type="hidden" id="current-track-id-mini" value=""> <input type="hidden" id="current-track-id-mini" value="">
<button id="notification-toggle" onclick="toggleNotifications()" class="persistent-notification-btn" title="Track notifications OFF - click to enable">🔕</button>
<button id="reconnect-btn" onclick="reconnectStream()" class="persistent-reconnect-btn" title="Reconnect if audio stops working"> <button id="reconnect-btn" onclick="reconnectStream()" class="persistent-reconnect-btn" title="Reconnect if audio stops working">
<img src="/asteroid/static/icons/sync.png" alt="Reconnect" style="width: 18px; height: 18px; vertical-align: middle; filter: invert(48%) sepia(79%) saturate(2476%) hue-rotate(86deg) brightness(118%) contrast(119%);"> <img src="/asteroid/static/icons/sync.png" alt="Reconnect" style="width: 18px; height: 18px; vertical-align: middle; filter: invert(48%) sepia(79%) saturate(2476%) hue-rotate(86deg) brightness(118%) contrast(119%);">
</button> </button>