Compare commits

..

1 Commits

Author SHA1 Message Date
glenneth d82ac5f9a8 Add system notifications for track changes
- Implement Web Notifications API in ParenScript
- Add notification toggle button (🔔/🔕) to player frame
- Show 'Artist - Track' notification when track changes
- Store notification preference in localStorage
- Auto-close notifications after 5 seconds
- Click notification to focus browser window
2026-01-18 12:54:36 +03:00
5 changed files with 12 additions and 27 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

@ -1403,11 +1403,10 @@
("avg_session_minutes" . ,(sixth row)))) ("avg_session_minutes" . ,(sixth row))))
stats)))))) stats))))))
(define-api asteroid/stats/geo (&optional (days "7") (sort-by "minutes")) () (define-api asteroid/stats/geo (&optional (days "7")) ()
"Get geographic distribution of listeners (admin only). "Get geographic distribution of listeners (admin only)"
SORT-BY can be 'minutes' (default) or 'listeners'."
(require-role :admin) (require-role :admin)
(let ((stats (get-geo-stats (parse-integer days :junk-allowed t) sort-by))) (let ((stats (get-geo-stats (parse-integer days :junk-allowed t))))
(api-output `(("status" . "success") (api-output `(("status" . "success")
("geo" . ,(mapcar (lambda (row) ("geo" . ,(mapcar (lambda (row)
`(("country_code" . ,(first row)) `(("country_code" . ,(first row))

View File

@ -369,21 +369,17 @@
(log:error "Failed to get daily stats: ~a" e) (log:error "Failed to get daily stats: ~a" e)
nil))) nil)))
(defun get-geo-stats (&optional (days 7) (order-by "minutes")) (defun get-geo-stats (&optional (days 7))
"Get geographic distribution for the last N days. "Get geographic distribution for the last N days"
ORDER-BY can be 'minutes' (default) or 'listeners'."
(handler-case (handler-case
(with-db (with-db
(let ((order-column (if (string= order-by "listeners") (postmodern:query
"total_listeners" (format nil "SELECT country_code, SUM(listener_count) as total_listeners, SUM(listen_minutes) as total_minutes
"total_minutes")))
(postmodern:query
(format nil "SELECT country_code, SUM(listener_count) as total_listeners, SUM(listen_minutes) as total_minutes
FROM listener_geo_stats FROM listener_geo_stats
WHERE date > NOW() - INTERVAL '~a days' WHERE date > NOW() - INTERVAL '~a days'
GROUP BY country_code GROUP BY country_code
ORDER BY ~a DESC ORDER BY total_listeners DESC
LIMIT 20" days order-column)))) LIMIT 20" days)))
(error (e) (error (e)
(log:error "Failed to get geo stats: ~a" e) (log:error "Failed to get geo stats: ~a" e)
nil))) nil)))

View File

@ -970,10 +970,8 @@
;; Refresh geo stats from API ;; Refresh geo stats from API
(defun refresh-geo-stats () (defun refresh-geo-stats ()
(let* ((sort-select (ps:chain document (get-element-by-id "geo-sort-by"))) (ps:chain
(sort-by (if sort-select (ps:@ sort-select value) "minutes"))) (fetch "/api/asteroid/stats/geo?days=7")
(ps:chain
(fetch (+ "/api/asteroid/stats/geo?days=7&sort-by=" sort-by))
(then (lambda (response) (ps:chain response (json)))) (then (lambda (response) (ps:chain response (json))))
(then (lambda (result) (then (lambda (result)
(let ((data (or (ps:@ result data) result)) (let ((data (or (ps:@ result data) result))
@ -1011,7 +1009,7 @@
(let ((tbody (ps:chain document (get-element-by-id "geo-stats-body")))) (let ((tbody (ps:chain document (get-element-by-id "geo-stats-body"))))
(when tbody (when tbody
(setf (ps:@ tbody inner-h-t-m-l) (setf (ps:@ tbody inner-h-t-m-l)
"<tr><td colspan=\"3\" style=\"color: #ff6666;\">Error loading geo data</td></tr>")))))))) "<tr><td colspan=\"3\" style=\"color: #ff6666;\">Error loading geo data</td></tr>")))))))
;; Toggle city display for a country ;; Toggle city display for a country
(defun toggle-country-cities (country) (defun toggle-country-cities (country)

View File

@ -86,13 +86,6 @@
<!-- Geo Stats --> <!-- Geo Stats -->
<h3 style="margin-top: 20px;">🌍 Listener Locations (Last 7 Days)</h3> <h3 style="margin-top: 20px;">🌍 Listener Locations (Last 7 Days)</h3>
<div style="margin-bottom: 10px;">
<label for="geo-sort-by">Sort by: </label>
<select id="geo-sort-by" class="sort-select" onchange="refreshGeoStats()">
<option value="minutes" selected>Minutes Listened</option>
<option value="listeners">Unique Listeners</option>
</select>
</div>
<div id="geo-stats-container"> <div id="geo-stats-container">
<table class="listener-stats-table" id="geo-stats-table"> <table class="listener-stats-table" id="geo-stats-table">
<thead> <thead>