Compare commits

..

No commits in common. "61d3e490daad79a7e8e9ba77fb5eddb6b432bd5a" and "0748466811b86df8b4473a898fda8f646379f1b6" have entirely different histories.

7 changed files with 89 additions and 184 deletions

View File

@ -389,11 +389,6 @@ Built with:
Special thanks to all contributors and the Common Lisp community. Special thanks to all contributors and the Common Lisp community.
* Credits
** Icons
- [[https://www.flaticon.com/free-icons/cycle][Cycle icons created by meaicon - Flaticon]] (sync.png reconnect button)
--- ---
*Last Updated: 2025-10-26* *Last Updated: 2025-10-26*

View File

@ -742,8 +742,6 @@
;; Front page - regular view by default ;; Front page - regular view by default
(define-page front-page #@"/" () (define-page front-page #@"/" ()
"Main front page" "Main front page"
;; Register this visitor for geo stats (captures real IP from X-Forwarded-For)
(register-web-listener)
(clip:process-to-string (clip:process-to-string
(load-template "front-page") (load-template "front-page")
:title "ASTEROID RADIO" :title "ASTEROID RADIO"
@ -786,8 +784,6 @@
;; Persistent audio player frame (bottom frame) ;; Persistent audio player frame (bottom frame)
(define-page audio-player-frame #@"/audio-player-frame" () (define-page audio-player-frame #@"/audio-player-frame" ()
"Persistent audio player frame (bottom of page)" "Persistent audio player frame (bottom of page)"
;; Register this visitor for geo stats (captures real IP from X-Forwarded-For)
(register-web-listener)
(clip:process-to-string (clip:process-to-string
(load-template "audio-player-frame") (load-template "audio-player-frame")
:stream-base-url *stream-base-url* :stream-base-url *stream-base-url*

View File

@ -1,9 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Ensure you copy this file to 'environment.sh' in this directory, and export ASTEROID_STREAM_URL='http://ice.asteroid.radio'
# change it to match your own stream configuration.
export ASTEROID_STREAM_URL='http://localhost:8080'
# source this file prior to starting the asteroid containers. Set the # source this file prior to starting the asteroid containers. Set the
# DB name and access params here. # DB name and access params here.

View File

@ -34,13 +34,6 @@
(defvar *active-listeners* (make-hash-table :test 'equal) (defvar *active-listeners* (make-hash-table :test 'equal)
"Hash table tracking active listeners by IP hash") "Hash table tracking active listeners by IP hash")
;;; Web request listener tracking (real IPs from X-Forwarded-For)
(defvar *web-listeners* (make-hash-table :test 'equal)
"Hash table tracking listeners by session - stores real IP from web requests")
(defvar *web-listener-timeout* 300
"Seconds before a web listener is considered inactive (5 minutes)")
;;; Geo lookup cache (IP hash -> country code) ;;; Geo lookup cache (IP hash -> country code)
(defvar *geo-cache* (make-hash-table :test 'equal) (defvar *geo-cache* (make-hash-table :test 'equal)
"Cache of IP hash to country code mappings") "Cache of IP hash to country code mappings")
@ -64,65 +57,6 @@
(format nil "~a-~a" (get-universal-time) (random 1000000)))) (format nil "~a-~a" (get-universal-time) (random 1000000))))
(subseq (ironclad:byte-array-to-hex-string (ironclad:produce-digest digest)) 0 32))) (subseq (ironclad:byte-array-to-hex-string (ironclad:produce-digest digest)) 0 32)))
;;; Web Listener Tracking (using real IPs from X-Forwarded-For)
(defun get-real-client-ip ()
"Get the real client IP from the current request.
Radiance automatically extracts X-Forwarded-For into (remote *request*)."
(when (boundp '*request*)
(let ((ip (remote *request*)))
;; Filter out private/internal IPs
(when (and ip
(not (string= ip "unknown"))
(not (search "172." ip))
(not (search "192.168." ip))
(not (search "10." ip))
(not (search "127." ip)))
ip))))
(defun register-web-listener ()
"Register current web request as an active listener.
Call this from player page or stream-related API endpoints."
(let ((ip (get-real-client-ip)))
(when ip
(let ((ip-hash (hash-ip-address ip)))
;; Use IP hash as key (no session required)
(setf (gethash ip-hash *web-listeners*)
(list :ip ip
:ip-hash ip-hash
:last-seen (get-universal-time)))
;; Do geo lookup and cache it
(unless (gethash ip-hash *geo-cache*)
(let ((geo (lookup-geoip ip)))
(when geo
(setf (gethash ip-hash *geo-cache*)
(list :country (getf geo :country-code)
:city (getf geo :city)
:region (getf geo :region)
:time (get-universal-time))))))
ip-hash))))
(defun cleanup-stale-web-listeners ()
"Remove web listeners that haven't been seen recently."
(let ((cutoff (- (get-universal-time) *web-listener-timeout*))
(to-remove nil))
(maphash (lambda (ip-hash data)
(when (< (getf data :last-seen) cutoff)
(push ip-hash to-remove)))
*web-listeners*)
(dolist (ip-hash to-remove)
(remhash ip-hash *web-listeners*))))
(defun get-active-web-listener-ips ()
"Get list of real IPs from active web listeners."
(cleanup-stale-web-listeners)
(let ((ips nil))
(maphash (lambda (session-id data)
(declare (ignore session-id))
(push (getf data :ip) ips))
*web-listeners*)
(remove-duplicates ips :test #'string=)))
;;; GeoIP Lookup ;;; GeoIP Lookup
(defun lookup-geoip (ip-address) (defun lookup-geoip (ip-address)
@ -416,7 +350,7 @@
country)))))) country))))))
(defun collect-geo-stats-for-mount (mount) (defun collect-geo-stats-for-mount (mount)
"Collect geo stats for all listeners on a mount (from Icecast - may show proxy IPs)" "Collect geo stats for all listeners on a mount"
(let ((listclients-xml (fetch-icecast-listclients mount))) (let ((listclients-xml (fetch-icecast-listclients mount)))
(when listclients-xml (when listclients-xml
(let ((ips (extract-listener-ips listclients-xml)) (let ((ips (extract-listener-ips listclients-xml))
@ -431,24 +365,6 @@
(update-geo-stats country count)) (update-geo-stats country count))
country-counts))))) country-counts)))))
(defun collect-geo-stats-from-web-listeners ()
"Collect geo stats from web listeners (uses real IPs from X-Forwarded-For)"
(cleanup-stale-web-listeners)
(let ((country-counts (make-hash-table :test 'equal)))
;; Count listeners by country from cached geo data
(maphash (lambda (session-id data)
(declare (ignore session-id))
(let* ((ip-hash (getf data :ip-hash))
(cached-geo (gethash ip-hash *geo-cache*))
(country (when cached-geo (getf cached-geo :country))))
(when country
(incf (gethash country country-counts 0)))))
*web-listeners*)
;; Store each country's count
(maphash (lambda (country count)
(update-geo-stats country count))
country-counts)))
(defun poll-and-store-stats () (defun poll-and-store-stats ()
"Single poll iteration: fetch stats and store" "Single poll iteration: fetch stats and store"
(let ((stats (fetch-icecast-stats))) (let ((stats (fetch-icecast-stats)))
@ -459,9 +375,10 @@
(listeners (getf source :listeners))) (listeners (getf source :listeners)))
(when mount (when mount
(store-listener-snapshot mount listeners) (store-listener-snapshot mount listeners)
(log:debug "Stored snapshot: ~a = ~a listeners" mount listeners))))))) ;; Collect geo stats if there are listeners
;; Collect geo stats from web listeners (uses real IPs from X-Forwarded-For) (when (and listeners (> listeners 0))
(collect-geo-stats-from-web-listeners)) (collect-geo-stats-for-mount mount))
(log:debug "Stored snapshot: ~a = ~a listeners" mount listeners))))))))
(defun stats-polling-loop () (defun stats-polling-loop ()
"Main polling loop - runs in background thread" "Main polling loop - runs in background thread"

View File

@ -408,7 +408,7 @@
;; Periodically update stream info if in frameset ;; Periodically update stream info if in frameset
(let ((is-frameset-page (not (= (ps:@ window parent) (ps:@ window self))))) (let ((is-frameset-page (not (= (ps:@ window parent) (ps:@ window self)))))
(when is-frameset-page (when is-frameset-page
(set-interval update-stream-information 10000))) (set-interval update-stream-information 1000)))
;; Update now playing ;; Update now playing
(update-now-playing) (update-now-playing)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 904 B

View File

@ -29,7 +29,7 @@
<span class="now-playing-mini" id="mini-now-playing">Loading...</span> <span class="now-playing-mini" id="mini-now-playing">Loading...</span>
<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%);"> 🔄
</button> </button>
<button onclick="disableFramesetMode()" class="persistent-disable-btn"> <button onclick="disableFramesetMode()" class="persistent-disable-btn">