From 0a7d5c3de501d55539a08cb3deb23ccc6ab02368 Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Mon, 17 Nov 2025 14:16:06 +0300 Subject: [PATCH 1/4] Add recently played tracks feature with MusicBrainz integration - Display last 3 played tracks on front page - Auto-updates every 30 seconds - Shows track title, artist, and time ago - Links to MusicBrainz search for each track - Thread-safe in-memory storage - Works in both normal and frameset modes - Hacker-themed green styling Implements feature request from fade to show recently played tracks with linkage to track info at music database. --- asteroid.lisp | 62 +++++++++++++++++ frontend-partials.lisp | 7 ++ static/asteroid.css | 112 +++++++++++++++++++++++++++++++ static/asteroid.lass | 80 ++++++++++++++++++++++ static/js/recently-played.js | 86 ++++++++++++++++++++++++ template/front-page-content.ctml | 9 +++ template/front-page.ctml | 9 +++ 7 files changed, 365 insertions(+) create mode 100644 static/js/recently-played.js diff --git a/asteroid.lisp b/asteroid.lisp index cb274af..2deb56b 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -24,6 +24,13 @@ (defparameter *supported-formats* '("mp3" "flac" "ogg" "wav")) (defparameter *stream-base-url* "http://localhost:8000") +;; Recently played tracks storage (in-memory) +(defparameter *recently-played* nil + "List of recently played tracks (max 3), newest first") +(defparameter *recently-played-lock* (bt:make-lock "recently-played-lock")) +(defparameter *last-known-track* nil + "Last known track title to detect changes") + ;; Configure JSON as the default API format (define-api-format json (data) "JSON API format for Radiance" @@ -33,6 +40,40 @@ ;; Set JSON as the default API format (setf *default-api-format* "json") +;; Recently played tracks management +(defun add-recently-played (track-info) + "Add a track to the recently played list (max 3 tracks)" + (bt:with-lock-held (*recently-played-lock*) + (push track-info *recently-played*) + (when (> (length *recently-played*) 3) + (setf *recently-played* (subseq *recently-played* 0 3))))) + +(defun universal-time-to-unix (universal-time) + "Convert Common Lisp universal time to Unix timestamp" + ;; Universal time is seconds since 1900-01-01, Unix is since 1970-01-01 + ;; Difference is 2208988800 seconds (70 years) + (- universal-time 2208988800)) + +(defun get-recently-played () + "Get the list of recently played tracks" + (bt:with-lock-held (*recently-played-lock*) + (copy-list *recently-played*))) + +(defun parse-track-title (title) + "Parse track title into artist and song name. Expected format: 'Artist - Song'" + (let ((pos (search " - " title))) + (if pos + (list :artist (string-trim " " (subseq title 0 pos)) + :song (string-trim " " (subseq title (+ pos 3)))) + (list :artist "Unknown" :song title)))) + +(defun generate-music-search-url (artist song) + "Generate MusicBrainz search URL for artist and song" + ;; MusicBrainz uses 'artist:' and 'recording:' prefixes for better search + (let ((query (format nil "artist:\"~a\" recording:\"~a\"" artist song))) + (format nil "https://musicbrainz.org/search?query=~a&type=recording&method=indexed" + (drakma:url-encode query :utf-8)))) + ;; API Routes using Radiance's define-api ;; API endpoints are accessed at /api/ automatically ;; They use lambda-lists for parameters and api-output for responses @@ -135,6 +176,27 @@ ("message" . "Playlist not found")) :status 404))))) +;; Recently played tracks API endpoint +(define-api asteroid/recently-played () () + "Get the last 3 played tracks with AllMusic links" + (with-error-handling + (let ((tracks (get-recently-played))) + (api-output `(("status" . "success") + ("tracks" . ,(mapcar (lambda (track) + (let* ((title (getf track :title)) + (timestamp (getf track :timestamp)) + (unix-timestamp (universal-time-to-unix timestamp)) + (parsed (parse-track-title title)) + (artist (getf parsed :artist)) + (song (getf parsed :song)) + (search-url (generate-music-search-url artist song))) + `(("title" . ,title) + ("artist" . ,artist) + ("song" . ,song) + ("timestamp" . ,unix-timestamp) + ("search_url" . ,search-url)))) + tracks))))))) + ;; API endpoint to get all tracks (for web player) (define-api asteroid/tracks () () "Get all tracks for web player" diff --git a/frontend-partials.lisp b/frontend-partials.lisp index cc6f976..383b212 100644 --- a/frontend-partials.lisp +++ b/frontend-partials.lisp @@ -33,6 +33,13 @@ (aref groups 0) "Unknown"))) "Unknown"))) + ;; Track recently played if title changed + (when (and title + (not (string= title "Unknown")) + (not (equal title *last-known-track*))) + (setf *last-known-track* title) + (add-recently-played (list :title title + :timestamp (get-universal-time)))) `((:listenurl . ,(format nil "~a/asteroid.mp3" *stream-base-url*)) (:title . ,title) (:listeners . ,total-listeners))))))) diff --git a/static/asteroid.css b/static/asteroid.css index 1f03f15..5d3e1a7 100644 --- a/static/asteroid.css +++ b/static/asteroid.css @@ -143,6 +143,118 @@ body .now-playing{ overflow: auto; } +body .recently-played-panel{ + background: #1a2332; + border: 1px solid #2a3441; + border-radius: 8px; + padding: 20px; + margin: 20px 0; +} + +body .recently-played-panel h3{ + margin: 0 0 15px 0; + color: #00ff00; + font-size: 1.2em; + font-weight: 600; +} + +body .recently-played-panel .recently-played-list{ + min-height: 100px; +} + +body .recently-played-panel .recently-played-list .loading, +body .recently-played-panel .recently-played-list .no-tracks, +body .recently-played-panel .recently-played-list .error{ + text-align: center; + color: #888; + padding: 20px; + font-style: italic; +} + +body .recently-played-panel .recently-played-list .error{ + color: #ff4444; +} + +body .recently-played-panel .recently-played-list.track-list{ + list-style: none; + padding: 0; + margin: 0; + border: none; + max-height: none; + overflow-y: visible; +} + +body .recently-played-panel .recently-played-list.track-item{ + padding: 12px; + border-bottom: 1px solid #2a3441; + -moz-transition: background-color 0.2s; + -o-transition: background-color 0.2s; + -webkit-transition: background-color 0.2s; + -ms-transition: background-color 0.2s; + transition: background-color 0.2s; +} + +body .recently-played-panel .recently-played-list.track-item LAST-CHILD{ + border-bottom: none; +} + +body .recently-played-panel .recently-played-list.track-item HOVER{ + background-color: #252525; +} + +body .recently-played-panel .recently-played-list.track-info{ + display: flex; + flex-direction: column; + gap: 4px; +} + +body .recently-played-panel .recently-played-list.track-title{ + color: #fff; + font-weight: 500; + font-size: 1em; +} + +body .recently-played-panel .recently-played-list.track-artist{ + color: #aaa; + font-size: 0.9em; +} + +body .recently-played-panel .recently-played-list.track-meta{ + display: flex; + align-items: center; + gap: 12px; + margin-top: 4px; +} + +body .recently-played-panel .recently-played-list.track-time{ + color: #666; + font-size: 0.85em; +} + +body .recently-played-panel .recently-played-list.allmusic-link{ + display: inline-flex; + align-items: center; + gap: 4px; + color: #00ff00; + text-decoration: none; + font-size: 0.85em; + -moz-transition: color 0.2s; + -o-transition: color 0.2s; + -webkit-transition: color 0.2s; + -ms-transition: color 0.2s; + transition: color 0.2s; +} + +body .recently-played-panel .recently-played-list.allmusic-link HOVER{ + color: #00cc00; + text-decoration: underline; +} + +body .recently-played-panel .recently-played-list.allmusic-link svg{ + width: 14px; + height: 14px; +} + body .back{ color: #00ffff; text-decoration: none; diff --git a/static/asteroid.lass b/static/asteroid.lass index f828d0f..da90b0c 100644 --- a/static/asteroid.lass +++ b/static/asteroid.lass @@ -123,6 +123,86 @@ :color "#4488ff" :overflow auto) + ;; Recently Played Panel + (.recently-played-panel + :background "#1a2332" + :border "1px solid #2a3441" + :border-radius "8px" + :padding "20px" + :margin "20px 0" + + (h3 :margin "0 0 15px 0" + :color "#00ff00" + :font-size "1.2em" + :font-weight 600) + + (.recently-played-list + :min-height "100px" + + ((:or .loading .no-tracks .error) + :text-align center + :color "#888" + :padding "20px" + :font-style italic) + + (.error :color "#ff4444")) + + ((:and .recently-played-list .track-list) + :list-style none + :padding 0 + :margin 0 + :border none + :max-height none + :overflow-y visible) + + ((:and .recently-played-list .track-item) + :padding "12px" + :border-bottom "1px solid #2a3441" + :transition "background-color 0.2s" + + ((:and :last-child) :border-bottom none) + ((:and :hover) :background-color "#252525")) + + ((:and .recently-played-list .track-info) + :display flex + :flex-direction column + :gap "4px") + + ((:and .recently-played-list .track-title) + :color "#fff" + :font-weight 500 + :font-size "1em") + + ((:and .recently-played-list .track-artist) + :color "#aaa" + :font-size "0.9em") + + ((:and .recently-played-list .track-meta) + :display flex + :align-items center + :gap "12px" + :margin-top "4px") + + ((:and .recently-played-list .track-time) + :color "#666" + :font-size "0.85em") + + ((:and .recently-played-list .allmusic-link) + :display inline-flex + :align-items center + :gap "4px" + :color "#00ff00" + :text-decoration none + :font-size "0.85em" + :transition "color 0.2s" + + ((:and :hover) + :color "#00cc00" + :text-decoration underline) + + (svg :width "14px" + :height "14px"))) + (.back :color "#00ffff" :text-decoration none diff --git a/static/js/recently-played.js b/static/js/recently-played.js new file mode 100644 index 0000000..e3d7e03 --- /dev/null +++ b/static/js/recently-played.js @@ -0,0 +1,86 @@ +// Recently Played Tracks functionality + +async function updateRecentlyPlayed() { + try { + const response = await fetch('/api/asteroid/recently-played'); + const result = await response.json(); + + // Radiance wraps API responses in a data envelope + const data = result.data || result; + + if (data.status === 'success' && data.tracks && data.tracks.length > 0) { + const listEl = document.getElementById('recently-played-list'); + if (!listEl) return; + + // Build HTML for tracks + let html = ''; + listEl.innerHTML = html; + } else { + const listEl = document.getElementById('recently-played-list'); + if (listEl) { + listEl.innerHTML = '

No tracks played yet

'; + } + } + } catch (error) { + console.error('Error fetching recently played:', error); + const listEl = document.getElementById('recently-played-list'); + if (listEl) { + listEl.innerHTML = '

Error loading recently played tracks

'; + } + } +} + +function formatTimeAgo(timestamp) { + const now = Math.floor(Date.now() / 1000); + const diff = now - timestamp; + + if (diff < 60) return 'Just now'; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Initialize on page load +document.addEventListener('DOMContentLoaded', function() { + const panel = document.getElementById('recently-played-panel'); + if (panel) { + updateRecentlyPlayed(); + // Update every 30 seconds + setInterval(updateRecentlyPlayed, 30000); + } else { + const list = document.getElementById('recently-played-list'); + if (list) { + updateRecentlyPlayed(); + setInterval(updateRecentlyPlayed, 30000); + } + } +}); diff --git a/template/front-page-content.ctml b/template/front-page-content.ctml index 8018f0f..39ef292 100644 --- a/template/front-page-content.ctml +++ b/template/front-page-content.ctml @@ -7,6 +7,7 @@ +
@@ -35,6 +36,14 @@
+ + +
+

Recently Played

+
+

Loading...

+
+
diff --git a/template/front-page.ctml b/template/front-page.ctml index 7a3f0c8..f164a07 100644 --- a/template/front-page.ctml +++ b/template/front-page.ctml @@ -7,6 +7,7 @@ +
@@ -61,6 +62,14 @@
+ + +
+

Recently Played

+
+

Loading...

+
+
From 79ab87436ecd95c95c7744dcc3e05412cfa028ef Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Mon, 17 Nov 2025 15:27:26 +0300 Subject: [PATCH 2/4] Refine recently played styling and MusicBrainz search - Use 2-column grid layout: track/artist left, time/link right - Match color scheme with now-playing section (blue text) - Tighter row spacing (6px padding) - Simplified MusicBrainz search query (no field prefixes) - Fix CSS selector for proper link styling - Right-align time and MusicBrainz link --- asteroid.lisp | 6 +-- static/asteroid.css | 63 ++++++++++++++++++------------ static/asteroid.lass | 76 +++++++++++++++++++++--------------- static/js/recently-played.js | 2 +- 4 files changed, 86 insertions(+), 61 deletions(-) diff --git a/asteroid.lisp b/asteroid.lisp index 2deb56b..027305e 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -69,9 +69,9 @@ (defun generate-music-search-url (artist song) "Generate MusicBrainz search URL for artist and song" - ;; MusicBrainz uses 'artist:' and 'recording:' prefixes for better search - (let ((query (format nil "artist:\"~a\" recording:\"~a\"" artist song))) - (format nil "https://musicbrainz.org/search?query=~a&type=recording&method=indexed" + ;; Simple search without field prefixes works better with URL encoding + (let ((query (format nil "~a ~a" artist song))) + (format nil "https://musicbrainz.org/search?query=~a&type=recording" (drakma:url-encode query :utf-8)))) ;; API Routes using Radiance's define-api diff --git a/static/asteroid.css b/static/asteroid.css index 5d3e1a7..3518c32 100644 --- a/static/asteroid.css +++ b/static/asteroid.css @@ -185,7 +185,7 @@ body .recently-played-panel .recently-played-list.track-list{ } body .recently-played-panel .recently-played-list.track-item{ - padding: 12px; + padding: 6px 12px; border-bottom: 1px solid #2a3441; -moz-transition: background-color 0.2s; -o-transition: background-color 0.2s; @@ -199,58 +199,71 @@ body .recently-played-panel .recently-played-list.track-item LAST-CHILD{ } body .recently-played-panel .recently-played-list.track-item HOVER{ - background-color: #252525; + background-color: #2a3441; } body .recently-played-panel .recently-played-list.track-info{ - display: flex; - flex-direction: column; - gap: 4px; + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: auto auto; + gap: 2px 20px; + align-items: center; } body .recently-played-panel .recently-played-list.track-title{ - color: #fff; + color: #4488ff; font-weight: 500; font-size: 1em; + grid-column: 1; + grid-row: 1; } body .recently-played-panel .recently-played-list.track-artist{ - color: #aaa; + color: #4488ff; font-size: 0.9em; -} - -body .recently-played-panel .recently-played-list.track-meta{ - display: flex; - align-items: center; - gap: 12px; - margin-top: 4px; + grid-column: 1; + grid-row: 2; } body .recently-played-panel .recently-played-list.track-time{ - color: #666; + color: #888; font-size: 0.85em; + grid-column: 2; + grid-row: 1; + text-align: right; } -body .recently-played-panel .recently-played-list.allmusic-link{ +body .recently-played-panel .recently-played-list.track-meta{ + grid-column: 2; + grid-row: 2; + text-align: right; +} + + + +body .recently-played-list .allmusic-link{ display: inline-flex; align-items: center; gap: 4px; - color: #00ff00; + color: #4488ff; text-decoration: none; font-size: 0.85em; - -moz-transition: color 0.2s; - -o-transition: color 0.2s; - -webkit-transition: color 0.2s; - -ms-transition: color 0.2s; - transition: color 0.2s; + letter-spacing: 0.08rem; + -moz-transition: all 0.2s; + -o-transition: all 0.2s; + -webkit-transition: all 0.2s; + -ms-transition: all 0.2s; + transition: all 0.2s; + white-space: nowrap; } -body .recently-played-panel .recently-played-list.allmusic-link HOVER{ - color: #00cc00; +body .recently-played-list .allmusic-link HOVER{ + color: #00ff00; text-decoration: underline; + text-underline-offset: 5px; } -body .recently-played-panel .recently-played-list.allmusic-link svg{ +body .recently-played-list .allmusic-link svg{ width: 14px; height: 14px; } diff --git a/static/asteroid.lass b/static/asteroid.lass index da90b0c..0132ffc 100644 --- a/static/asteroid.lass +++ b/static/asteroid.lass @@ -156,52 +156,64 @@ :overflow-y visible) ((:and .recently-played-list .track-item) - :padding "12px" + :padding "6px 12px" :border-bottom "1px solid #2a3441" :transition "background-color 0.2s" ((:and :last-child) :border-bottom none) - ((:and :hover) :background-color "#252525")) + ((:and :hover) :background-color "#2a3441")) ((:and .recently-played-list .track-info) - :display flex - :flex-direction column - :gap "4px") + :display grid + :grid-template-columns "1fr auto" + :grid-template-rows "auto auto" + :gap "2px 20px" + :align-items center) ((:and .recently-played-list .track-title) - :color "#fff" + :color "#4488ff" :font-weight 500 - :font-size "1em") + :font-size "1em" + :grid-column 1 + :grid-row 1) ((:and .recently-played-list .track-artist) - :color "#aaa" - :font-size "0.9em") - - ((:and .recently-played-list .track-meta) - :display flex - :align-items center - :gap "12px" - :margin-top "4px") + :color "#4488ff" + :font-size "0.9em" + :grid-column 1 + :grid-row 2) ((:and .recently-played-list .track-time) - :color "#666" - :font-size "0.85em") - - ((:and .recently-played-list .allmusic-link) - :display inline-flex - :align-items center - :gap "4px" - :color "#00ff00" - :text-decoration none + :color "#888" :font-size "0.85em" - :transition "color 0.2s" - - ((:and :hover) - :color "#00cc00" - :text-decoration underline) - - (svg :width "14px" - :height "14px"))) + :grid-column 2 + :grid-row 1 + :text-align right) + + ((:and .recently-played-list .track-meta) + :grid-column 2 + :grid-row 2 + :text-align right)) + + (.recently-played-list + (.allmusic-link + :display inline-flex + :align-items center + :gap "4px" + :color "#4488ff" + :text-decoration none + :font-size "0.85em" + :letter-spacing "0.08rem" + :transition "all 0.2s" + :white-space nowrap + + ((:and :hover) + :color "#00ff00" + :text-decoration underline + :text-underline-offset "5px") + + (svg :width "14px" + :height "14px"))) (.back :color "#00ffff" diff --git a/static/js/recently-played.js b/static/js/recently-played.js index e3d7e03..db79f1c 100644 --- a/static/js/recently-played.js +++ b/static/js/recently-played.js @@ -21,8 +21,8 @@ async function updateRecentlyPlayed() {
${escapeHtml(track.song)}
${escapeHtml(track.artist)}
+ ${timeAgo}
- ${timeAgo} From 3a08ff61c0c591fa5917dd9ac8d07da8f3798913 Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Thu, 20 Nov 2025 11:29:31 +0300 Subject: [PATCH 3/4] feat: Add favicon and asteroid.png graphics to front page - Add favicon.ico, favicon-16x16.png, and favicon-32x32.png - Add asteroid.png graphic to header on both front-page.ctml and front-page-content.ctml - Update header styling to display asteroid graphics flanking the station name --- static/favicon-16x16.png | Bin 0 -> 2903 bytes static/favicon-32x32.png | Bin 0 -> 3967 bytes static/favicon.ico | Bin 0 -> 15086 bytes template/front-page-content.ctml | 9 ++++++++- template/front-page.ctml | 9 ++++++++- 5 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 static/favicon-16x16.png create mode 100644 static/favicon-32x32.png create mode 100644 static/favicon.ico diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..9da2059b26af33f6be4ab444fceb4fd187a8e2c6 GIT binary patch literal 2903 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 L3~pX|_6q$UXT(iI literal 0 HcmV?d00001 diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..650b637c0907b4585d4b10fd91fb784a821ab22d GIT binary patch literal 3967 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 Ng^sU;?xWE0{{tVqENuV) literal 0 HcmV?d00001 diff --git a/template/front-page-content.ctml b/template/front-page-content.ctml index 39ef292..b8912dd 100644 --- a/template/front-page-content.ctml +++ b/template/front-page-content.ctml @@ -4,6 +4,9 @@ 🎵 ASTEROID RADIO 🎵 + + + @@ -12,7 +15,11 @@
-

🎵 ASTEROID RADIO 🎵

+

+ Asteroid + ASTEROID RADIO + Asteroid +