Compare commits

...

2 Commits

Author SHA1 Message Date
Luis Pereira 2effe3bdef fix: listening history using integers 2025-12-27 17:02:31 -05:00
Luis Pereira 554f23ac40 fix: move playlist functions to data-model 2025-12-27 17:02:31 -05:00
1 changed files with 61 additions and 69 deletions

View File

@ -7,90 +7,79 @@
;;; User Favorites - Track likes/ratings ;;; User Favorites - Track likes/ratings
;;; ========================================================================== ;;; ==========================================================================
(defun get-favorite (user-id track-id &optional track-title)
"Gets a user's favorite track by id or name"
(when (and user-id (or track-id track-title))
(let ((query (if track-id
(db:query (:and (:= 'user-id user-id) (:= 'track-id track-id)))
(when track-title
(db:query (:and (:= 'user-id user-id) (:= 'track_title track-title)))))))
(when query
(dm:get-one "user_favorites" query)))))
(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) When favorite already exists for user, returns it instead to avoid duplicates."
(return-from add-favorite nil)) (when (and user-id (or track-id track-title))
(let ((rating-val (max 1 (min 5 (or rating 1))))) (let ((favorite (get-favorite user-id track-id track-title)))
(with-db (if favorite
(if track-id favorite
(postmodern:query (let ((rating-val (max 1 (min 5 (or rating 1))))
(:raw (format nil "INSERT INTO user_favorites (\"user-id\", \"track-id\", track_title, rating) VALUES (~a, ~a, ~a, ~a)" (favorite (dm:hull "user_favorites")))
user-id track-id (setf (dm:field favorite "user-id") user-id)
(if track-title (format nil "$$~a$$" track-title) "NULL") (setf (dm:field favorite "rating") rating-val)
rating-val))) (when track-id
;; No track-id, store by title only (setf (dm:field favorite "track-id") track-id))
(when track-title (when track-title
(postmodern:query (setf (dm:field favorite "track_title") track-title))
(:raw (format nil "INSERT INTO user_favorites (\"user-id\", track_title, rating) VALUES (~a, $$~a$$, ~a)" (dm:insert favorite))))))
user-id track-title rating-val))))))))
(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) (let ((favorite (get-favorite user-id track-id track-title)))
(return-from remove-favorite nil)) (when favorite
(with-db (dm:delete favorite))))
(if track-id
(postmodern:query
(:raw (format nil "DELETE FROM user_favorites WHERE \"user-id\" = ~a AND \"track-id\" = ~a"
user-id track-id)))
(when track-title
(postmodern:query
(:raw (format nil "DELETE FROM user_favorites WHERE \"user-id\" = ~a AND track_title = $$~a$$"
user-id track-title)))))))
(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) (when (null user-id)
(return-from update-favorite-rating nil)) (return-from update-favorite-rating nil))
(let ((rating-val (max 1 (min 5 rating)))) (let ((rating-val (max 1 (min 5 rating)))
(with-db (favorite (get-favorite user-id track-id)))
(postmodern:query (unless favorite
(:update 'user_favorites (error 'not-found-error
:set 'rating rating-val :message (format nil "Favorite #~a not found for user #~a"
:where (:and (:= '"user-id" user-id) track-id
(:= '"track-id" track-id))))))) user-id)))
(setf (dm:field favorite "rating-val") rating-val)
(data-model-save favorite)))
(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) (when user-id
(return-from get-user-favorites nil)) (dm:get "user_favorites" (db:query (:= 'user-id user-id))
(with-db :amount limit
(postmodern:query :skip offset
(: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" :sort '(("created-date" :DESC)))))
user-id limit offset))
:alists)))
(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) (when (and user-id track-id)
(return-from is-track-favorited nil)) (dm:get-one "user_favorites" (db:query (:and (:= 'user-id user-id)
(with-db (:= 'track-id track-id))))))
(postmodern:query
(:raw (format nil "SELECT rating FROM user_favorites WHERE \"user-id\" = ~a AND \"track-id\" = ~a"
user-id track-id))
:single)))
(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) (when user-id
(return-from get-favorites-count 0)) (db:count "user_favorites" (db:query (:= 'user-id user-id)))))
(with-db
(postmodern:query
(:raw (format nil "SELECT COUNT(*) FROM user_favorites WHERE \"user-id\" = ~a" user-id))
:single)))
(defun get-track-favorite-count (track-title) (defun get-track-favorite-count (track-title)
"Get count of how many users have favorited a track by title" "Get count of how many users have favorited a track by title"
(if (and track-title (not (string= track-title ""))) (if (and track-title (not (string= track-title "")))
(handler-case (handler-case
(with-db (let ((result (db:count "user_favorites" (db:query (:= 'track_title track-title)))))
(let* ((escaped-title (sql-escape-string track-title)) (or result 0))
(result (postmodern:query
(:raw (format nil "SELECT COUNT(*) FROM user_favorites WHERE track_title = '~a'" escaped-title))
:single)))
(or result 0)))
(error (e) (error (e)
(declare (ignore e)) (declare (ignore e))
0)) 0))
@ -123,11 +112,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"
@ -185,12 +174,12 @@
(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" . ,(or (mapcar (lambda (fav)
`(("id" . ,(aget-profile "-ID" fav)) `(("id" . ,(dm:id fav))
("track_id" . ,(aget-profile "TRACK-ID" fav)) ("track_id" . ,(dm:field fav "track-id"))
("title" . ,(aget-profile "TRACK-TITLE" fav)) ("title" . ,(dm:field fav "track_title"))
("rating" . ,(aget-profile "RATING" fav)))) ("rating" . ,(dm:field fav "rating"))))
favorites) favorites)
(list))) ; Return empty list instead of null (list)))
("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) ()
@ -204,6 +193,9 @@
(track-id-int (when (and track-id (not (string= track-id ""))) (track-id-int (when (and track-id (not (string= track-id "")))
(parse-integer track-id :junk-allowed t))) (parse-integer track-id :junk-allowed t)))
(rating-int (if rating (parse-integer rating :junk-allowed t) 1))) (rating-int (if rating (parse-integer rating :junk-allowed t) 1)))
(unless user-id
(error 'authentication-error
:message "User not authenticated"))
(format t "Adding favorite: user-id=~a track-id=~a title=~a~%" user-id track-id-int title) (format t "Adding favorite: user-id=~a track-id=~a title=~a~%" user-id track-id-int title)
(add-favorite user-id track-id-int (or rating-int 1) title) (add-favorite user-id track-id-int (or rating-int 1) title)
(api-output `(("status" . "success") (api-output `(("status" . "success")