Compare commits

..

No commits in common. "4a79558c75c861fc2e4170d058e737eef6de7075" and "820228bac1dacdc27f627d5b0c9bbcab8686c9cd" have entirely different histories.

4 changed files with 36 additions and 79 deletions

View File

@ -1,39 +1,23 @@
(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
;; Parse 'Artist - Title' format if present (let* ((search-pattern (format nil "%~a%" title))
(let* ((parts (cl-ppcre:split " - " title :limit 2)) (result (postmodern:query
(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 (:limit
(:select '_id (:select '_id
:from 'tracks :from 'tracks
:where (:and (:ilike 'artist (format nil "%~a%" artist-part)) :where (:ilike 'title search-pattern))
(:ilike 'title (format nil "%~a%" title-part))))
1) 1)
:single) :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))
nil)))) nil))))
(defun icecast-now-playing (icecast-base-url &optional (mount "asteroid.mp3")) (defun icecast-now-playing (icecast-base-url &optional (mount "asteroid.mp3"))
"Fetch now-playing information from Icecast server. "Fetch now-playing information from Icecast server.

View File

@ -183,9 +183,7 @@
(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
@ -701,9 +699,8 @@
(= (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 (don't call update-now-playing as it would ;; Refresh now playing to update favorite count
;; check the old cache before reload completes) (update-now-playing))))
(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
@ -721,9 +718,7 @@
(= (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 (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 adding favorite:" error))))))))))) (ps:chain console (error "Error adding favorite:" error)))))))))))

View File

@ -213,15 +213,10 @@
(ps:chain response (json)) (ps:chain response (json))
nil))) nil)))
(then (lambda (data) (then (lambda (data)
(when data (when (and data (ps:@ data data) (ps:@ data data favorites))
;; 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* (setf *user-favorites-cache-mini*
(ps:chain favorites (map (lambda (f) (ps:@ f title))))) (ps:chain (ps:@ data data favorites)
;; Update UI after cache is loaded (map (lambda (f) (ps:@ f title))))))))
(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
@ -229,10 +224,9 @@
(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* ((track-title (ps:@ title-el text-content)) (let ((title (ps:@ title-el text-content))
(star-icon (ps:chain btn (query-selector ".star-icon"))) (star-icon (ps:chain btn (query-selector ".star-icon"))))
(is-in-cache (ps:chain *user-favorites-cache-mini* (includes track-title)))) (if (ps:chain *user-favorites-cache-mini* (includes 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) "★")))
@ -316,9 +310,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 to update favorite count (don't call update-mini-now-playing ;; Reload cache and refresh display to update favorite count
;; as it would check the old cache before reload completes) (load-favorites-cache-mini)
(load-favorites-cache-mini)))) (update-mini-now-playing))))
(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
@ -336,9 +330,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 to update favorite count (don't call update-mini-now-playing ;; Reload cache and refresh display to update favorite count
;; as it would check the old cache before reload completes) (load-favorites-cache-mini)
(load-favorites-cache-mini)))) (update-mini-now-playing))))
(catch (lambda (error) (catch (lambda (error)
(ps:chain console (error "Error adding favorite:" error))))))))))) (ps:chain console (error "Error adding favorite:" error)))))))))))

View File

@ -10,8 +10,6 @@
(defun add-favorite (user-id track-id &optional (rating 1) track-title) (defun add-favorite (user-id track-id &optional (rating 1) track-title)
"Add a track to user's favorites with optional rating (1-5). "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." 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))))) (let ((rating-val (max 1 (min 5 (or rating 1)))))
(with-db (with-db
(if track-id (if track-id
@ -28,8 +26,6 @@
(defun remove-favorite (user-id track-id &optional track-title) (defun remove-favorite (user-id track-id &optional track-title)
"Remove a track from user's favorites by track-id or title" "Remove a track from user's favorites by track-id or title"
(when (null user-id)
(return-from remove-favorite nil))
(with-db (with-db
(if track-id (if track-id
(postmodern:query (postmodern:query
@ -42,8 +38,6 @@
(defun update-favorite-rating (user-id track-id rating) (defun update-favorite-rating (user-id track-id rating)
"Update the rating for a favorited track" "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)))) (let ((rating-val (max 1 (min 5 rating))))
(with-db (with-db
(postmodern:query (postmodern:query
@ -54,8 +48,6 @@
(defun get-user-favorites (user-id &key (limit 50) (offset 0)) (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" "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 (with-db
(postmodern:query (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" (: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"
@ -64,8 +56,6 @@
(defun is-track-favorited (user-id track-id) (defun is-track-favorited (user-id track-id)
"Check if a track is in user's favorites, returns rating or nil" "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 (with-db
(postmodern:query (postmodern:query
(:raw (format nil "SELECT rating FROM user_favorites WHERE \"user-id\" = ~a AND \"track-id\" = ~a" (:raw (format nil "SELECT rating FROM user_favorites WHERE \"user-id\" = ~a AND \"track-id\" = ~a"
@ -74,8 +64,6 @@
(defun get-favorites-count (user-id) (defun get-favorites-count (user-id)
"Get total count of user's favorites" "Get total count of user's favorites"
(when (null user-id)
(return-from get-favorites-count 0))
(with-db (with-db
(postmodern:query (postmodern:query
(:raw (format nil "SELECT COUNT(*) FROM user_favorites WHERE \"user-id\" = ~a" user-id)) (:raw (format nil "SELECT COUNT(*) FROM user_favorites WHERE \"user-id\" = ~a" user-id))
@ -123,11 +111,11 @@
(:raw (format nil "INSERT INTO listening_history (\"user-id\", \"track-id\", track_title, \"listen-duration\", completed) VALUES (~a, ~a, ~a, ~a, ~a)" (: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 user-id track-id
(if track-title (format nil "'~a'" (sql-escape-string track-title)) "NULL") (if track-title (format nil "'~a'" (sql-escape-string track-title)) "NULL")
duration (if completed "TRUE" "FALSE")))) duration (if completed 1 0))))
(when track-title (when track-title
(postmodern:query (postmodern:query
(:raw (format nil "INSERT INTO listening_history (\"user-id\", track_title, \"listen-duration\", completed) VALUES (~a, '~a', ~a, ~a)" (: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 "TRUE" "FALSE")))))))))) user-id (sql-escape-string track-title) duration (if completed 1 0))))))))))
(defun get-listening-history (user-id &key (limit 20) (offset 0)) (defun get-listening-history (user-id &key (limit 20) (offset 0))
"Get user's listening history - works with title-based history" "Get user's listening history - works with title-based history"
@ -173,10 +161,6 @@
;;; 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)
@ -184,13 +168,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" . ,(or (mapcar (lambda (fav) ("favorites" . ,(mapcar (lambda (fav)
`(("id" . ,(aget-profile "-ID" fav)) `(("id" . ,(cdr (assoc :_id fav)))
("track_id" . ,(aget-profile "TRACK-ID" fav)) ("track_id" . ,(cdr (assoc :track-id fav)))
("title" . ,(aget-profile "TRACK-TITLE" fav)) ("title" . ,(or (cdr (assoc :track-title fav))
("rating" . ,(aget-profile "RATING" fav)))) (cdr (assoc :track_title fav))))
favorites) ("rating" . ,(cdr (assoc :rating fav)))))
(list))) ; Return empty list instead of null favorites))
("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) ()