diff --git a/asteroid.lisp b/asteroid.lisp index 6708dc8..cb274af 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -51,16 +51,16 @@ (require-authentication) (with-error-handling (let ((tracks (with-db-error-handling "select" - (db:select "tracks" (db:query :all))))) + (dm:get "tracks" (db:query :all))))) (api-output `(("status" . "success") ("tracks" . ,(mapcar (lambda (track) - `(("id" . ,(gethash "_id" track)) - ("title" . ,(first (gethash "title" track))) - ("artist" . ,(first (gethash "artist" track))) - ("album" . ,(first (gethash "album" track))) - ("duration" . ,(first (gethash "duration" track))) - ("format" . ,(first (gethash "format" track))) - ("bitrate" . ,(first (gethash "bitrate" track))))) + `(("id" . ,(dm:id track)) + ("title" . ,(dm:field track "title")) + ("artist" . ,(dm:field track "artist")) + ("album" . ,(dm:field track "album")) + ("duration" . ,(dm:field track "duration")) + ("format" . ,(dm:field track "format")) + ("bitrate" . ,(dm:field track "bitrate")))) tracks))))))) ;; Playlist API endpoints @@ -73,26 +73,19 @@ (playlists (get-user-playlists user-id))) (api-output `(("status" . "success") ("playlists" . ,(mapcar (lambda (playlist) - (let ((name-val (gethash "name" playlist)) - (desc-val (gethash "description" playlist)) - (track-ids-val (gethash "track-ids" playlist)) - (created-val (gethash "created-date" playlist)) - (id-val (gethash "_id" playlist))) - ;; Calculate track count from comma-separated string - ;; Handle nil, empty string, or list containing empty string - (let* ((track-ids-str (if (listp track-ids-val) - (first track-ids-val) - track-ids-val)) - (track-count (if (and track-ids-str - (stringp track-ids-str) - (not (string= track-ids-str ""))) - (length (cl-ppcre:split "," track-ids-str)) - 0))) - `(("id" . ,(if (listp id-val) (first id-val) id-val)) - ("name" . ,(if (listp name-val) (first name-val) name-val)) - ("description" . ,(if (listp desc-val) (first desc-val) desc-val)) - ("track-count" . ,track-count) - ("created-date" . ,(if (listp created-val) (first created-val) created-val)))))) + (let* ((track-ids (dm:field playlist "track-ids")) + ;; Calculate track count from comma-separated string + ;; Handle nil, empty string, or list containing empty string + (track-count (if (and track-ids + (stringp track-ids) + (not (string= track-ids ""))) + (length (cl-ppcre:split "," track-ids)) + 0))) + `(("id" . ,(dm:id playlist)) + ("name" . ,(dm:field playlist "name")) + ("description" . ,(dm:field playlist "description")) + ("track-count" . ,track-count) + ("created-date" . ,(dm:field playlist "created-date"))))) playlists))))))) (define-api asteroid/playlists/create (name &optional description) () @@ -124,23 +117,19 @@ (let* ((id (parse-integer playlist-id :junk-allowed t)) (playlist (get-playlist-by-id id))) (if playlist - (let* ((track-ids-raw (gethash "tracks" playlist)) - (track-ids (if (listp track-ids-raw) track-ids-raw (list track-ids-raw))) + (let* ((track-ids (dm:field playlist "tracks")) (tracks (mapcar (lambda (track-id) - (let ((track-list (db:select "tracks" (db:query (:= "_id" track-id))))) - (when (> (length track-list) 0) - (first track-list)))) + (dm:get-one "tracks" (db:query (:= '_id track-id)))) track-ids)) (valid-tracks (remove nil tracks))) (api-output `(("status" . "success") ("playlist" . (("id" . ,id) - ("name" . ,(let ((n (gethash "name" playlist))) - (if (listp n) (first n) n))) + ("name" . ,(dm:field playlist "name")) ("tracks" . ,(mapcar (lambda (track) - `(("id" . ,(gethash "_id" track)) - ("title" . ,(gethash "title" track)) - ("artist" . ,(gethash "artist" track)) - ("album" . ,(gethash "album" track)))) + `(("id" . ,(dm:id track)) + ("title" . ,(dm:field track "title")) + ("artist" . ,(dm:field track "artist")) + ("album" . ,(dm:field track "album")))) valid-tracks))))))) (api-output `(("status" . "error") ("message" . "Playlist not found")) @@ -152,15 +141,15 @@ (require-authentication) (with-error-handling (let ((tracks (with-db-error-handling "select" - (db:select "tracks" (db:query :all))))) + (dm:get "tracks" (db:query :all))))) (api-output `(("status" . "success") ("tracks" . ,(mapcar (lambda (track) - `(("id" . ,(gethash "_id" track)) - ("title" . ,(gethash "title" track)) - ("artist" . ,(gethash "artist" track)) - ("album" . ,(gethash "album" track)) - ("duration" . ,(gethash "duration" track)) - ("format" . ,(gethash "format" track)))) + `(("id" . ,(dm:id track)) + ("title" . ,(dm:field track "title")) + ("artist" . ,(dm:field track "artist")) + ("album" . ,(dm:field track "album")) + ("duration" . ,(dm:field track "duration")) + ("format" . ,(dm:field track "format")))) tracks))))))) ;; Stream Control API Endpoints @@ -173,9 +162,9 @@ ("queue" . ,(mapcar (lambda (track-id) (let ((track (get-track-by-id track-id))) `(("id" . ,track-id) - ("title" . ,(gethash "title" track)) - ("artist" . ,(gethash "artist" track)) - ("album" . ,(gethash "album" track))))) + ("title" . ,(dm:field track "title")) + ("artist" . ,(dm:field track "artist")) + ("album" . ,(dm:field track "album"))))) queue))))))) (define-api asteroid/stream/queue/add (track-id &optional (position "end")) () @@ -235,17 +224,7 @@ (defun get-track-by-id (track-id) "Get a track by its ID - handles type mismatches" - ;; Try direct query first - (let ((tracks (db:select "tracks" (db:query (:= "_id" track-id))))) - (if (> (length tracks) 0) - (first tracks) - ;; If not found, search manually (ID might be stored as list) - (let ((all-tracks (db:select "tracks" (db:query :all)))) - (find-if (lambda (track) - (let ((stored-id (gethash "_id" track))) - (or (equal stored-id track-id) - (and (listp stored-id) (equal (first stored-id) track-id))))) - all-tracks))))) + (dm:get-one "tracks" (db:query (:= '_id track-id)))) (defun get-mime-type-for-format (format) "Get MIME type for audio format" @@ -263,8 +242,8 @@ (track (get-track-by-id id))) (unless track (signal-not-found "track" id)) - (let* ((file-path (first (gethash "file-path" track))) - (format (first (gethash "format" track))) + (let* ((file-path (dm:field track "file-path")) + (format (dm:field track "format")) (file (probe-file file-path))) (unless file (error 'not-found-error @@ -276,8 +255,8 @@ (setf (radiance:header "Accept-Ranges") "bytes") (setf (radiance:header "Cache-Control") "public, max-age=3600") ;; Increment play count - (db:update "tracks" (db:query (:= '_id id)) - `(("play-count" ,(1+ (first (gethash "play-count" track)))))) + (setf (dm:field track "play-count") (1+ (dm:field track "play-count"))) + (data-model-save track) ;; Return file contents (alexandria:read-file-into-byte-vector file))))) @@ -333,8 +312,8 @@ (api-output `(("status" . "success") ("message" . "Playback started") ("track" . (("id" . ,id) - ("title" . ,(first (gethash "title" track))) - ("artist" . ,(first (gethash "artist" track))))) + ("title" . ,(dm:field track "title")) + ("artist" . ,(dm:field track "artist")))) ("player" . ,(get-player-status))))))) (define-api asteroid/player/pause () () @@ -524,7 +503,7 @@ "Admin dashboard" (require-authentication) (let ((track-count (handler-case - (length (db:select "tracks" (db:query :all))) + (length (dm:get "tracks" (db:query :all))) (error () 0)))) (clip:process-to-string (load-template "admin") diff --git a/conditions.lisp b/conditions.lisp index d010aa6..b228705 100644 --- a/conditions.lisp +++ b/conditions.lisp @@ -157,7 +157,7 @@ Usage: (with-db-error-handling \"select\" - (db:select 'tracks (db:query :all)))" + (dm:get 'tracks (db:query :all)))" `(handler-case (progn ,@body) (error (e) diff --git a/database.lisp b/database.lisp index d71903a..bbeb568 100644 --- a/database.lisp +++ b/database.lisp @@ -20,6 +20,7 @@ (db:create "playlists" '((name :text) (description :text) (created-date :integer) + (user-id :integer) (track-ids :text)))) (unless (db:collection-exists-p "USERS") diff --git a/playlist-management.lisp b/playlist-management.lisp index 1012fe6..5d16057 100644 --- a/playlist-management.lisp +++ b/playlist-management.lisp @@ -10,94 +10,72 @@ (unless (db:collection-exists-p "playlists") (error "Playlists collection does not exist in database")) - (let ((playlist-data `(("user-id" ,user-id) - ("name" ,name) - ("description" ,(or description "")) - ("track-ids" "") ; Empty string for text field - ("created-date" ,(local-time:timestamp-to-unix (local-time:now)))))) + (let ((playlist (dm:hull "playlists"))) + (setf (dm:field playlist "user-id") user-id) + (setf (dm:field playlist "name") name) + (setf (dm:field playlist "description") (or description "")) + (setf (dm:field playlist "track-ids") "") ; Empty string for text field + (setf (dm:field playlist "created-date") (local-time:timestamp-to-unix (local-time:now))) (format t "Creating playlist with user-id: ~a (type: ~a)~%" user-id (type-of user-id)) - (format t "Playlist data: ~a~%" playlist-data) - (db:insert "playlists" playlist-data) + (format t "Playlist data: ~a~%" (data-model-as-alist playlist)) + (dm:insert playlist) t)) (defun get-user-playlists (user-id) "Get all playlists for a user" (format t "Querying playlists with user-id: ~a (type: ~a)~%" user-id (type-of user-id)) - (let ((all-playlists (db:select "playlists" (db:query :all)))) + (let ((all-playlists (dm:get "playlists" (db:query :all)))) (format t "Total playlists in database: ~a~%" (length all-playlists)) (when (> (length all-playlists) 0) - (let ((first-playlist (first all-playlists))) + (let* ((first-playlist (first all-playlists)) + (first-playlist-user (dm:field first-playlist "user-id"))) (format t "First playlist user-id: ~a (type: ~a)~%" - (gethash "user-id" first-playlist) - (type-of (gethash "user-id" first-playlist))))) + first-playlist-user + (type-of first-playlist-user)))) ;; Filter manually since DB stores user-id as a list (2) instead of 2 (remove-if-not (lambda (playlist) - (let ((stored-user-id (gethash "user-id" playlist))) - (or (equal stored-user-id user-id) - (and (listp stored-user-id) - (equal (first stored-user-id) user-id))))) + (let ((stored-user-id (dm:field playlist "user-id"))) + (equal stored-user-id user-id))) all-playlists))) (defun get-playlist-by-id (playlist-id) "Get a specific playlist by ID" (format t "get-playlist-by-id called with: ~a (type: ~a)~%" playlist-id (type-of playlist-id)) - ;; Try direct query first - (let ((playlists (db:select "playlists" (db:query (:= "_id" playlist-id))))) - (if (> (length playlists) 0) - (progn - (format t "Found via direct query~%") - (first playlists)) - ;; If not found, search manually (ID might be stored as list) - (let ((all-playlists (db:select "playlists" (db:query :all)))) - (format t "Searching through ~a playlists manually~%" (length all-playlists)) - (find-if (lambda (playlist) - (let ((stored-id (gethash "_id" playlist))) - (format t "Checking playlist _id: ~a (type: ~a)~%" stored-id (type-of stored-id)) - (or (equal stored-id playlist-id) - (and (listp stored-id) (equal (first stored-id) playlist-id))))) - all-playlists))))) + (dm:get-one "playlists" (db:query (:= '_id playlist-id)))) (defun add-track-to-playlist (playlist-id track-id) "Add a track to a playlist" - (let ((playlist (get-playlist-by-id playlist-id))) - (when playlist - (let* ((current-track-ids-raw (gethash "track-ids" playlist)) - ;; Handle database storing as list - extract string - (current-track-ids (if (listp current-track-ids-raw) - (first current-track-ids-raw) - current-track-ids-raw)) - ;; Parse comma-separated string into list - (tracks-list (if (and current-track-ids - (stringp current-track-ids) - (not (string= current-track-ids ""))) - (mapcar #'parse-integer - (cl-ppcre:split "," current-track-ids)) - nil)) - (new-tracks (append tracks-list (list track-id))) - ;; Convert back to comma-separated string - (track-ids-str (format nil "~{~a~^,~}" new-tracks))) - (format t "Adding track ~a to playlist ~a~%" track-id playlist-id) - (format t "Current track-ids raw: ~a (type: ~a)~%" current-track-ids-raw (type-of current-track-ids-raw)) - (format t "Current track-ids: ~a~%" current-track-ids) - (format t "Tracks list: ~a~%" tracks-list) - (format t "New tracks: ~a~%" new-tracks) - (format t "Track IDs string: ~a~%" track-ids-str) - ;; Update using track-ids field (defined in schema) - (db:update "playlists" - (db:query (:= "_id" playlist-id)) - `(("track-ids" ,track-ids-str))) - (format t "Update complete~%") - t)))) + (db:with-transaction () + (let ((playlist (get-playlist-by-id playlist-id))) + (when playlist + (let* ((current-track-ids (dm:field playlist "track-ids")) + ;; Parse comma-separated string into list + (tracks-list (if (and current-track-ids + (stringp current-track-ids) + (not (string= current-track-ids ""))) + (mapcar #'parse-integer + (cl-ppcre:split "," current-track-ids)) + nil)) + (new-tracks (append tracks-list (list track-id))) + ;; Convert back to comma-separated string + (track-ids-str (format nil "~{~a~^,~}" new-tracks))) + (format t "Adding track ~a to playlist ~a~%" track-id playlist-id) + (format t "Current track-ids raw: ~a (type: ~a)~%" current-track-ids-raw (type-of current-track-ids-raw)) + (format t "Current track-ids: ~a~%" current-track-ids) + (format t "Tracks list: ~a~%" tracks-list) + (format t "New tracks: ~a~%" new-tracks) + (format t "Track IDs string: ~a~%" track-ids-str) + ;; Update using track-ids field (defined in schema) + (setf (dm:field playlist "track-ids") track-ids-str) + (data-model-save playlist) + (format t "Update complete~%") + t))))) (defun remove-track-from-playlist (playlist-id track-id) "Remove a track from a playlist" (let ((playlist (get-playlist-by-id playlist-id))) (when playlist - (let* ((current-track-ids-raw (gethash "track-ids" playlist)) - ;; Handle database storing as list - extract string - (current-track-ids (if (listp current-track-ids-raw) - (first current-track-ids-raw) - current-track-ids-raw)) + (let* ((current-track-ids (dm:field playlist "track-ids")) ;; Parse comma-separated string into list (tracks-list (if (and current-track-ids (stringp current-track-ids) @@ -108,28 +86,11 @@ (new-tracks (remove track-id tracks-list :test #'equal)) ;; Convert back to comma-separated string (track-ids-str (format nil "~{~a~^,~}" new-tracks))) - (db:update "playlists" - (db:query (:= "_id" playlist-id)) - `(("track-ids" ,track-ids-str))) + (setf (dm:field playlist "track-ids") track-ids-str) + (data-model-save playlist) t)))) (defun delete-playlist (playlist-id) "Delete a playlist" - (db:remove "playlists" (db:query (:= "_id" playlist-id))) + (dm:delete "playlists" (db:query (:= '_id playlist-id))) t) - -(defun ensure-playlists-collection () - "Ensure playlists collection exists in database" - (unless (db:collection-exists-p "playlists") - (format t "Creating playlists collection...~%") - (db:create "playlists")) - - ;; Debug: Print the actual structure - (format t "~%=== PLAYLISTS COLLECTION STRUCTURE ===~%") - (format t "Structure: ~a~%~%" (db:structure "playlists")) - - ;; Debug: Check existing playlists - (let ((playlists (db:select "playlists" (db:query :all)))) - (when playlists - (format t "Sample playlist fields: ~{~a~^, ~}~%~%" - (alexandria:hash-table-keys (first playlists)))))) diff --git a/static/js/player.js b/static/js/player.js index f0ae480..51783f2 100644 --- a/static/js/player.js +++ b/static/js/player.js @@ -130,8 +130,8 @@ function renderLibraryPage() { return `