diff --git a/frontend-partials.lisp b/frontend-partials.lisp index 4fd15da..4035e71 100644 --- a/frontend-partials.lisp +++ b/frontend-partials.lisp @@ -1,23 +1,39 @@ (in-package :asteroid) (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"))) (handler-case (with-db - (let* ((search-pattern (format nil "%~a%" title)) - (result (postmodern:query - (:limit - (:select '_id - :from 'tracks - :where (:ilike 'title search-pattern)) - 1) - :single))) + ;; Parse 'Artist - Title' format if present + (let* ((parts (cl-ppcre:split " - " title :limit 2)) + (has-artist (> (length parts) 1)) + (artist-part (when has-artist (first parts))) + (title-part (if has-artist (second parts) title)) + (result + (if has-artist + ;; 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)) (error (e) (declare (ignore e)) nil)))) - (defun icecast-now-playing (icecast-base-url &optional (mount "asteroid.mp3")) "Fetch now-playing information from Icecast server. diff --git a/parenscript/front-page.lisp b/parenscript/front-page.lisp index 3477a7e..487999d 100644 --- a/parenscript/front-page.lisp +++ b/parenscript/front-page.lisp @@ -183,7 +183,9 @@ (when (and data (ps:@ data data) (ps:@ data data favorites)) (setf *user-favorites-cache* (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)))) ;; Check if current track is in favorites and update UI @@ -699,8 +701,9 @@ (= (ps:@ data data status) "success"))) (ps:chain btn class-list (remove "favorited")) (setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "☆") - ;; Refresh now playing to update favorite count - (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) (ps:chain console (error "Error removing favorite:" error))))) ;; Add favorite @@ -718,7 +721,9 @@ (= (ps:@ data data status) "success"))) (ps:chain btn class-list (add "favorited")) (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) (ps:chain console (error "Error adding favorite:" error))))))))))) diff --git a/parenscript/stream-player.lisp b/parenscript/stream-player.lisp index cad3fb2..491daef 100644 --- a/parenscript/stream-player.lisp +++ b/parenscript/stream-player.lisp @@ -213,10 +213,15 @@ (ps:chain response (json)) nil))) (then (lambda (data) - (when (and data (ps:@ data data) (ps:@ data data favorites)) - (setf *user-favorites-cache-mini* - (ps:chain (ps:@ data data favorites) - (map (lambda (f) (ps:@ f title)))))))) + (when data + ;; Handle both wrapped (data.data.favorites) and unwrapped (data.favorites) responses + (let ((favorites (or (and (ps:@ data data) (ps:@ data data favorites)) + (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)))) ;; 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"))) (btn (ps:chain document (get-element-by-id "favorite-btn-mini")))) (when (and title-el btn) - (let ((title (ps:@ title-el text-content)) - (star-icon (ps:chain btn (query-selector ".star-icon")))) - (if (ps:chain *user-favorites-cache-mini* (includes title)) + (let* ((track-title (ps:@ title-el text-content)) + (star-icon (ps:chain btn (query-selector ".star-icon"))) + (is-in-cache (ps:chain *user-favorites-cache-mini* (includes track-title)))) + (if is-in-cache (progn (ps:chain btn class-list (add "favorited")) (when star-icon (setf (ps:@ star-icon text-content) "★"))) @@ -310,9 +316,9 @@ (= (ps:@ data data status) "success"))) (ps:chain btn class-list (remove "favorited")) (setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "☆") - ;; Reload cache and refresh display to update favorite count - (load-favorites-cache-mini) - (update-mini-now-playing)))) + ;; Reload cache to update favorite count (don't call update-mini-now-playing + ;; as it would check the old cache before reload completes) + (load-favorites-cache-mini)))) (catch (lambda (error) (ps:chain console (error "Error removing favorite:" error))))) ;; Add favorite @@ -330,9 +336,9 @@ (= (ps:@ data data status) "success"))) (ps:chain btn class-list (add "favorited")) (setf (ps:@ (ps:chain btn (query-selector ".star-icon")) text-content) "★") - ;; Reload cache and refresh display to update favorite count - (load-favorites-cache-mini) - (update-mini-now-playing)))) + ;; Reload cache to update favorite count (don't call update-mini-now-playing + ;; as it would check the old cache before reload completes) + (load-favorites-cache-mini)))) (catch (lambda (error) (ps:chain console (error "Error adding favorite:" error))))))))))) diff --git a/user-profile.lisp b/user-profile.lisp index e811132..4f33403 100644 --- a/user-profile.lisp +++ b/user-profile.lisp @@ -10,6 +10,8 @@ (defun add-favorite (user-id track-id &optional (rating 1) track-title) "Add a track to user's favorites with optional rating (1-5). If track-id is nil but track-title is provided, stores by title." + (when (null user-id) + (return-from add-favorite nil)) (let ((rating-val (max 1 (min 5 (or rating 1))))) (with-db (if track-id @@ -26,6 +28,8 @@ (defun remove-favorite (user-id track-id &optional track-title) "Remove a track from user's favorites by track-id or title" + (when (null user-id) + (return-from remove-favorite nil)) (with-db (if track-id (postmodern:query @@ -38,6 +42,8 @@ (defun update-favorite-rating (user-id track-id rating) "Update the rating for a favorited track" + (when (null user-id) + (return-from update-favorite-rating nil)) (let ((rating-val (max 1 (min 5 rating)))) (with-db (postmodern:query @@ -48,6 +54,8 @@ (defun get-user-favorites (user-id &key (limit 50) (offset 0)) "Get user's favorite tracks - works with both track-id and title-based favorites" + (when (null user-id) + (return-from get-user-favorites nil)) (with-db (postmodern:query (:raw (format nil "SELECT _id, rating, \"created-date\", track_title, \"track-id\" FROM user_favorites WHERE \"user-id\" = ~a ORDER BY \"created-date\" DESC LIMIT ~a OFFSET ~a" @@ -56,6 +64,8 @@ (defun is-track-favorited (user-id track-id) "Check if a track is in user's favorites, returns rating or nil" + (when (null user-id) + (return-from is-track-favorited nil)) (with-db (postmodern:query (:raw (format nil "SELECT rating FROM user_favorites WHERE \"user-id\" = ~a AND \"track-id\" = ~a" @@ -64,6 +74,8 @@ (defun get-favorites-count (user-id) "Get total count of user's favorites" + (when (null user-id) + (return-from get-favorites-count 0)) (with-db (postmodern:query (:raw (format nil "SELECT COUNT(*) FROM user_favorites WHERE \"user-id\" = ~a" user-id)) @@ -111,11 +123,11 @@ (:raw (format nil "INSERT INTO listening_history (\"user-id\", \"track-id\", track_title, \"listen-duration\", completed) VALUES (~a, ~a, ~a, ~a, ~a)" user-id track-id (if track-title (format nil "'~a'" (sql-escape-string track-title)) "NULL") - duration (if completed 1 0)))) + duration (if completed "TRUE" "FALSE")))) (when track-title (postmodern:query (:raw (format nil "INSERT INTO listening_history (\"user-id\", track_title, \"listen-duration\", completed) VALUES (~a, '~a', ~a, ~a)" - user-id (sql-escape-string track-title) duration (if completed 1 0)))))))))) + user-id (sql-escape-string track-title) duration (if completed "TRUE" "FALSE")))))))))) (defun get-listening-history (user-id &key (limit 20) (offset 0)) "Get user's listening history - works with title-based history" @@ -161,6 +173,10 @@ ;;; 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 () () "Get current user's favorite tracks" (require-authentication) @@ -168,13 +184,13 @@ (let* ((user-id (session:field "user-id")) (favorites (get-user-favorites user-id))) (api-output `(("status" . "success") - ("favorites" . ,(mapcar (lambda (fav) - `(("id" . ,(cdr (assoc :_id fav))) - ("track_id" . ,(cdr (assoc :track-id fav))) - ("title" . ,(or (cdr (assoc :track-title fav)) - (cdr (assoc :track_title fav)))) - ("rating" . ,(cdr (assoc :rating fav))))) - favorites)) + ("favorites" . ,(or (mapcar (lambda (fav) + `(("id" . ,(aget-profile "-ID" fav)) + ("track_id" . ,(aget-profile "TRACK-ID" fav)) + ("title" . ,(aget-profile "TRACK-TITLE" fav)) + ("rating" . ,(aget-profile "RATING" fav)))) + favorites) + (list))) ; Return empty list instead of null ("count" . ,(get-favorites-count user-id))))))) (define-api asteroid/user/favorites/add (&optional track-id rating title) ()