Compare commits
No commits in common. "349fa31d8f2c26abef8f358d92cd2ad9407d93a9" and "b415ca9530e881efb1b0cf6e85d1b402f6ab6d0d" have entirely different histories.
349fa31d8f
...
b415ca9530
|
|
@ -53,14 +53,6 @@
|
||||||
(setf (session:field "user-id") nil)
|
(setf (session:field "user-id") nil)
|
||||||
(radiance:redirect "/"))
|
(radiance:redirect "/"))
|
||||||
|
|
||||||
;; Helper to convert Common Lisp universal time to Unix epoch
|
|
||||||
(defun cl-time-to-unix (cl-time)
|
|
||||||
"Convert Common Lisp universal time to Unix epoch.
|
|
||||||
CL epoch is 1900-01-01, Unix epoch is 1970-01-01.
|
|
||||||
Difference is 2208988800 seconds."
|
|
||||||
(when cl-time
|
|
||||||
(- cl-time 2208988800)))
|
|
||||||
|
|
||||||
;; API: Get all users (admin only)
|
;; API: Get all users (admin only)
|
||||||
(define-api asteroid/users () ()
|
(define-api asteroid/users () ()
|
||||||
"API endpoint to get all users"
|
"API endpoint to get all users"
|
||||||
|
|
@ -74,8 +66,8 @@
|
||||||
("email" . ,(dm:field user "email"))
|
("email" . ,(dm:field user "email"))
|
||||||
("role" . ,(dm:field user "role"))
|
("role" . ,(dm:field user "role"))
|
||||||
("active" . ,(= (dm:field user "active") 1))
|
("active" . ,(= (dm:field user "active") 1))
|
||||||
("created-date" . ,(cl-time-to-unix (dm:field user "created-date")))
|
("created-date" . ,(dm:field user "created-date"))
|
||||||
("last-login" . ,(cl-time-to-unix (dm:field user "last-login")))))
|
("last-login" . ,(dm:field user "last-login"))))
|
||||||
users)))))
|
users)))))
|
||||||
(error (e)
|
(error (e)
|
||||||
(api-output `(("status" . "error")
|
(api-output `(("status" . "error")
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,3 @@
|
||||||
(progn
|
(progn
|
||||||
(setf (header "Content-Type") "text/plain")
|
(setf (header "Content-Type") "text/plain")
|
||||||
"Stream Offline")))))
|
"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))))))
|
|
||||||
|
|
|
||||||
|
|
@ -595,36 +595,7 @@
|
||||||
;; Update now playing every 5 seconds
|
;; Update now playing every 5 seconds
|
||||||
(set-interval update-now-playing 5000)
|
(set-interval update-now-playing 5000)
|
||||||
|
|
||||||
;; Poll server for channel name changes (works across all listeners)
|
;; Listen for messages from popout window
|
||||||
(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
|
(ps:chain window
|
||||||
(add-event-listener
|
(add-event-listener
|
||||||
"message"
|
"message"
|
||||||
|
|
|
||||||
|
|
@ -556,33 +556,20 @@
|
||||||
;; Update quality selector state based on channel
|
;; Update quality selector state based on channel
|
||||||
(update-quality-selector-state)
|
(update-quality-selector-state)
|
||||||
|
|
||||||
;; Poll server for channel name changes (works across all listeners)
|
;; Check for channel name changes from localStorage periodically
|
||||||
(let ((last-channel-name nil))
|
(let ((last-channel-name (ps:chain local-storage (get-item "curated-channel-name"))))
|
||||||
(set-interval
|
(set-interval
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(ps:chain
|
(let ((current-channel-name (ps:chain local-storage (get-item "curated-channel-name"))))
|
||||||
(fetch "/api/asteroid/channel-name")
|
(when (and current-channel-name
|
||||||
(then (lambda (response)
|
(not (= current-channel-name last-channel-name)))
|
||||||
(if (ps:@ response ok)
|
(setf last-channel-name current-channel-name)
|
||||||
(ps:chain response (json))
|
(let ((channel-selector (ps:chain document (get-element-by-id "stream-channel"))))
|
||||||
nil)))
|
(when channel-selector
|
||||||
(then (lambda (data)
|
(let ((curated-option (ps:chain channel-selector (query-selector "option[value='curated']"))))
|
||||||
(when data
|
(when curated-option
|
||||||
(let ((current-channel-name (or (ps:@ data data channel_name)
|
(setf (ps:@ curated-option text-content) (+ "🎧 " current-channel-name)))))))))
|
||||||
(ps:@ data channel_name))))
|
2000))
|
||||||
(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
|
;; Start now playing updates
|
||||||
(set-timeout update-mini-now-playing 1000)
|
(set-timeout update-mini-now-playing 1000)
|
||||||
|
|
|
||||||
|
|
@ -57,13 +57,9 @@
|
||||||
"</select>"
|
"</select>"
|
||||||
"</td>"
|
"</td>"
|
||||||
"<td>" (if (ps:@ user active) "✅ Active" "❌ Inactive") "</td>"
|
"<td>" (if (ps:@ user active) "✅ Active" "❌ Inactive") "</td>"
|
||||||
"<td>" (let ((login-val (ps:getprop user "last-login")))
|
"<td>" (if (ps:getprop user "last-login")
|
||||||
(if login-val
|
(ps:chain (ps:new (-date (* (ps:getprop user "last-login") 1000))) (to-locale-string))
|
||||||
(let ((date-val (if (> login-val 9999999999)
|
"Never") "</td>"
|
||||||
login-val ; Already milliseconds
|
|
||||||
(* login-val 1000)))) ; Convert seconds to ms
|
|
||||||
(ps:chain (ps:new (-date date-val)) (to-locale-string)))
|
|
||||||
"Never")) "</td>"
|
|
||||||
"<td class=\"user-actions\">"
|
"<td class=\"user-actions\">"
|
||||||
(if (ps:@ user active)
|
(if (ps:@ user active)
|
||||||
(+ "<button class=\"btn btn-danger\" onclick=\"deactivateUser('" (ps:@ user id) "')\">Deactivate</button>")
|
(+ "<button class=\"btn btn-danger\" onclick=\"deactivateUser('" (ps:@ user id) "')\">Deactivate</button>")
|
||||||
|
|
|
||||||
|
|
@ -1250,28 +1250,4 @@
|
||||||
(0% :opacity 1)
|
(0% :opacity 1)
|
||||||
(50% :opacity 0.3)
|
(50% :opacity 0.3)
|
||||||
(100% :opacity 1))
|
(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
|
) ;; End of let block
|
||||||
|
|
|
||||||
|
|
@ -93,15 +93,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="site-footer">
|
|
||||||
<span>Listed on <a href="http://www.internet-radio.com/stations/ambient/" target="_blank" rel="noopener">Internet Radio</a></span>
|
|
||||||
<span class="craftering">
|
|
||||||
<a href="https://craftering.systemcrafters.net/@asteroid/previous">←</a>
|
|
||||||
<a href="https://craftering.systemcrafters.net/">craftering</a>
|
|
||||||
<a href="https://craftering.systemcrafters.net/@asteroid/next">→</a>
|
|
||||||
</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -126,15 +126,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="site-footer">
|
|
||||||
<span>Listed on <a href="http://www.internet-radio.com/stations/ambient/" target="_blank" rel="noopener">Internet Radio</a></span>
|
|
||||||
<span class="craftering">
|
|
||||||
<a href="https://craftering.systemcrafters.net/@asteroid/previous">←</a>
|
|
||||||
<a href="https://craftering.systemcrafters.net/">craftering</a>
|
|
||||||
<a href="https://craftering.systemcrafters.net/@asteroid/next">→</a>
|
|
||||||
</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -70,14 +70,9 @@
|
||||||
(when (and (= 1 user-active)
|
(when (and (= 1 user-active)
|
||||||
(verify-password password user-password))
|
(verify-password password user-password))
|
||||||
;; Update last login using data-model (database agnostic)
|
;; Update last login using data-model (database agnostic)
|
||||||
;; Use ISO 8601 format in UTC that PostgreSQL TIMESTAMP can parse
|
|
||||||
(handler-case
|
(handler-case
|
||||||
(progn
|
(progn
|
||||||
(setf (dm:field user "last-login")
|
(setf (dm:field user "last-login") (format nil "~a" (local-time:now)))
|
||||||
(local-time:format-timestring nil (local-time:now)
|
|
||||||
:format '(:year "-" (:month 2) "-" (:day 2) " "
|
|
||||||
(:hour 2) ":" (:min 2) ":" (:sec 2))
|
|
||||||
:timezone local-time:+utc-zone+))
|
|
||||||
(dm:save user))
|
(dm:save user))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "Warning: Could not update last-login: ~a~%" e)))
|
(format t "Warning: Could not update last-login: ~a~%" e)))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue