Compare commits
No commits in common. "20ed7ecb0216bce12a44e92b9b482b06ce8840e1" and "76d331248b36bcdd43fe0b47da665c326f2dea3c" have entirely different histories.
20ed7ecb02
...
76d331248b
|
|
@ -1 +1 @@
|
||||||
Subproject commit cc4215d1c663c5aed4e9758c755a944016fa6aaa
|
Subproject commit 23b5ee1e35e6c967cab12751ac7b8930d21b3307
|
||||||
|
|
@ -120,14 +120,12 @@
|
||||||
(artist (getf parsed :artist))
|
(artist (getf parsed :artist))
|
||||||
(song (getf parsed :song))
|
(song (getf parsed :song))
|
||||||
(search-url (generate-music-search-url artist song)))
|
(search-url (generate-music-search-url artist song)))
|
||||||
(let ((remaining (cdr (assoc :remaining now-playing-stats))))
|
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("title" . ,title)
|
("title" . ,title)
|
||||||
("listeners" . ,(cdr (assoc :listeners now-playing-stats)))
|
("listeners" . ,(cdr (assoc :listeners now-playing-stats)))
|
||||||
("track_id" . ,(cdr (assoc :track-id now-playing-stats)))
|
("track_id" . ,(cdr (assoc :track-id now-playing-stats)))
|
||||||
("favorite_count" . ,favorite-count)
|
("favorite_count" . ,favorite-count)
|
||||||
("search_url" . ,search-url)
|
("search_url" . ,search-url))))
|
||||||
,@(when remaining `(("remaining" . ,remaining)))))))
|
|
||||||
(api-output `(("status" . "offline")
|
(api-output `(("status" . "offline")
|
||||||
("title" . "Stream Offline")
|
("title" . "Stream Offline")
|
||||||
("track_id" . nil)))))))
|
("track_id" . nil)))))))
|
||||||
|
|
|
||||||
|
|
@ -866,52 +866,7 @@
|
||||||
(ps:chain console (log "Could not load recent requests:" error))))))))
|
(ps:chain console (log "Could not load recent requests:" error))))))))
|
||||||
|
|
||||||
;; Load recent requests on page load
|
;; Load recent requests on page load
|
||||||
(load-recent-requests)
|
(load-recent-requests)))
|
||||||
|
|
||||||
;; Main page countdown timer
|
|
||||||
(defvar *main-remaining* nil)
|
|
||||||
|
|
||||||
(defun format-countdown (seconds)
|
|
||||||
(let ((m (ps:chain -math (floor (/ seconds 60))))
|
|
||||||
(s (ps:chain -math (floor (mod seconds 60)))))
|
|
||||||
(+ (if (< m 10) (+ "0" m) m) ":" (if (< s 10) (+ "0" s) s))))
|
|
||||||
|
|
||||||
(defun poll-now-playing ()
|
|
||||||
(let ((mount (or (ps:chain local-storage (get-item "stream-mount")) "asteroid.mp3")))
|
|
||||||
(ps:chain
|
|
||||||
(fetch (+ "/api/asteroid/partial/now-playing-json?mount=" mount))
|
|
||||||
(then (lambda (response)
|
|
||||||
(if (ps:@ response ok)
|
|
||||||
(ps:chain response (json))
|
|
||||||
nil)))
|
|
||||||
(then (lambda (data)
|
|
||||||
(when data
|
|
||||||
(let ((title (or (ps:@ data data title) (ps:@ data title)))
|
|
||||||
(remaining (or (ps:@ data data remaining) (ps:@ data remaining)))
|
|
||||||
(listeners (or (ps:@ data data listeners) (ps:@ data listeners)))
|
|
||||||
(title-el (ps:chain document (get-element-by-id "current-track-title")))
|
|
||||||
(listener-el (ps:chain document (get-element-by-id "current-listeners"))))
|
|
||||||
(when (and title-el title)
|
|
||||||
(setf (ps:@ title-el text-content) title))
|
|
||||||
(when (and listener-el listeners)
|
|
||||||
(setf (ps:@ listener-el text-content) listeners))
|
|
||||||
(when remaining
|
|
||||||
(setf *main-remaining* remaining))))))
|
|
||||||
(catch (lambda (error) nil)))))
|
|
||||||
|
|
||||||
;; Start polling and countdown ticker on the main page
|
|
||||||
(set-timeout poll-now-playing 2000)
|
|
||||||
(set-interval poll-now-playing 15000)
|
|
||||||
(set-interval
|
|
||||||
(lambda ()
|
|
||||||
(let ((el (ps:chain document (get-element-by-id "track-countdown-main"))))
|
|
||||||
(when el
|
|
||||||
(if (and *main-remaining* (> *main-remaining* 0))
|
|
||||||
(progn
|
|
||||||
(decf *main-remaining*)
|
|
||||||
(setf (ps:@ el text-content) (+ "[" (format-countdown *main-remaining*) "]")))
|
|
||||||
(setf (ps:@ el text-content) "")))))
|
|
||||||
1000)))
|
|
||||||
"Compiled JavaScript for front-page - generated at load time")
|
"Compiled JavaScript for front-page - generated at load time")
|
||||||
|
|
||||||
(defun generate-front-page-js ()
|
(defun generate-front-page-js ()
|
||||||
|
|
|
||||||
|
|
@ -336,30 +336,6 @@
|
||||||
;; Track last notified title to avoid duplicate notifications
|
;; Track last notified title to avoid duplicate notifications
|
||||||
(defvar *last-notified-title* nil)
|
(defvar *last-notified-title* nil)
|
||||||
|
|
||||||
;; Countdown timer state
|
|
||||||
(defvar *track-remaining-seconds* nil)
|
|
||||||
(defvar *countdown-interval* nil)
|
|
||||||
|
|
||||||
(defun format-countdown (seconds)
|
|
||||||
(let ((m (ps:chain -math (floor (/ seconds 60))))
|
|
||||||
(s (ps:chain -math (floor (mod seconds 60)))))
|
|
||||||
(+ (if (< m 10) (+ "0" m) m) ":" (if (< s 10) (+ "0" s) s))))
|
|
||||||
|
|
||||||
(defun start-countdown-ticker ()
|
|
||||||
(when *countdown-interval*
|
|
||||||
(clear-interval *countdown-interval*))
|
|
||||||
(setf *countdown-interval*
|
|
||||||
(set-interval
|
|
||||||
(lambda ()
|
|
||||||
(let ((el (ps:chain document (get-element-by-id "track-countdown"))))
|
|
||||||
(when el
|
|
||||||
(if (and *track-remaining-seconds* (> *track-remaining-seconds* 0))
|
|
||||||
(progn
|
|
||||||
(decf *track-remaining-seconds*)
|
|
||||||
(setf (ps:@ el text-content) (+ "[" (format-countdown *track-remaining-seconds*) "]")))
|
|
||||||
(setf (ps:@ el text-content) "")))))
|
|
||||||
1000)))
|
|
||||||
|
|
||||||
;; Check if notifications are enabled in localStorage
|
;; Check if notifications are enabled in localStorage
|
||||||
(defun notifications-enabled-p ()
|
(defun notifications-enabled-p ()
|
||||||
(= (ps:chain local-storage (get-item "notifications-enabled")) "true"))
|
(= (ps:chain local-storage (get-item "notifications-enabled")) "true"))
|
||||||
|
|
@ -434,12 +410,6 @@
|
||||||
|
|
||||||
;; Show a system notification for track change
|
;; Show a system notification for track change
|
||||||
(defun show-track-notification (title body)
|
(defun show-track-notification (title body)
|
||||||
(ps:chain console (log "[NOTIFY] show-track-notification called:"
|
|
||||||
"supported=" (notifications-supported-p)
|
|
||||||
"permission=" (get-notification-permission)
|
|
||||||
"enabled=" (notifications-enabled-p)
|
|
||||||
"last=" *last-notified-title*
|
|
||||||
"title=" title))
|
|
||||||
(when (and (notifications-supported-p)
|
(when (and (notifications-supported-p)
|
||||||
(= (get-notification-permission) "granted")
|
(= (get-notification-permission) "granted")
|
||||||
(notifications-enabled-p)
|
(notifications-enabled-p)
|
||||||
|
|
@ -452,7 +422,6 @@
|
||||||
:tag "asteroid-track-change"
|
:tag "asteroid-track-change"
|
||||||
:renotify true
|
:renotify true
|
||||||
:silent false)))))
|
:silent false)))))
|
||||||
(ps:chain console (log "[NOTIFY] Notification created successfully"))
|
|
||||||
;; Auto-close after 5 seconds
|
;; Auto-close after 5 seconds
|
||||||
(set-timeout (lambda () (ps:chain notification (close))) 5000)
|
(set-timeout (lambda () (ps:chain notification (close))) 5000)
|
||||||
;; Click to focus the window
|
;; Click to focus the window
|
||||||
|
|
@ -461,7 +430,7 @@
|
||||||
(ps:chain window (focus))
|
(ps:chain window (focus))
|
||||||
(ps:chain notification (close)))))
|
(ps:chain notification (close)))))
|
||||||
(:catch (e)
|
(:catch (e)
|
||||||
(ps:chain console (log "[NOTIFY] Notification error:" e))))))
|
(ps:chain console (log "Notification error:" e))))))
|
||||||
|
|
||||||
;; Notify track change (called from update-mini-now-playing)
|
;; Notify track change (called from update-mini-now-playing)
|
||||||
(defun notify-track-change (title)
|
(defun notify-track-change (title)
|
||||||
|
|
@ -545,15 +514,16 @@
|
||||||
(when el
|
(when el
|
||||||
;; Check if track changed and record to history + notify
|
;; Check if track changed and record to history + notify
|
||||||
(when (not (= (ps:@ el text-content) title))
|
(when (not (= (ps:@ el text-content) title))
|
||||||
(ps:chain console (log "[STREAM-SYNC] Title changed:" title))
|
|
||||||
(record-track-listen title)
|
(record-track-listen title)
|
||||||
(notify-track-change 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-favorite-status-mini))
|
(check-favorite-status-mini))
|
||||||
(update-media-session title)
|
(update-media-session title)
|
||||||
(when track-id-el
|
(when track-id-el
|
||||||
(let ((track-id (or (ps:@ data data track_id) (ps:@ data track_id))))
|
(let ((track-id (or (ps:@ data data track_id) (ps:@ data track_id))))
|
||||||
(setf (ps:@ track-id-el value) (or track-id ""))))
|
(setf (ps:@ track-id-el value) (or track-id ""))))
|
||||||
|
;; Update favorite count display
|
||||||
(let ((count-el (ps:chain document (get-element-by-id "favorite-count-mini")))
|
(let ((count-el (ps:chain document (get-element-by-id "favorite-count-mini")))
|
||||||
(fav-count (or (ps:@ data data favorite_count) (ps:@ data favorite_count) 0)))
|
(fav-count (or (ps:@ data data favorite_count) (ps:@ data favorite_count) 0)))
|
||||||
(when count-el
|
(when count-el
|
||||||
|
|
@ -561,10 +531,7 @@
|
||||||
((= fav-count 0) (setf (ps:@ count-el text-content) ""))
|
((= fav-count 0) (setf (ps:@ count-el text-content) ""))
|
||||||
((= fav-count 1) (setf (ps:@ count-el text-content) "1 ❤️"))
|
((= fav-count 1) (setf (ps:@ count-el text-content) "1 ❤️"))
|
||||||
(t (setf (ps:@ count-el text-content) (+ fav-count " ❤️"))))))
|
(t (setf (ps:@ count-el text-content) (+ fav-count " ❤️"))))))
|
||||||
;; Sync countdown timer from server
|
;; Update MusicBrainz search link
|
||||||
(let ((remaining (or (ps:@ data data remaining) (ps:@ data remaining))))
|
|
||||||
(when remaining
|
|
||||||
(setf *track-remaining-seconds* remaining)))
|
|
||||||
(let ((mb-link (ps:chain document (get-element-by-id "mini-musicbrainz-link")))
|
(let ((mb-link (ps:chain document (get-element-by-id "mini-musicbrainz-link")))
|
||||||
(search-url (or (ps:@ data data search_url) (ps:@ data search_url))))
|
(search-url (or (ps:@ data data search_url) (ps:@ data search_url))))
|
||||||
(when mb-link
|
(when mb-link
|
||||||
|
|
@ -757,35 +724,6 @@
|
||||||
(catch (lambda (err)
|
(catch (lambda (err)
|
||||||
(ps:chain console (log "Reconnect failed:" err))))))
|
(ps:chain console (log "Reconnect failed:" err))))))
|
||||||
|
|
||||||
;; Buffer bloat detection and reset
|
|
||||||
(defvar *max-buffer-seconds* 15)
|
|
||||||
(defvar *buffer-check-interval* nil)
|
|
||||||
|
|
||||||
(defun get-buffer-ahead (audio-element)
|
|
||||||
"Return seconds of audio buffered ahead of current playback position."
|
|
||||||
(ps:try
|
|
||||||
(when (and (ps:@ audio-element buffered)
|
|
||||||
(> (ps:@ audio-element buffered length) 0))
|
|
||||||
(- (ps:chain audio-element buffered (end (- (ps:@ audio-element buffered length) 1)))
|
|
||||||
(ps:@ audio-element current-time)))
|
|
||||||
(:catch (e) 0)))
|
|
||||||
|
|
||||||
(defun start-buffer-monitor (audio-element)
|
|
||||||
(when *buffer-check-interval*
|
|
||||||
(clear-interval *buffer-check-interval*))
|
|
||||||
(setf *buffer-check-interval*
|
|
||||||
(set-interval
|
|
||||||
(lambda ()
|
|
||||||
(when (and (not (ps:@ audio-element paused))
|
|
||||||
(not *is-reconnecting*))
|
|
||||||
(let ((ahead (get-buffer-ahead audio-element)))
|
|
||||||
(when (and ahead (> ahead 5))
|
|
||||||
(ps:chain console (log (+ "[BUFFER] " (ps:chain ahead (to-fixed 1)) "s ahead"))))
|
|
||||||
(when (and ahead (> ahead *max-buffer-seconds*))
|
|
||||||
(ps:chain console (log (+ "[BUFFER] Bloat detected (" (ps:chain ahead (to-fixed 1)) "s), resetting stream")))
|
|
||||||
(reconnect-stream)))))
|
|
||||||
30000)))
|
|
||||||
|
|
||||||
;; Attach event listeners to audio element
|
;; Attach event listeners to audio element
|
||||||
(defun attach-audio-listeners (audio-element)
|
(defun attach-audio-listeners (audio-element)
|
||||||
(ps:chain audio-element
|
(ps:chain audio-element
|
||||||
|
|
@ -969,9 +907,8 @@
|
||||||
:artist "Asteroid Radio"
|
:artist "Asteroid Radio"
|
||||||
:album "Live Broadcast")))))
|
:album "Live Broadcast")))))
|
||||||
|
|
||||||
;; Attach event listeners and buffer monitor
|
;; Attach event listeners
|
||||||
(attach-audio-listeners audio-element)
|
(attach-audio-listeners audio-element)
|
||||||
(start-buffer-monitor audio-element)
|
|
||||||
|
|
||||||
;; Restore user channel preference
|
;; Restore user channel preference
|
||||||
(let ((channel-selector (ps:chain document (get-element-by-id "stream-channel")))
|
(let ((channel-selector (ps:chain document (get-element-by-id "stream-channel")))
|
||||||
|
|
@ -1021,10 +958,9 @@
|
||||||
(ps:chain console (log "Could not fetch channel name:" error))))))
|
(ps:chain console (log "Could not fetch channel name:" error))))))
|
||||||
15000)) ;; Poll every 15 seconds
|
15000)) ;; Poll every 15 seconds
|
||||||
|
|
||||||
;; Start now playing updates and countdown ticker
|
;; Start now playing updates
|
||||||
(set-timeout update-mini-now-playing 1000)
|
(set-timeout update-mini-now-playing 1000)
|
||||||
(set-interval update-mini-now-playing 15000)
|
(set-interval update-mini-now-playing 15000))))
|
||||||
(start-countdown-ticker))))
|
|
||||||
|
|
||||||
;; Initialize popout player
|
;; Initialize popout player
|
||||||
(defun init-popout-player ()
|
(defun init-popout-player ()
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
#EXTM3U
|
|
||||||
#PLAYLIST:Deep Focus
|
|
||||||
#PHASE:Deep Focus
|
|
||||||
#DURATION:5 hours (approx)
|
|
||||||
#CURATOR:Asteroid Radio
|
|
||||||
#DESCRIPTION:Upbeat and cheerful electronic music for long coding sessions - IDM, shoegaze electronica, melodic techno, and ambient rhythms
|
|
||||||
|
|
||||||
#EXTINF:-1,Tycho - Glider
|
|
||||||
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/01 - Glider.flac
|
|
||||||
#EXTINF:-1,Boards of Canada - Spectrum
|
|
||||||
/app/music/Boards of Canada/A Few Old Tunes/01 - Spectrum.mp3
|
|
||||||
#EXTINF:-1,Ulrich Schnauss - Melts into Air
|
|
||||||
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/01. Melts into Air (2019 Version).flac
|
|
||||||
#EXTINF:-1,Plaid - Do Matter
|
|
||||||
/app/music/Plaid - The Digging Remedy (2016) [FLAC]/01 - Do Matter.flac
|
|
||||||
#EXTINF:-1,Four Tet - Two Thousand And Seventeen
|
|
||||||
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/02 Two Thousand And Seventeen.flac
|
|
||||||
#EXTINF:-1,Kiasmos - Lit
|
|
||||||
/app/music/Kiasmos/2014 - Kiasmos/01 - Lit.flac
|
|
||||||
#EXTINF:-1,Proem - A Good Soaking
|
|
||||||
/app/music/Proem - Twelve Tails-(2021) @FLAC [16-48]/12 - A Good Soaking.flac
|
|
||||||
#EXTINF:-1,Cut Copy - Standing in the Middle of the Field
|
|
||||||
/app/music/Cut Copy - Haiku From Zero (2017) [FLAC] {2557864014}/01 - Standing in the Middle of the Field.flac
|
|
||||||
#EXTINF:-1,Marconi Union - Sleeper
|
|
||||||
/app/music/Marconi Union - Ghost Stations (2016 - WEB - FLAC)/01. Marconi Union - Sleeper.flac
|
|
||||||
#EXTINF:-1,Clark - Peak Magnetic
|
|
||||||
/app/music/Clark - Death Peak (2017) [FLAC]/03 - Peak Magnetic.flac
|
|
||||||
#EXTINF:-1,Tycho - Weather
|
|
||||||
/app/music/Tycho - Simulcast (2020) [WEB FLAC]/01 - Weather.flac
|
|
||||||
#EXTINF:-1,Boards of Canada - Happy Cycling
|
|
||||||
/app/music/Boards of Canada/A Few Old Tunes/06 - Happy Cycling.mp3
|
|
||||||
#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Asteroid 2467
|
|
||||||
/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/01. Asteroid 2467.flac
|
|
||||||
#EXTINF:-1,Faux Tales - Avalon
|
|
||||||
/app/music/Faux Tales - 2015 - Kairos [FLAC] {Kensai Records KNS006 WEB}/3 - Avalon.flac
|
|
||||||
#EXTINF:-1,Vector Lovers - City Lights From A Train
|
|
||||||
/app/music/Vector Lovers/2005 - Capsule For One/01 - City Lights From A Train.mp3
|
|
||||||
#EXTINF:-1,Plaid - Maru
|
|
||||||
/app/music/Plaid - Polymer (2019) [WEB FLAC]/03 - Maru.flac
|
|
||||||
#EXTINF:-1,Four Tet - Baby
|
|
||||||
/app/music/Four Tet - Sixteen Oceans (2020) {Text Records - TEXT051} [CD FLAC]/02 - Four Tet - Baby.flac
|
|
||||||
#EXTINF:-1,Bluetech - Laika
|
|
||||||
/app/music/Bluetech - Spacehop Chronicles Vol. 1 (flac)/01. Laika.flac
|
|
||||||
#EXTINF:-1,Kiasmos - Looped
|
|
||||||
/app/music/Kiasmos/2014 - Kiasmos/03 - Looped.flac
|
|
||||||
#EXTINF:-1,Proem - Snow Drifts
|
|
||||||
/app/music/Proem - Twelve Tails-(2021) @FLAC [16-48]/11 - Snow Drifts.flac
|
|
||||||
#EXTINF:-1,Thievery Corporation - Fragments (Tycho Remix)
|
|
||||||
/app/music/Thievery Corporation and Tycho - Fragments Ascension EP (flac)/2. Thievery Corporation - Fragments (Tycho Remix).flac
|
|
||||||
#EXTINF:-1,Ulrich Schnauss - Love Grows Out of Thin Air
|
|
||||||
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/02. Love Grows Out of Thin Air (2019 Version).flac
|
|
||||||
#EXTINF:-1,Clark - Kiri's Glee
|
|
||||||
/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/04 - Kiri's Glee.flac
|
|
||||||
#EXTINF:-1,Cut Copy - Airborne
|
|
||||||
/app/music/Cut Copy - Haiku From Zero (2017) [FLAC] {2557864014}/05 - Airborne.flac
|
|
||||||
#EXTINF:-1,Marconi Union - Riser
|
|
||||||
/app/music/Marconi Union - Ghost Stations (2016 - WEB - FLAC)/04. Marconi Union - Riser.flac
|
|
||||||
#EXTINF:-1,Boards of Canada - Forest Moon
|
|
||||||
/app/music/Boards of Canada/A Few Old Tunes/09 - Forest Moon.mp3
|
|
||||||
#EXTINF:-1,Tycho - Ascension
|
|
||||||
/app/music/Thievery Corporation and Tycho - Fragments Ascension EP (flac)/3. Tycho - Ascension.flac
|
|
||||||
#EXTINF:-1,Plaid - Dancers
|
|
||||||
/app/music/Plaid - Polymer (2019) [WEB FLAC]/07 - Dancers.flac
|
|
||||||
#EXTINF:-1,Quaeschning & Ulrich Schnauss - A Calm but Steady Flow
|
|
||||||
/app/music/Quaeschning and Ulrich Schnauss - Synthwaves (2017) {vista003, GER, CD} [FLAC]/05 - A Calm but Steady Flow.flac
|
|
||||||
#EXTINF:-1,Bitcrush - Engale
|
|
||||||
/app/music/Bitcrush - Enarc (flac)/01 - Engale.flac
|
|
||||||
#EXTINF:-1,Four Tet - Parallel 1
|
|
||||||
/app/music/Four Tet - Parallel (2020) - WEB FLAC/01. Parallel 1.flac
|
|
||||||
#EXTINF:-1,woob - INNA
|
|
||||||
/app/music/woob - Mass Distraction EP [WEB FLAC 24]/woob - Mass Distraction EP - 01 INNA.flac
|
|
||||||
#EXTINF:-1,Vector Lovers - Nostalgia 4 The Future
|
|
||||||
/app/music/Vector Lovers/2005 - Capsule For One/05 - Nostalgia 4 The Future.mp3
|
|
||||||
#EXTINF:-1,Kiasmos - Swayed
|
|
||||||
/app/music/Kiasmos/2014 - Kiasmos/04 - Swayed.flac
|
|
||||||
#EXTINF:-1,Proem - Keep This Whole
|
|
||||||
/app/music/Proem - Twelve Tails-(2021) @FLAC [16-48]/10 - Keep This Whole.flac
|
|
||||||
#EXTINF:-1,Ulrich Schnauss - Her and the Sea
|
|
||||||
/app/music/Ulrich Schnauss - A Long Way To Fall - Rebound (2020) - WEB FLAC/01. Her and the Sea.flac
|
|
||||||
#EXTINF:-1,Faux Tales - Oceania
|
|
||||||
/app/music/Faux Tales - 2015 - Kairos [FLAC] {Kensai Records KNS006 WEB}/4 - Oceania.flac
|
|
||||||
#EXTINF:-1,Cut Copy - Stars Last Me a Lifetime
|
|
||||||
/app/music/Cut Copy - Haiku From Zero (2017) [FLAC] {2557864014}/04 - Stars Last Me a Lifetime.flac
|
|
||||||
#EXTINF:-1,Plaid - The Bee
|
|
||||||
/app/music/Plaid - The Digging Remedy (2016) [FLAC]/04 - The Bee.flac
|
|
||||||
#EXTINF:-1,Clark - Living Fantasy
|
|
||||||
/app/music/Clark - Death Peak (2017) [FLAC]/08 - Living Fantasy.flac
|
|
||||||
#EXTINF:-1,Tycho - Japan (Satin Jackets Remix)
|
|
||||||
/app/music/Tycho - Weather Remixes (2020) - WEB FLAC/03. Japan (Satin Jackets Remix).flac
|
|
||||||
#EXTINF:-1,Boards of Canada - Skimming Stones
|
|
||||||
/app/music/Boards of Canada/A Few Old Tunes/10 - Skimming Stones.mp3
|
|
||||||
#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Perpetual Motion
|
|
||||||
/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/04. Perpetual Motion.flac
|
|
||||||
#EXTINF:-1,Bluetech - Skybox
|
|
||||||
/app/music/Bluetech - Spacehop Chronicles Vol. 1 (flac)/04. Skybox.flac
|
|
||||||
#EXTINF:-1,God is an Astronaut - Epitaph
|
|
||||||
/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/01. Epitaph.flac
|
|
||||||
#EXTINF:-1,Seba & Ulrich Schnauss - M7
|
|
||||||
/app/music/Seba & Ulrich Schnauss - Snöflingor EP [2017] [WEB_FLAC]/01. Seba & Ulrich Schnauss - M7.flac
|
|
||||||
#EXTINF:-1,Quaeschning & Ulrich Schnauss - Prism
|
|
||||||
/app/music/Quaeschning and Ulrich Schnauss - Synthwaves (2017) {vista003, GER, CD} [FLAC]/08 - Prism.flac
|
|
||||||
#EXTINF:-1,High Energy Protons - The Heavens (Monolith mix)
|
|
||||||
/app/music/High Energy Protons/03 - The Heavens (Monolith mix).mp3
|
|
||||||
#EXTINF:-1,woob - Subterranean District
|
|
||||||
/app/music/woob - Tokyo Run - Series 8 Keycard - Lv.6 [2017] WEB [FLAC24] [16-44]/Tokyo Run - 24 Bit Masters/06 woob - Subterranean District.flac
|
|
||||||
#EXTINF:-1,Bitcrush - Two Go From There
|
|
||||||
/app/music/Bitcrush - Enarc (flac)/03 - Two Go From There.flac
|
|
||||||
#EXTINF:-1,Daft Punk - Derezzed
|
|
||||||
/app/music/Daft Punk - TRON Legacy - The Complete Edition (2020 - WEB - FLAC)/13 Derezzed.flac
|
|
||||||
#EXTINF:-1,God is an Astronaut - Komorebi
|
|
||||||
/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/05. Komorebi.flac
|
|
||||||
#EXTINF:-1,Daft Punk - The Son of Flynn
|
|
||||||
/app/music/Daft Punk - TRON Legacy - The Complete Edition (2020 - WEB - FLAC)/03 The Son of Flynn.flac
|
|
||||||
|
|
@ -1372,14 +1372,6 @@ body.persistent-player-container .now-playing-mini{
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.persistent-player-container .track-countdown-mini{
|
|
||||||
color: #888;
|
|
||||||
font-size: 0.75em;
|
|
||||||
font-family: monospace;
|
|
||||||
margin-left: 6px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.persistent-player-container .persistent-reconnect-btn{
|
body.persistent-player-container .persistent-reconnect-btn{
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #00ff00;
|
color: #00ff00;
|
||||||
|
|
@ -1603,13 +1595,6 @@ body.popout-body .status-mini{
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-countdown{
|
|
||||||
color: #888;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-family: monospace;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.now-playing-track{
|
.now-playing-track{
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -1108,13 +1108,6 @@
|
||||||
:flex 1
|
:flex 1
|
||||||
:min-width "300px")
|
:min-width "300px")
|
||||||
|
|
||||||
(.track-countdown-mini
|
|
||||||
:color "#888"
|
|
||||||
:font-size "0.75em"
|
|
||||||
:font-family "monospace"
|
|
||||||
:margin-left "6px"
|
|
||||||
:white-space "nowrap")
|
|
||||||
|
|
||||||
(.persistent-reconnect-btn
|
(.persistent-reconnect-btn
|
||||||
:background transparent
|
:background transparent
|
||||||
:color "#00ff00"
|
:color "#00ff00"
|
||||||
|
|
@ -1295,12 +1288,6 @@
|
||||||
(.craftering
|
(.craftering
|
||||||
(a :margin "0 5px")))
|
(a :margin "0 5px")))
|
||||||
|
|
||||||
(.track-countdown
|
|
||||||
:color "#888"
|
|
||||||
:font-size "0.8em"
|
|
||||||
:font-family "monospace"
|
|
||||||
:margin-left "8px")
|
|
||||||
|
|
||||||
;; Now playing favorite button
|
;; Now playing favorite button
|
||||||
(.now-playing-track
|
(.now-playing-track
|
||||||
:display "flex"
|
:display "flex"
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,6 @@
|
||||||
(setf *current-playlist-path* playlist-path)
|
(setf *current-playlist-path* playlist-path)
|
||||||
(log:info "Playlist now active: ~A" (file-namestring playlist-path)))
|
(log:info "Playlist now active: ~A" (file-namestring playlist-path)))
|
||||||
|
|
||||||
(defvar *pending-save-file* nil
|
|
||||||
"The file path that will be saved on the NEXT track change.
|
|
||||||
This one-track delay ensures we persist the track that was actually playing,
|
|
||||||
not the one being loaded during crossfade.")
|
|
||||||
|
|
||||||
(defun on-harmony-track-change (pipeline track-info)
|
(defun on-harmony-track-change (pipeline track-info)
|
||||||
"Called by cl-streamer when a track changes.
|
"Called by cl-streamer when a track changes.
|
||||||
Updates recently-played lists and finds the track in the database."
|
Updates recently-played lists and finds the track in the database."
|
||||||
|
|
@ -173,10 +168,9 @@
|
||||||
:track-id track-id)
|
:track-id track-id)
|
||||||
:curated)
|
:curated)
|
||||||
(setf *last-known-track-curated* display-title))
|
(setf *last-known-track-curated* display-title))
|
||||||
;; Save the PREVIOUS track (which was actually playing) and queue this one
|
;; Persist current track for resume-on-restart
|
||||||
(when *pending-save-file*
|
(when file-path
|
||||||
(save-playback-state *pending-save-file*))
|
(save-playback-state file-path))
|
||||||
(setf *pending-save-file* file-path)
|
|
||||||
(log:info "Track change: ~A (track-id: ~A)" display-title track-id)))
|
(log:info "Track change: ~A (track-id: ~A)" display-title track-id)))
|
||||||
|
|
||||||
(defun find-track-by-file-path (file-path)
|
(defun find-track-by-file-path (file-path)
|
||||||
|
|
@ -197,41 +191,19 @@
|
||||||
|
|
||||||
(defun harmony-now-playing (&optional (mount "asteroid.mp3"))
|
(defun harmony-now-playing (&optional (mount "asteroid.mp3"))
|
||||||
"Get now-playing information from cl-streamer pipeline.
|
"Get now-playing information from cl-streamer pipeline.
|
||||||
Uses the metadata timeline to report what listeners are actually hearing,
|
Returns an alist with now-playing data, or NIL if the pipeline is not running."
|
||||||
accounting for ring buffer and browser decode buffering."
|
|
||||||
(when (and *harmony-pipeline*
|
(when (and *harmony-pipeline*
|
||||||
(cl-streamer/harmony:pipeline-current-track *harmony-pipeline*))
|
(cl-streamer/harmony:pipeline-current-track *harmony-pipeline*))
|
||||||
(let* ((server (cl-streamer/harmony:pipeline-server *harmony-pipeline*))
|
(let* ((track-info (cl-streamer/harmony:pipeline-current-track *harmony-pipeline*))
|
||||||
(listener-title (when server
|
(display-title (or (getf track-info :display-title) "Unknown"))
|
||||||
(cl-streamer:get-listener-now-playing
|
|
||||||
server (format nil "/~A" mount))))
|
|
||||||
(track-info (cl-streamer/harmony:pipeline-current-track *harmony-pipeline*))
|
|
||||||
(display-title (or listener-title
|
|
||||||
(getf track-info :display-title)
|
|
||||||
"Unknown"))
|
|
||||||
(listeners (cl-streamer:pipeline-listener-count *harmony-pipeline*))
|
(listeners (cl-streamer:pipeline-listener-count *harmony-pipeline*))
|
||||||
(track-id (or (find-track-by-title display-title)
|
(track-id (or (find-track-by-title display-title)
|
||||||
(find-track-by-file-path (getf track-info :file))))
|
(find-track-by-file-path (getf track-info :file)))))
|
||||||
(pipeline-title (getf track-info :display-title))
|
|
||||||
(raw-remaining (cl-streamer/harmony:pipeline-track-remaining *harmony-pipeline*))
|
|
||||||
(titles-match (or (null listener-title)
|
|
||||||
(null pipeline-title)
|
|
||||||
(string= listener-title pipeline-title)))
|
|
||||||
;; Only show remaining when titles match (delay has passed).
|
|
||||||
;; During the transition window the countdown would be inaccurate.
|
|
||||||
(remaining (when (and raw-remaining titles-match)
|
|
||||||
(max 0 (floor raw-remaining)))))
|
|
||||||
;; Diagnostic: log when listener-title differs from pipeline title
|
|
||||||
(when (and listener-title pipeline-title
|
|
||||||
(not (string= listener-title pipeline-title)))
|
|
||||||
(log:info "[SYNC-DIAG] API returning ~S (pipeline has ~S, delay=~As)"
|
|
||||||
listener-title pipeline-title cl-streamer::*browser-buffer-seconds*))
|
|
||||||
`((:listenurl . ,(format nil "~A/~A" *stream-base-url* mount))
|
`((:listenurl . ,(format nil "~A/~A" *stream-base-url* mount))
|
||||||
(:title . ,display-title)
|
(:title . ,display-title)
|
||||||
(:listeners . ,(or listeners 0))
|
(:listeners . ,(or listeners 0))
|
||||||
(:track-id . ,track-id)
|
(:track-id . ,track-id)
|
||||||
(:favorite-count . ,(or (get-track-favorite-count display-title) 0))
|
(:favorite-count . ,(or (get-track-favorite-count display-title) 0))))))
|
||||||
,@(when remaining `((:remaining . ,remaining)))))))
|
|
||||||
|
|
||||||
;;; ---- Pipeline Lifecycle ----
|
;;; ---- Pipeline Lifecycle ----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@
|
||||||
|
|
||||||
<a id="mini-musicbrainz-link" href="#" target="_blank" title="Search on MusicBrainz" style="display: none; margin-right: 4px; text-decoration: none;">🔗</a>
|
<a id="mini-musicbrainz-link" href="#" target="_blank" title="Search on MusicBrainz" style="display: none; margin-right: 4px; text-decoration: none;">🔗</a>
|
||||||
<span class="now-playing-mini" id="mini-now-playing">Loading...</span>
|
<span class="now-playing-mini" id="mini-now-playing">Loading...</span>
|
||||||
<span id="track-countdown" class="track-countdown-mini"></span>
|
|
||||||
<span class="favorite-count-mini" id="favorite-count-mini"></span>
|
<span class="favorite-count-mini" id="favorite-count-mini"></span>
|
||||||
|
|
||||||
<button class="btn-favorite-mini" id="favorite-btn-mini" onclick="toggleFavoriteMini()" title="Add to favorites">
|
<button class="btn-favorite-mini" id="favorite-btn-mini" onclick="toggleFavoriteMini()" title="Add to favorites">
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
<c:then>
|
<c:then>
|
||||||
<c:using value="stats">
|
<c:using value="stats">
|
||||||
<div class="now-playing-track">
|
<div class="now-playing-track">
|
||||||
<p>Track: <span lquery="(text title)" id="current-track-title">The Void - Silence</span>
|
<p>Track: <span lquery="(text title)" id="current-track-title">The Void - Silence</span></p>
|
||||||
<span id="track-countdown-main" class="track-countdown"></span></p>
|
|
||||||
<button class="btn-favorite" id="favorite-btn" onclick="toggleFavorite()" title="Add to favorites">
|
<button class="btn-favorite" id="favorite-btn" onclick="toggleFavorite()" title="Add to favorites">
|
||||||
<span class="star-icon">☆</span>
|
<span class="star-icon">☆</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue