Compare commits
No commits in common. "249844160fe845f05ff2e603a886c2072ee1391f" and "bad9d4294b8e98c2a27c0855934c3a55bc88669f" have entirely different histories.
249844160f
...
bad9d4294b
|
|
@ -138,24 +138,17 @@
|
||||||
(define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 2 :timeout 1)
|
(define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 2 :timeout 1)
|
||||||
"Get JSON with now playing info including track ID for favorites.
|
"Get JSON with now playing info including track ID for favorites.
|
||||||
Optional MOUNT parameter specifies which stream to get metadata from."
|
Optional MOUNT parameter specifies which stream to get metadata from."
|
||||||
;; Register web listener for geo stats (keeps listener active during playback)
|
|
||||||
(register-web-listener)
|
|
||||||
(with-error-handling
|
(with-error-handling
|
||||||
(let* ((mount-name (or mount "asteroid.mp3"))
|
(let* ((mount-name (or mount "asteroid.mp3"))
|
||||||
(now-playing-stats (icecast-now-playing *stream-base-url* mount-name)))
|
(now-playing-stats (icecast-now-playing *stream-base-url* mount-name)))
|
||||||
(if now-playing-stats
|
(if now-playing-stats
|
||||||
(let* ((title (cdr (assoc :title now-playing-stats)))
|
(let* ((title (cdr (assoc :title now-playing-stats)))
|
||||||
(favorite-count (or (get-track-favorite-count title) 0))
|
(favorite-count (or (get-track-favorite-count title) 0)))
|
||||||
(parsed (parse-track-title title))
|
|
||||||
(artist (getf parsed :artist))
|
|
||||||
(song (getf parsed :song))
|
|
||||||
(search-url (generate-music-search-url artist song)))
|
|
||||||
(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))))
|
|
||||||
(api-output `(("status" . "offline")
|
(api-output `(("status" . "offline")
|
||||||
("title" . "Stream Offline")
|
("title" . "Stream Offline")
|
||||||
("track_id" . nil)))))))
|
("track_id" . nil)))))))
|
||||||
|
|
|
||||||
|
|
@ -268,18 +268,18 @@
|
||||||
(defun update-geo-stats (country-code listener-count &optional city)
|
(defun update-geo-stats (country-code listener-count &optional city)
|
||||||
"Update geo stats for today, optionally including city.
|
"Update geo stats for today, optionally including city.
|
||||||
listener_count tracks peak concurrent listeners (max seen today).
|
listener_count tracks peak concurrent listeners (max seen today).
|
||||||
listen_minutes increments by listener_count per poll (1 minute per listener per poll)."
|
listen_minutes increments by 1 per poll (approximates total listen time)."
|
||||||
(when country-code
|
(when country-code
|
||||||
(handler-case
|
(handler-case
|
||||||
(with-db
|
(with-db
|
||||||
(let ((city-sql (if city (format nil "'~a'" city) "NULL")))
|
(let ((city-sql (if city (format nil "'~a'" city) "NULL")))
|
||||||
(postmodern:execute
|
(postmodern:execute
|
||||||
(format nil "INSERT INTO listener_geo_stats (date, country_code, city, listener_count, listen_minutes)
|
(format nil "INSERT INTO listener_geo_stats (date, country_code, city, listener_count, listen_minutes)
|
||||||
VALUES (CURRENT_DATE, '~a', ~a, ~a, ~a)
|
VALUES (CURRENT_DATE, '~a', ~a, ~a, 1)
|
||||||
ON CONFLICT (date, country_code, city)
|
ON CONFLICT (date, country_code, city)
|
||||||
DO UPDATE SET listener_count = GREATEST(listener_geo_stats.listener_count, ~a),
|
DO UPDATE SET listener_count = GREATEST(listener_geo_stats.listener_count, ~a),
|
||||||
listen_minutes = listener_geo_stats.listen_minutes + ~a"
|
listen_minutes = listener_geo_stats.listen_minutes + 1"
|
||||||
country-code city-sql listener-count listener-count listener-count listener-count))))
|
country-code city-sql listener-count listener-count))))
|
||||||
(error (e)
|
(error (e)
|
||||||
(log:error "Failed to update geo stats: ~a" e)))))
|
(log:error "Failed to update geo stats: ~a" e)))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
-- Migration: Fix Listen Minutes Calculation
|
|
||||||
-- Version: 009
|
|
||||||
-- Date: 2026-01-11
|
|
||||||
-- Description: Document the fix for listen_minutes calculation
|
|
||||||
--
|
|
||||||
-- ISSUE: listen_minutes was incrementing by 1 per poll regardless of listener count.
|
|
||||||
-- This meant a country with 5 listeners for 1 hour would only show 60 minutes,
|
|
||||||
-- not 300 listener-minutes.
|
|
||||||
--
|
|
||||||
-- FIX: Application code in listener-stats.lisp now increments listen_minutes by
|
|
||||||
-- the listener_count value (1 minute per listener per poll interval).
|
|
||||||
--
|
|
||||||
-- ADDITIONAL FIX: register-web-listener is now called from the now-playing-json API
|
|
||||||
-- endpoint, keeping listeners registered during continuous playback
|
|
||||||
-- instead of timing out after 5 minutes.
|
|
||||||
--
|
|
||||||
-- No schema changes required - this migration documents the application logic fix.
|
|
||||||
|
|
||||||
-- Add a comment to the table for future reference
|
|
||||||
COMMENT ON COLUMN listener_geo_stats.listen_minutes IS
|
|
||||||
'Total listener-minutes: increments by listener_count per poll (1 min per listener per 60s poll)';
|
|
||||||
|
|
||||||
-- Success message
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
RAISE NOTICE 'Migration 009: listen_minutes calculation fix documented';
|
|
||||||
RAISE NOTICE 'listen_minutes now represents true listener-minutes (count * time)';
|
|
||||||
END $$;
|
|
||||||
|
|
@ -50,18 +50,24 @@
|
||||||
(let ((audio-element nil)
|
(let ((audio-element nil)
|
||||||
(canvas-element (ps:chain document (get-element-by-id "spectrum-canvas"))))
|
(canvas-element (ps:chain document (get-element-by-id "spectrum-canvas"))))
|
||||||
|
|
||||||
;; Only use audio element from current frame
|
;; Try to find audio element in current frame first
|
||||||
;; Cross-frame MediaElementSource causes audio to break when content frame navigates
|
|
||||||
(setf audio-element (or (ps:chain document (get-element-by-id "live-audio"))
|
(setf audio-element (or (ps:chain document (get-element-by-id "live-audio"))
|
||||||
(ps:chain document (get-element-by-id "persistent-audio"))))
|
(ps:chain document (get-element-by-id "persistent-audio"))))
|
||||||
|
|
||||||
;; In frameset mode, the audio is in the player frame - we cannot safely connect
|
;; If not found and we're in a frame, try to access from parent frameset
|
||||||
;; to it from the content frame because createMediaElementSource breaks audio
|
|
||||||
;; routing when the content frame navigates away
|
|
||||||
(when (and (not audio-element)
|
(when (and (not audio-element)
|
||||||
(ps:@ window parent)
|
(ps:@ window parent)
|
||||||
(not (eq (ps:@ window parent) window)))
|
(not (eq (ps:@ window parent) window)))
|
||||||
(ps:chain console (log "Spectrum analyzer: In frameset mode, audio is in player frame - visualization disabled to prevent audio issues")))
|
(ps:chain console (log "Trying to access audio from parent frame..."))
|
||||||
|
(ps:try
|
||||||
|
(progn
|
||||||
|
;; Try accessing via parent.frames
|
||||||
|
(let ((player-frame (ps:getprop (ps:@ window parent) "player-frame")))
|
||||||
|
(when player-frame
|
||||||
|
(setf audio-element (ps:chain player-frame document (get-element-by-id "persistent-audio")))
|
||||||
|
(ps:chain console (log "Found audio in player-frame:" audio-element)))))
|
||||||
|
(:catch (e)
|
||||||
|
(ps:chain console (log "Cross-frame access error:" e)))))
|
||||||
|
|
||||||
(when (and audio-element canvas-element)
|
(when (and audio-element canvas-element)
|
||||||
;; Store current audio element
|
;; Store current audio element
|
||||||
|
|
|
||||||
|
|
@ -7,143 +7,6 @@
|
||||||
(ps:ps*
|
(ps:ps*
|
||||||
'(progn
|
'(progn
|
||||||
|
|
||||||
;; ========================================
|
|
||||||
;; Mini Spectrum Analyzer (for player frame)
|
|
||||||
;; ========================================
|
|
||||||
|
|
||||||
(defvar *mini-audio-context* nil)
|
|
||||||
(defvar *mini-analyser* nil)
|
|
||||||
(defvar *mini-media-source* nil)
|
|
||||||
(defvar *mini-canvas* nil)
|
|
||||||
(defvar *mini-canvas-ctx* nil)
|
|
||||||
(defvar *mini-animation-id* nil)
|
|
||||||
|
|
||||||
;; Color themes (same as main spectrum analyzer)
|
|
||||||
(defvar *mini-themes*
|
|
||||||
(ps:create
|
|
||||||
"monotone" (ps:create "top" "#0047ab" "mid" "#002966" "bottom" "#000d1a")
|
|
||||||
"green" (ps:create "top" "#00ff00" "mid" "#00aa00" "bottom" "#005500")
|
|
||||||
"blue" (ps:create "top" "#00ffff" "mid" "#0088ff" "bottom" "#0044aa")
|
|
||||||
"purple" (ps:create "top" "#ff00ff" "mid" "#aa00aa" "bottom" "#550055")
|
|
||||||
"red" (ps:create "top" "#ff0000" "mid" "#aa0000" "bottom" "#550000")
|
|
||||||
"amber" (ps:create "top" "#ffaa00" "mid" "#ff6600" "bottom" "#aa3300")
|
|
||||||
"rainbow" (ps:create "top" "#ff00ff" "mid" "#00ffff" "bottom" "#00ff00")))
|
|
||||||
|
|
||||||
(defun get-mini-theme ()
|
|
||||||
"Get current theme from localStorage (shared with main analyzer)"
|
|
||||||
(let ((saved-theme (ps:chain local-storage (get-item "spectrum-theme"))))
|
|
||||||
(if (and saved-theme (ps:getprop *mini-themes* saved-theme))
|
|
||||||
saved-theme
|
|
||||||
"green")))
|
|
||||||
|
|
||||||
(defun get-mini-style ()
|
|
||||||
"Get current style from localStorage (shared with main analyzer)"
|
|
||||||
(let ((saved-style (ps:chain local-storage (get-item "spectrum-style"))))
|
|
||||||
(if (and saved-style (or (= saved-style "bars") (= saved-style "wave") (= saved-style "dots")))
|
|
||||||
saved-style
|
|
||||||
"bars")))
|
|
||||||
|
|
||||||
(defun init-mini-spectrum (audio-element)
|
|
||||||
"Initialize mini spectrum analyzer in player frame"
|
|
||||||
(let ((canvas (ps:chain document (get-element-by-id "mini-spectrum-canvas"))))
|
|
||||||
(when (and audio-element canvas (not *mini-audio-context*))
|
|
||||||
(ps:try
|
|
||||||
(progn
|
|
||||||
(setf *mini-audio-context* (ps:new (or (ps:@ window |AudioContext|)
|
|
||||||
(ps:@ window |webkitAudioContext|))))
|
|
||||||
(setf *mini-analyser* (ps:chain *mini-audio-context* (create-analyser)))
|
|
||||||
(setf (ps:@ *mini-analyser* |fftSize|) 64)
|
|
||||||
(setf (ps:@ *mini-analyser* |smoothingTimeConstant|) 0.8)
|
|
||||||
(setf *mini-media-source* (ps:chain *mini-audio-context* (create-media-element-source audio-element)))
|
|
||||||
(ps:chain *mini-media-source* (connect *mini-analyser*))
|
|
||||||
(ps:chain *mini-analyser* (connect (ps:@ *mini-audio-context* destination)))
|
|
||||||
(setf *mini-canvas* canvas)
|
|
||||||
(setf *mini-canvas-ctx* (ps:chain canvas (get-context "2d")))
|
|
||||||
(ps:chain console (log "Mini spectrum analyzer initialized")))
|
|
||||||
(:catch (e)
|
|
||||||
(ps:chain console (log "Error initializing mini spectrum:" e)))))))
|
|
||||||
|
|
||||||
(defun draw-mini-spectrum ()
|
|
||||||
"Draw mini spectrum visualization using theme and style from localStorage"
|
|
||||||
(setf *mini-animation-id* (request-animation-frame draw-mini-spectrum))
|
|
||||||
(when (and *mini-analyser* *mini-canvas* *mini-canvas-ctx*)
|
|
||||||
(let* ((buffer-length (ps:@ *mini-analyser* |frequencyBinCount|))
|
|
||||||
(data-array (ps:new (|Uint8Array| buffer-length)))
|
|
||||||
(width (ps:@ *mini-canvas* width))
|
|
||||||
(height (ps:@ *mini-canvas* height))
|
|
||||||
(bar-width (/ width buffer-length))
|
|
||||||
(theme-name (get-mini-theme))
|
|
||||||
(theme (ps:getprop *mini-themes* theme-name))
|
|
||||||
(style (get-mini-style)))
|
|
||||||
(ps:chain *mini-analyser* (get-byte-frequency-data data-array))
|
|
||||||
;; Clear with fade effect
|
|
||||||
(setf (ps:@ *mini-canvas-ctx* fill-style) "rgba(0, 0, 0, 0.2)")
|
|
||||||
(ps:chain *mini-canvas-ctx* (fill-rect 0 0 width height))
|
|
||||||
|
|
||||||
(cond
|
|
||||||
;; Bar graph style
|
|
||||||
((= style "bars")
|
|
||||||
(dotimes (i buffer-length)
|
|
||||||
(let* ((value (ps:getprop data-array i))
|
|
||||||
(bar-height (* (/ value 255) height))
|
|
||||||
(x (* i bar-width)))
|
|
||||||
(when (> bar-height 0)
|
|
||||||
(let ((gradient (ps:chain *mini-canvas-ctx*
|
|
||||||
(create-linear-gradient 0 (- height bar-height) 0 height))))
|
|
||||||
(ps:chain gradient (add-color-stop 0 (ps:@ theme top)))
|
|
||||||
(ps:chain gradient (add-color-stop 0.5 (ps:@ theme mid)))
|
|
||||||
(ps:chain gradient (add-color-stop 1 (ps:@ theme bottom)))
|
|
||||||
(setf (ps:@ *mini-canvas-ctx* fill-style) gradient)
|
|
||||||
(ps:chain *mini-canvas-ctx* (fill-rect x (- height bar-height) bar-width bar-height)))))))
|
|
||||||
|
|
||||||
;; Wave/line style
|
|
||||||
((= style "wave")
|
|
||||||
(ps:chain *mini-canvas-ctx* (begin-path))
|
|
||||||
(setf (ps:@ *mini-canvas-ctx* |lineWidth|) 2)
|
|
||||||
(setf (ps:@ *mini-canvas-ctx* |strokeStyle|) (ps:@ theme top))
|
|
||||||
(let ((x 0))
|
|
||||||
(dotimes (i buffer-length)
|
|
||||||
(let* ((value (ps:getprop data-array i))
|
|
||||||
(bar-height (* (/ value 255) height))
|
|
||||||
(y (- height bar-height)))
|
|
||||||
(if (= i 0)
|
|
||||||
(ps:chain *mini-canvas-ctx* (move-to x y))
|
|
||||||
(ps:chain *mini-canvas-ctx* (line-to x y)))
|
|
||||||
(incf x bar-width))))
|
|
||||||
(ps:chain *mini-canvas-ctx* (stroke)))
|
|
||||||
|
|
||||||
;; Dots/particles style
|
|
||||||
((= style "dots")
|
|
||||||
(setf (ps:@ *mini-canvas-ctx* |fillStyle|) (ps:@ theme top))
|
|
||||||
(let ((x 0))
|
|
||||||
(dotimes (i buffer-length)
|
|
||||||
(let* ((value (ps:getprop data-array i))
|
|
||||||
(bar-height (* (/ value 255) height))
|
|
||||||
(y (- height bar-height))
|
|
||||||
(dot-radius (ps:max 1 (/ bar-height 10))))
|
|
||||||
(when (> value 0)
|
|
||||||
(ps:chain *mini-canvas-ctx* (begin-path))
|
|
||||||
(ps:chain *mini-canvas-ctx* (arc x y dot-radius 0 6.283185307179586))
|
|
||||||
(ps:chain *mini-canvas-ctx* (fill)))
|
|
||||||
(incf x bar-width)))))))))
|
|
||||||
|
|
||||||
(defun start-mini-spectrum ()
|
|
||||||
"Start mini spectrum animation"
|
|
||||||
(when (and *mini-analyser* (not *mini-animation-id*))
|
|
||||||
(draw-mini-spectrum)))
|
|
||||||
|
|
||||||
(defun stop-mini-spectrum ()
|
|
||||||
"Stop mini spectrum animation"
|
|
||||||
(when *mini-animation-id*
|
|
||||||
(cancel-animation-frame *mini-animation-id*)
|
|
||||||
(setf *mini-animation-id* nil))
|
|
||||||
;; Clear canvas
|
|
||||||
(when (and *mini-canvas* *mini-canvas-ctx*)
|
|
||||||
(let ((width (ps:@ *mini-canvas* width))
|
|
||||||
(height (ps:@ *mini-canvas* height)))
|
|
||||||
(setf (ps:@ *mini-canvas-ctx* fill-style) "#000000")
|
|
||||||
(ps:chain *mini-canvas-ctx* (fill-rect 0 0 width height)))))
|
|
||||||
|
|
||||||
;; ========================================
|
;; ========================================
|
||||||
;; Stream Configuration
|
;; Stream Configuration
|
||||||
;; ========================================
|
;; ========================================
|
||||||
|
|
@ -425,16 +288,7 @@
|
||||||
(cond
|
(cond
|
||||||
((= 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 " ❤️"))))))))))
|
||||||
;; Update MusicBrainz search 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))))
|
|
||||||
(when mb-link
|
|
||||||
(if search-url
|
|
||||||
(progn
|
|
||||||
(setf (ps:@ mb-link href) search-url)
|
|
||||||
(setf (ps:@ mb-link style display) "inline"))
|
|
||||||
(setf (ps:@ mb-link style display) "none"))))))))
|
|
||||||
(catch (lambda (error)
|
(catch (lambda (error)
|
||||||
(ps:chain console (log "Could not fetch now playing:" error)))))))
|
(ps:chain console (log "Could not fetch now playing:" error)))))))
|
||||||
|
|
||||||
|
|
@ -675,9 +529,6 @@
|
||||||
(add-event-listener "playing"
|
(add-event-listener "playing"
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(ps:chain console (log "Audio playing"))
|
(ps:chain console (log "Audio playing"))
|
||||||
;; Initialize and start mini spectrum on first play
|
|
||||||
(init-mini-spectrum audio-element)
|
|
||||||
(start-mini-spectrum)
|
|
||||||
(hide-status)
|
(hide-status)
|
||||||
(setf *stream-error-count* 0)
|
(setf *stream-error-count* 0)
|
||||||
(setf *is-reconnecting* false)
|
(setf *is-reconnecting* false)
|
||||||
|
|
@ -726,8 +577,6 @@
|
||||||
(ps:chain audio-element
|
(ps:chain audio-element
|
||||||
(add-event-listener "pause"
|
(add-event-listener "pause"
|
||||||
(lambda ()
|
(lambda ()
|
||||||
;; Stop mini spectrum when paused
|
|
||||||
(stop-mini-spectrum)
|
|
||||||
;; If paused while muted and we didn't initiate it, browser may have throttled
|
;; If paused while muted and we didn't initiate it, browser may have throttled
|
||||||
(when (and (ps:@ audio-element muted) (not *is-reconnecting*))
|
(when (and (ps:@ audio-element muted) (not *is-reconnecting*))
|
||||||
(ps:chain console (log "Stream paused while muted (possible browser throttling), will reconnect in 3 seconds..."))
|
(ps:chain console (log "Stream paused while muted (possible browser throttling), will reconnect in 3 seconds..."))
|
||||||
|
|
|
||||||
|
|
@ -1340,8 +1340,8 @@ body.persistent-player-container .quality-selector select{
|
||||||
color: #00ff00;
|
color: #00ff00;
|
||||||
letter-spacing: 0.08rem;
|
letter-spacing: 0.08rem;
|
||||||
border: 1px solid #00ff00;
|
border: 1px solid #00ff00;
|
||||||
padding: 3px 6px;
|
padding: 3px 8px;
|
||||||
min-width: 55px;
|
min-width: 140px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
|
|
||||||
|
|
@ -1079,8 +1079,8 @@
|
||||||
:color "#00ff00"
|
:color "#00ff00"
|
||||||
:letter-spacing "0.08rem"
|
:letter-spacing "0.08rem"
|
||||||
:border "1px solid #00ff00"
|
:border "1px solid #00ff00"
|
||||||
:padding "3px 6px"
|
:padding "3px 8px"
|
||||||
:min-width "55px"
|
:min-width "140px"
|
||||||
:font-size "0.9em"
|
:font-size "0.9em"
|
||||||
:height "26px"
|
:height "26px"
|
||||||
:line-height "18px")
|
:line-height "18px")
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="persistent-player-container">
|
<body class="persistent-player-container">
|
||||||
<div class="persistent-player">
|
<div class="persistent-player">
|
||||||
<!-- Mini spectrum visualizer -->
|
|
||||||
<canvas id="mini-spectrum-canvas" width="100" height="28" style="vertical-align: middle; margin-right: 8px; border-radius: 3px; background: #000;"></canvas>
|
|
||||||
|
|
||||||
<span class="player-label">
|
<span class="player-label">
|
||||||
<span class="live-stream-indicator">🟢 </span>
|
<span class="live-stream-indicator">🟢 </span>
|
||||||
LIVE:
|
LIVE:
|
||||||
|
|
@ -26,10 +23,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="quality-selector">
|
<div class="quality-selector">
|
||||||
<select id="stream-quality" onchange="changeStreamQuality()" title="Stream Quality">
|
<label for="stream-quality">Quality:</label>
|
||||||
<option value="aac">AAC</option>
|
<select id="stream-quality" onchange="changeStreamQuality()">
|
||||||
<option value="mp3">MP3</option>
|
<option value="aac">AAC 96k</option>
|
||||||
<option value="low">Low</option>
|
<option value="mp3">MP3 128k</option>
|
||||||
|
<option value="low">MP3 64k</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -37,7 +35,6 @@
|
||||||
<source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)">
|
<source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)">
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
<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 class="favorite-count-mini" id="favorite-count-mini"></span>
|
<span class="favorite-count-mini" id="favorite-count-mini"></span>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue