From 0306820d1d85b0e30fbc5670261f6e884c728cc1 Mon Sep 17 00:00:00 2001 From: glenneth Date: Thu, 18 Dec 2025 05:47:56 +0300 Subject: [PATCH] Fix playlist display polling and add footer links - Add /api/asteroid/channel-name endpoint for live channel name updates - Update front-page.js and stream-player.js to poll server for channel name instead of relying on localStorage (fixes issue where listeners don't see playlist changes until page refresh) - Add footer with Internet Radio directory link and craftering webring links - Fix last-login display bug: use proper ISO 8601 timestamp format and parse as Date string instead of Unix epoch --- frontend-partials.lisp | 6 ++++++ parenscript/front-page.lisp | 33 ++++++++++++++++++++++++++-- parenscript/stream-player.lisp | 37 +++++++++++++++++++++----------- parenscript/users.lisp | 2 +- static/asteroid.lass | 24 +++++++++++++++++++++ template/front-page-content.ctml | 9 ++++++++ template/front-page.ctml | 9 ++++++++ user-management.lisp | 6 +++++- 8 files changed, 110 insertions(+), 16 deletions(-) diff --git a/frontend-partials.lisp b/frontend-partials.lisp index 157ef61..a300e4c 100644 --- a/frontend-partials.lisp +++ b/frontend-partials.lisp @@ -96,3 +96,9 @@ (progn (setf (header "Content-Type") "text/plain") "Stream Offline"))))) + +(define-api asteroid/channel-name () () + "Get the current curated channel name for live updates. + Returns JSON with the channel name from the current playlist's PHASE header." + (with-error-handling + (api-output `(("channel_name" . ,(get-curated-channel-name)))))) diff --git a/parenscript/front-page.lisp b/parenscript/front-page.lisp index 628324d..de76396 100644 --- a/parenscript/front-page.lisp +++ b/parenscript/front-page.lisp @@ -594,8 +594,37 @@ ;; Update now playing every 5 seconds (set-interval update-now-playing 5000) - - ;; Listen for messages from popout window + + ;; Poll server for channel name changes (works across all listeners) + (let ((last-channel-name nil)) + (set-interval + (lambda () + (ps:chain + (fetch "/api/asteroid/channel-name") + (then (lambda (response) + (if (ps:@ response ok) + (ps:chain response (json)) + nil))) + (then (lambda (data) + (when data + (let ((current-channel-name (or (ps:@ data data channel_name) + (ps:@ data channel_name)))) + (when (and current-channel-name + (not (= current-channel-name last-channel-name))) + (setf last-channel-name current-channel-name) + ;; Update localStorage for cross-window sync + (ps:chain local-storage (set-item "curated-channel-name" current-channel-name)) + ;; Update channel selector in current document + (let ((channel-selector (ps:chain document (get-element-by-id "stream-channel")))) + (when channel-selector + (let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']")))) + (when curated-option + (setf (ps:@ curated-option text-content) (+ "🎧 " current-channel-name))))))))))) + (catch (lambda (error) + (ps:chain console (log "Could not fetch channel name:" error)))))) + 10000)) ;; Poll every 10 seconds + + ;; Listen for messages from popout window (ps:chain window (add-event-listener "message" diff --git a/parenscript/stream-player.lisp b/parenscript/stream-player.lisp index a2bcbc3..a69c7ab 100644 --- a/parenscript/stream-player.lisp +++ b/parenscript/stream-player.lisp @@ -556,20 +556,33 @@ ;; Update quality selector state based on channel (update-quality-selector-state) - ;; Check for channel name changes from localStorage periodically - (let ((last-channel-name (ps:chain local-storage (get-item "curated-channel-name")))) + ;; Poll server for channel name changes (works across all listeners) + (let ((last-channel-name nil)) (set-interval (lambda () - (let ((current-channel-name (ps:chain local-storage (get-item "curated-channel-name")))) - (when (and current-channel-name - (not (= current-channel-name last-channel-name))) - (setf last-channel-name current-channel-name) - (let ((channel-selector (ps:chain document (get-element-by-id "stream-channel")))) - (when channel-selector - (let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']")))) - (when curated-option - (setf (ps:@ curated-option text-content) (+ "🎧 " current-channel-name))))))))) - 2000)) + (ps:chain + (fetch "/api/asteroid/channel-name") + (then (lambda (response) + (if (ps:@ response ok) + (ps:chain response (json)) + nil))) + (then (lambda (data) + (when data + (let ((current-channel-name (or (ps:@ data data channel_name) + (ps:@ data channel_name)))) + (when (and current-channel-name + (not (= current-channel-name last-channel-name))) + (setf last-channel-name current-channel-name) + ;; Update localStorage for popout player sync + (ps:chain local-storage (set-item "curated-channel-name" current-channel-name)) + (let ((channel-selector (ps:chain document (get-element-by-id "stream-channel")))) + (when channel-selector + (let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']")))) + (when curated-option + (setf (ps:@ curated-option text-content) (+ "🎧 " current-channel-name))))))))))) + (catch (lambda (error) + (ps:chain console (log "Could not fetch channel name:" error)))))) + 10000)) ;; Poll every 10 seconds ;; Start now playing updates (set-timeout update-mini-now-playing 1000) diff --git a/parenscript/users.lisp b/parenscript/users.lisp index 28bad00..a2c5971 100644 --- a/parenscript/users.lisp +++ b/parenscript/users.lisp @@ -58,7 +58,7 @@ "" "" (if (ps:@ user active) "✅ Active" "❌ Inactive") "" "" (if (ps:getprop user "last-login") - (ps:chain (ps:new (-date (* (ps:getprop user "last-login") 1000))) (to-locale-string)) + (ps:chain (ps:new (-date (ps:getprop user "last-login"))) (to-locale-string)) "Never") "" "" (if (ps:@ user active) diff --git a/static/asteroid.lass b/static/asteroid.lass index 6f7348b..9265903 100644 --- a/static/asteroid.lass +++ b/static/asteroid.lass @@ -1250,4 +1250,28 @@ (0% :opacity 1) (50% :opacity 0.3) (100% :opacity 1)) + + ;; Site footer - subtle, unobtrusive + (.site-footer + :text-align "center" + :padding "20px 0" + :margin-top "30px" + :border-top "1px solid #333" + :font-size "0.85em" + :color "#666" + :display "flex" + :justify-content "center" + :gap "30px" + :flex-wrap "wrap" + + (a + :color "#888" + :text-decoration "none" + :transition "color 0.2s ease") + + ((:and a :hover) + :color "#00ff00") + + (.craftering + (a :margin "0 5px"))) ) ;; End of let block diff --git a/template/front-page-content.ctml b/template/front-page-content.ctml index 2cc4bb4..8105c0a 100644 --- a/template/front-page-content.ctml +++ b/template/front-page-content.ctml @@ -93,6 +93,15 @@ + + diff --git a/template/front-page.ctml b/template/front-page.ctml index adcf481..77994cd 100644 --- a/template/front-page.ctml +++ b/template/front-page.ctml @@ -126,6 +126,15 @@ + + diff --git a/user-management.lisp b/user-management.lisp index acfa1c6..4f23df6 100644 --- a/user-management.lisp +++ b/user-management.lisp @@ -70,9 +70,13 @@ (when (and (= 1 user-active) (verify-password password user-password)) ;; Update last login using data-model (database agnostic) + ;; Use ISO 8601 format that PostgreSQL TIMESTAMP can parse (handler-case (progn - (setf (dm:field user "last-login") (format nil "~a" (local-time:now))) + (setf (dm:field user "last-login") + (local-time:format-timestring nil (local-time:now) + :format '(:year "-" (:month 2) "-" (:day 2) " " + (:hour 2) ":" (:min 2) ":" (:sec 2)))) (dm:save user)) (error (e) (format t "Warning: Could not update last-login: ~a~%" e)))