Compare commits

...

2 Commits

Author SHA1 Message Date
Glenn Thompson 20ed7ecb02 Tune sync delay to 10s, fix countdown during transitions, add buffer bloat monitor
- Set *browser-buffer-seconds* to 10 (5s=23s early, 23s=14s late)
- Hide countdown timer during title transition window (titles mismatch)
- Add client-side buffer bloat monitor: logs buffer size every 30s,
  auto-reconnects if buffer exceeds 15s to prevent drift
- Reduce buffer monitor log spam (only log when >5s, check every 30s)
- Update cl-streamer submodule ref
2026-04-09 17:12:15 +01:00
Glenn Thompson 10c75f04e1 Stream sync, countdown timer, playback state fix, buffer bloat detection
Multi-pronged attempt to fix cumulative lag between audio playback,
now-playing display, and browser notifications:

== Synchronization (work in progress) ==
- Server-side time-based metadata delay via get-listener-now-playing
  with configurable *browser-buffer-seconds* parameter
- First track after restart syncs perfectly; subsequent tracks drift
  due to browser audio buffer bloat (buffer grows unbounded over time)
- Added client-side buffer bloat detection: monitors audio.buffered
  vs audio.currentTime and auto-reconnects when buffer exceeds 15s
- Added [STREAM-SYNC], [NOTIFY], [BUFFER] diagnostic logging to
  browser console for ongoing diagnosis

== Countdown timer ==
- Server exposes remaining seconds via pipeline-track-remaining
- now-playing JSON API includes 'remaining' field adjusted for
  browser buffer delay
- Player frame shows [mm:ss] countdown next to track title
- Main page now-playing area also shows countdown with its own
  polling (15s interval) and local 1-second ticker
- LASS styles for .track-countdown and .track-countdown-mini

== Playback state fix ==
- Fixed save-playback-state saving one track ahead of what was
  actually playing (was saving the track loading during crossfade)
- Uses *pending-save-file* with one-track delay so the saved state
  reflects the track that was actually heard

== Notifications ==
- All notification conditions working correctly per diagnostic logs
- show-track-notification logs supported/permission/enabled/last/title
- Notifications fire consistently on track changes
2026-04-09 16:32:58 +01:00
10 changed files with 310 additions and 28 deletions

@ -1 +1 @@
Subproject commit 23b5ee1e35e6c967cab12751ac7b8930d21b3307
Subproject commit cc4215d1c663c5aed4e9758c755a944016fa6aaa

View File

@ -120,12 +120,14 @@
(artist (getf parsed :artist))
(song (getf parsed :song))
(search-url (generate-music-search-url artist song)))
(api-output `(("status" . "success")
("title" . ,title)
("listeners" . ,(cdr (assoc :listeners now-playing-stats)))
("track_id" . ,(cdr (assoc :track-id now-playing-stats)))
("favorite_count" . ,favorite-count)
("search_url" . ,search-url))))
(let ((remaining (cdr (assoc :remaining now-playing-stats))))
(api-output `(("status" . "success")
("title" . ,title)
("listeners" . ,(cdr (assoc :listeners now-playing-stats)))
("track_id" . ,(cdr (assoc :track-id now-playing-stats)))
("favorite_count" . ,favorite-count)
("search_url" . ,search-url)
,@(when remaining `(("remaining" . ,remaining)))))))
(api-output `(("status" . "offline")
("title" . "Stream Offline")
("track_id" . nil)))))))

View File

@ -866,7 +866,52 @@
(ps:chain console (log "Could not load recent requests:" error))))))))
;; 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")
(defun generate-front-page-js ()

View File

@ -336,6 +336,30 @@
;; Track last notified title to avoid duplicate notifications
(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
(defun notifications-enabled-p ()
(= (ps:chain local-storage (get-item "notifications-enabled")) "true"))
@ -410,6 +434,12 @@
;; Show a system notification for track change
(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)
(= (get-notification-permission) "granted")
(notifications-enabled-p)
@ -422,6 +452,7 @@
:tag "asteroid-track-change"
:renotify true
:silent false)))))
(ps:chain console (log "[NOTIFY] Notification created successfully"))
;; Auto-close after 5 seconds
(set-timeout (lambda () (ps:chain notification (close))) 5000)
;; Click to focus the window
@ -430,7 +461,7 @@
(ps:chain window (focus))
(ps:chain notification (close)))))
(:catch (e)
(ps:chain console (log "Notification error:" e))))))
(ps:chain console (log "[NOTIFY] Notification error:" e))))))
;; Notify track change (called from update-mini-now-playing)
(defun notify-track-change (title)
@ -514,16 +545,15 @@
(when el
;; Check if track changed and record to history + notify
(when (not (= (ps:@ el text-content) title))
(ps:chain console (log "[STREAM-SYNC] Title changed:" title))
(record-track-listen title)
(notify-track-change title))
(setf (ps:@ el text-content) title)
;; Check if this track is in user's favorites
(check-favorite-status-mini))
(update-media-session title)
(when track-id-el
(let ((track-id (or (ps:@ data data track_id) (ps:@ data 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")))
(fav-count (or (ps:@ data data favorite_count) (ps:@ data favorite_count) 0)))
(when count-el
@ -531,7 +561,10 @@
((= fav-count 0) (setf (ps:@ count-el text-content) ""))
((= fav-count 1) (setf (ps:@ count-el text-content) "1 ❤️"))
(t (setf (ps:@ count-el text-content) (+ fav-count " ❤️"))))))
;; Update MusicBrainz search link
;; Sync countdown timer from server
(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")))
(search-url (or (ps:@ data data search_url) (ps:@ data search_url))))
(when mb-link
@ -724,6 +757,35 @@
(catch (lambda (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
(defun attach-audio-listeners (audio-element)
(ps:chain audio-element
@ -907,8 +969,9 @@
:artist "Asteroid Radio"
:album "Live Broadcast")))))
;; Attach event listeners
;; Attach event listeners and buffer monitor
(attach-audio-listeners audio-element)
(start-buffer-monitor audio-element)
;; Restore user channel preference
(let ((channel-selector (ps:chain document (get-element-by-id "stream-channel")))
@ -958,9 +1021,10 @@
(ps:chain console (log "Could not fetch channel name:" error))))))
15000)) ;; Poll every 15 seconds
;; Start now playing updates
;; Start now playing updates and countdown ticker
(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
(defun init-popout-player ()

113
playlists/deep-focus.m3u Normal file
View File

@ -0,0 +1,113 @@
#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

View File

@ -1372,6 +1372,14 @@ body.persistent-player-container .now-playing-mini{
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{
background: transparent;
color: #00ff00;
@ -1595,6 +1603,13 @@ body.popout-body .status-mini{
margin: 0 5px;
}
.track-countdown{
color: #888;
font-size: 0.8em;
font-family: monospace;
margin-left: 8px;
}
.now-playing-track{
display: flex;
align-items: center;

View File

@ -1108,6 +1108,13 @@
:flex 1
:min-width "300px")
(.track-countdown-mini
:color "#888"
:font-size "0.75em"
:font-family "monospace"
:margin-left "6px"
:white-space "nowrap")
(.persistent-reconnect-btn
:background transparent
:color "#00ff00"
@ -1288,6 +1295,12 @@
(.craftering
(a :margin "0 5px")))
(.track-countdown
:color "#888"
:font-size "0.8em"
:font-family "monospace"
:margin-left "8px")
;; Now playing favorite button
(.now-playing-track
:display "flex"

View File

@ -148,6 +148,11 @@
(setf *current-playlist-path* 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)
"Called by cl-streamer when a track changes.
Updates recently-played lists and finds the track in the database."
@ -168,9 +173,10 @@
:track-id track-id)
:curated)
(setf *last-known-track-curated* display-title))
;; Persist current track for resume-on-restart
(when file-path
(save-playback-state file-path))
;; Save the PREVIOUS track (which was actually playing) and queue this one
(when *pending-save-file*
(save-playback-state *pending-save-file*))
(setf *pending-save-file* file-path)
(log:info "Track change: ~A (track-id: ~A)" display-title track-id)))
(defun find-track-by-file-path (file-path)
@ -191,19 +197,41 @@
(defun harmony-now-playing (&optional (mount "asteroid.mp3"))
"Get now-playing information from cl-streamer pipeline.
Returns an alist with now-playing data, or NIL if the pipeline is not running."
Uses the metadata timeline to report what listeners are actually hearing,
accounting for ring buffer and browser decode buffering."
(when (and *harmony-pipeline*
(cl-streamer/harmony:pipeline-current-track *harmony-pipeline*))
(let* ((track-info (cl-streamer/harmony:pipeline-current-track *harmony-pipeline*))
(display-title (or (getf track-info :display-title) "Unknown"))
(let* ((server (cl-streamer/harmony:pipeline-server *harmony-pipeline*))
(listener-title (when server
(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*))
(track-id (or (find-track-by-title display-title)
(find-track-by-file-path (getf track-info :file)))))
`((:listenurl . ,(format nil "~A/~A" *stream-base-url* mount))
(:title . ,display-title)
(:listeners . ,(or listeners 0))
(:track-id . ,track-id)
(:favorite-count . ,(or (get-track-favorite-count display-title) 0))))))
(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))
(:title . ,display-title)
(:listeners . ,(or listeners 0))
(:track-id . ,track-id)
(:favorite-count . ,(or (get-track-favorite-count display-title) 0))
,@(when remaining `((:remaining . ,remaining)))))))
;;; ---- Pipeline Lifecycle ----

View File

@ -39,6 +39,7 @@
<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 id="track-countdown" class="track-countdown-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">

View File

@ -3,7 +3,8 @@
<c:then>
<c:using value="stats">
<div class="now-playing-track">
<p>Track: <span lquery="(text title)" id="current-track-title">The Void - Silence</span></p>
<p>Track: <span lquery="(text title)" id="current-track-title">The Void - Silence</span>
<span id="track-countdown-main" class="track-countdown"></span></p>
<button class="btn-favorite" id="favorite-btn" onclick="toggleFavorite()" title="Add to favorites">
<span class="star-icon">☆</span>
</button>