fix: Favorites star UI and track-id lookup

- Fix find-track-by-title to parse 'Artist - Title' format from Icecast
  and search both artist and title columns in tracks table
- Fix favorites API alist key mismatch (TRACK-TITLE not TRACK_TITLE)
- Fix favorites cache to update UI after loading
- Fix race condition where star reverted after clicking
- Add aget-profile helper for Postmodern uppercase key lookup
This commit is contained in:
glenneth 2025-12-27 20:06:37 +03:00
parent 820228bac1
commit 0d8f4c664a
4 changed files with 65 additions and 33 deletions

View File

@ -1,18 +1,35 @@
(in-package :asteroid) (in-package :asteroid)
(defun find-track-by-title (title) (defun find-track-by-title (title)
"Find a track in the database by its title. Returns track ID or nil." "Find a track in the database by its title. Returns track ID or nil.
Handles 'Artist - Title' format from Icecast metadata."
(when (and title (not (string= title "Unknown"))) (when (and title (not (string= title "Unknown")))
(handler-case (handler-case
(with-db (with-db
(let* ((search-pattern (format nil "%~a%" title)) ;; Parse 'Artist - Title' format if present
(result (postmodern:query (let* ((parts (cl-ppcre:split " - " title :limit 2))
(:limit (has-artist (> (length parts) 1))
(:select '_id (artist-part (when has-artist (first parts)))
:from 'tracks (title-part (if has-artist (second parts) title))
:where (:ilike 'title search-pattern)) (result
1) (if has-artist
:single))) ;; Search by both artist and title
(postmodern:query
(:limit
(:select '_id
:from 'tracks
:where (:and (:ilike 'artist (format nil "%~a%" artist-part))
(:ilike 'title (format nil "%~a%" title-part))))
1)
:single)
;; Fallback: search by title only
(postmodern:query
(:limit
(:select '_id
:from 'tracks
:where (:ilike 'title (format nil "%~a%" title-part)))
1)
:single))))
result)) result))
(error (e) (error (e)
(declare (ignore e)) (declare (ignore e))

View File

@ -183,7 +183,9 @@
(when (and data (ps:@ data data) (ps:@ data data favorites)) (when (and data (ps:@ data data) (ps:@ data data favorites))
(setf *user-favorites-cache* (setf *user-favorites-cache*
(ps:chain (ps:@ data data favorites) (ps:chain (ps:@ data data favorites)
(map (lambda (f) (ps:@ f title)))))))) (map (lambda (f) (ps:@ f title)))))
;; Update UI after cache is loaded
(check-favorite-status))))
(catch (lambda (error) nil)))) (catch (lambda (error) nil))))
;; Check if current track is in favorites and update UI ;; Check if current track is in favorites and update UI
@ -699,8 +701,9 @@
(= (ps:@ data data status) "success"))) (= (ps:@ data data status) "success")))
(ps:chain btn class-list (remove "favorited")) (ps:chain btn class-list (remove "favorited"))
(setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "☆") (setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "☆")
;; Refresh now playing to update favorite count ;; Reload cache (don't call update-now-playing as it would
(update-now-playing)))) ;; check the old cache before reload completes)
(load-favorites-cache))))
(catch (lambda (error) (catch (lambda (error)
(ps:chain console (error "Error removing favorite:" error))))) (ps:chain console (error "Error removing favorite:" error)))))
;; Add favorite ;; Add favorite
@ -718,7 +721,9 @@
(= (ps:@ data data status) "success"))) (= (ps:@ data data status) "success")))
(ps:chain btn class-list (add "favorited")) (ps:chain btn class-list (add "favorited"))
(setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "★") (setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "★")
(update-now-playing)))) ;; Reload cache (don't call update-now-playing as it would
;; check the old cache before reload completes)
(load-favorites-cache))))
(catch (lambda (error) (catch (lambda (error)
(ps:chain console (error "Error adding favorite:" error))))))))))) (ps:chain console (error "Error adding favorite:" error)))))))))))

View File

@ -213,10 +213,15 @@
(ps:chain response (json)) (ps:chain response (json))
nil))) nil)))
(then (lambda (data) (then (lambda (data)
(when (and data (ps:@ data data) (ps:@ data data favorites)) (when data
(setf *user-favorites-cache-mini* ;; Handle both wrapped (data.data.favorites) and unwrapped (data.favorites) responses
(ps:chain (ps:@ data data favorites) (let ((favorites (or (and (ps:@ data data) (ps:@ data data favorites))
(map (lambda (f) (ps:@ f title)))))))) (ps:@ data favorites))))
(when favorites
(setf *user-favorites-cache-mini*
(ps:chain favorites (map (lambda (f) (ps:@ f title)))))
;; Update UI after cache is loaded
(check-favorite-status-mini))))))
(catch (lambda (error) nil)))) (catch (lambda (error) nil))))
;; Check if current track is in favorites and update mini player UI ;; Check if current track is in favorites and update mini player UI
@ -224,9 +229,10 @@
(let ((title-el (ps:chain document (get-element-by-id "mini-now-playing"))) (let ((title-el (ps:chain document (get-element-by-id "mini-now-playing")))
(btn (ps:chain document (get-element-by-id "favorite-btn-mini")))) (btn (ps:chain document (get-element-by-id "favorite-btn-mini"))))
(when (and title-el btn) (when (and title-el btn)
(let ((title (ps:@ title-el text-content)) (let* ((track-title (ps:@ title-el text-content))
(star-icon (ps:chain btn (query-selector ".star-icon")))) (star-icon (ps:chain btn (query-selector ".star-icon")))
(if (ps:chain *user-favorites-cache-mini* (includes title)) (is-in-cache (ps:chain *user-favorites-cache-mini* (includes track-title))))
(if is-in-cache
(progn (progn
(ps:chain btn class-list (add "favorited")) (ps:chain btn class-list (add "favorited"))
(when star-icon (setf (ps:@ star-icon text-content) "★"))) (when star-icon (setf (ps:@ star-icon text-content) "★")))
@ -310,9 +316,9 @@
(= (ps:@ data data status) "success"))) (= (ps:@ data data status) "success")))
(ps:chain btn class-list (remove "favorited")) (ps:chain btn class-list (remove "favorited"))
(setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "☆") (setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "☆")
;; Reload cache and refresh display to update favorite count ;; Reload cache to update favorite count (don't call update-mini-now-playing
(load-favorites-cache-mini) ;; as it would check the old cache before reload completes)
(update-mini-now-playing)))) (load-favorites-cache-mini))))
(catch (lambda (error) (catch (lambda (error)
(ps:chain console (error "Error removing favorite:" error))))) (ps:chain console (error "Error removing favorite:" error)))))
;; Add favorite ;; Add favorite
@ -330,9 +336,9 @@
(= (ps:@ data data status) "success"))) (= (ps:@ data data status) "success")))
(ps:chain btn class-list (add "favorited")) (ps:chain btn class-list (add "favorited"))
(setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "★") (setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "★")
;; Reload cache and refresh display to update favorite count ;; Reload cache to update favorite count (don't call update-mini-now-playing
(load-favorites-cache-mini) ;; as it would check the old cache before reload completes)
(update-mini-now-playing)))) (load-favorites-cache-mini))))
(catch (lambda (error) (catch (lambda (error)
(ps:chain console (error "Error adding favorite:" error))))))))))) (ps:chain console (error "Error adding favorite:" error)))))))))))

View File

@ -161,6 +161,10 @@
;;; API Endpoints for User Favorites ;;; API Endpoints for User Favorites
;;; ========================================================================== ;;; ==========================================================================
(defun aget-profile (key alist)
"Get value from alist using string-equal comparison for key (Postmodern returns uppercase keys)"
(cdr (assoc key alist :test (lambda (a b) (string-equal (string a) (string b))))))
(define-api asteroid/user/favorites () () (define-api asteroid/user/favorites () ()
"Get current user's favorite tracks" "Get current user's favorite tracks"
(require-authentication) (require-authentication)
@ -168,13 +172,13 @@
(let* ((user-id (session:field "user-id")) (let* ((user-id (session:field "user-id"))
(favorites (get-user-favorites user-id))) (favorites (get-user-favorites user-id)))
(api-output `(("status" . "success") (api-output `(("status" . "success")
("favorites" . ,(mapcar (lambda (fav) ("favorites" . ,(or (mapcar (lambda (fav)
`(("id" . ,(cdr (assoc :_id fav))) `(("id" . ,(aget-profile "-ID" fav))
("track_id" . ,(cdr (assoc :track-id fav))) ("track_id" . ,(aget-profile "TRACK-ID" fav))
("title" . ,(or (cdr (assoc :track-title fav)) ("title" . ,(aget-profile "TRACK-TITLE" fav))
(cdr (assoc :track_title fav)))) ("rating" . ,(aget-profile "RATING" fav))))
("rating" . ,(cdr (assoc :rating fav))))) favorites)
favorites)) (list))) ; Return empty list instead of null
("count" . ,(get-favorites-count user-id))))))) ("count" . ,(get-favorites-count user-id)))))))
(define-api asteroid/user/favorites/add (&optional track-id rating title) () (define-api asteroid/user/favorites/add (&optional track-id rating title) ()