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/stream-control.lisp b/stream-control.lisp index bca504f..8cbfd42 100644 --- a/stream-control.lisp +++ b/stream-control.lisp @@ -45,11 +45,8 @@ "Add all tracks from a playlist to the stream queue" (let ((playlist (get-playlist-by-id playlist-id))) (when playlist - (let* ((track-ids-raw (gethash "track-ids" playlist)) - (track-ids-str (if (listp track-ids-raw) - (first track-ids-raw) - track-ids-raw)) - (track-ids (if (and track-ids-str + (let* ((track-ids-str (dm:field playlist "track-ids")) + (track-ids (if (and track-ids-str (stringp track-ids-str) (not (string= track-ids-str ""))) (mapcar #'parse-integer @@ -65,10 +62,7 @@ "Get the file path for a track by ID" (let ((track (get-track-by-id track-id))) (when track - (let ((file-path (gethash "file-path" track))) - (if (listp file-path) - (first file-path) - file-path))))) + (dm:field track "file-path")))) (defun convert-to-docker-path (host-path) "Convert host file path to Docker container path" @@ -101,11 +95,10 @@ (asdf:system-source-directory :asteroid)))) (if (null *stream-queue*) ;; If queue is empty, generate from all tracks (fallback) - (let ((all-tracks (db:select "tracks" (db:query :all)))) + (let ((all-tracks (dm:get "tracks" (db:query :all)))) (generate-m3u-playlist - (mapcar (lambda (track) - (let ((id (gethash "_id" track))) - (if (listp id) (first id) id))) + (mapcar (lambda (track) + (dm:id track)) all-tracks) playlist-path)) ;; Generate from queue @@ -115,11 +108,8 @@ "Export a user playlist to an M3U file" (let ((playlist (get-playlist-by-id playlist-id))) (when playlist - (let* ((track-ids-raw (gethash "track-ids" playlist)) - (track-ids-str (if (listp track-ids-raw) - (first track-ids-raw) - track-ids-raw)) - (track-ids (if (and track-ids-str + (let* ((track-ids-str (dm:field playlist "track-ids")) + (track-ids (if (and track-ids-str (stringp track-ids-str) (not (string= track-ids-str ""))) (mapcar #'parse-integer @@ -128,7 +118,6 @@ (generate-m3u-playlist track-ids output-path))))) ;;; Stream History Management - (defun add-to-stream-history (track-id) "Add a track to the stream history" (push track-id *stream-history*) @@ -145,12 +134,11 @@ (defun build-smart-queue (genre &optional (count 20)) "Build a smart queue based on genre" - (let ((tracks (db:select "tracks" (db:query :all)))) + (let ((tracks (dm:get "tracks" (db:query :all)))) ;; For now, just add random tracks ;; TODO: Implement genre filtering when we have genre metadata (let ((track-ids (mapcar (lambda (track) - (let ((id (gethash "_id" track))) - (if (listp id) (first id) id))) + (dm:id track)) tracks))) (setf *stream-queue* (subseq (alexandria:shuffle track-ids) 0 @@ -160,18 +148,16 @@ (defun build-queue-from-artist (artist-name &optional (count 20)) "Build a queue from tracks by a specific artist" - (let ((tracks (db:select "tracks" (db:query :all)))) + (let ((tracks (dm:get "tracks" (db:query :all)))) (let ((matching-tracks (remove-if-not (lambda (track) - (let ((artist (gethash "artist" track))) + (let ((artist (dm:field track "artist"))) (when artist - (let ((artist-str (if (listp artist) (first artist) artist))) - (search artist-name artist-str :test #'char-equal))))) + (search artist-name artist :test #'char-equal)))) tracks))) (let ((track-ids (mapcar (lambda (track) - (let ((id (gethash "_id" track))) - (if (listp id) (first id) id))) + (dm:id track)) matching-tracks))) (setf *stream-queue* (subseq track-ids 0 (min count (length track-ids)))) (regenerate-stream-playlist) @@ -192,7 +178,7 @@ (let* ((m3u-path (merge-pathnames "stream-queue.m3u" (asdf:system-source-directory :asteroid))) (track-ids '()) - (all-tracks (db:select "tracks" (db:query :all)))) + (all-tracks (dm:get "tracks" (db:query :all)))) (when (probe-file m3u-path) (with-open-file (stream m3u-path :direction :input) @@ -206,14 +192,12 @@ ;; Find track by file path (let ((track (find-if (lambda (trk) - (let ((fp (gethash "file-path" trk))) - (let ((file-path (if (listp fp) (first fp) fp))) - (string= file-path host-path)))) + (let ((file-path (dm:field trk "file-path"))) + (string= file-path host-path))) all-tracks))) (when track - (let ((id (gethash "_id" track))) - (push (if (listp id) (first id) id) track-ids))))))))) - + (push (dm:id track) track-ids)))))))) + ;; Reverse to maintain order from file (setf track-ids (nreverse track-ids)) (setf *stream-queue* track-ids) diff --git a/stream-media.lisp b/stream-media.lisp index e076822..3b8b787 100644 --- a/stream-media.lisp +++ b/stream-media.lisp @@ -62,16 +62,17 @@ (defun track-exists-p (file-path) "Check if a track with the given file path already exists in the database" ;; Try direct query first - (let ((existing (db:select "tracks" (db:query (:= "file-path" file-path))))) + (let ((existing (dm:get "tracks" (db:query (:= "file-path" file-path))))) (if (> (length existing) 0) t ;; If not found, search manually (file-path might be stored as list) - (let ((all-tracks (db:select "tracks" (db:query :all)))) + (let ((all-tracks (dm:get "tracks" (db:query :all)))) (some (lambda (track) - (let ((stored-path (gethash "file-path" track))) + (let ((stored-path (dm:field track "file-path"))) (or (equal stored-path file-path) (and (listp stored-path) (equal (first stored-path) file-path))))) - all-tracks))))) + all-tracks) + )))) (defun insert-track-to-database (metadata) "Insert track metadata into database if it doesn't already exist" @@ -83,17 +84,17 @@ (let ((file-path (getf metadata :file-path))) (if (track-exists-p file-path) nil - (progn - (db:insert "tracks" - (list (list "title" (getf metadata :title)) - (list "artist" (getf metadata :artist)) - (list "album" (getf metadata :album)) - (list "duration" (getf metadata :duration)) - (list "file-path" file-path) - (list "format" (getf metadata :format)) - (list "bitrate" (getf metadata :bitrate)) - (list "added-date" (local-time:timestamp-to-unix (local-time:now))) - (list "play-count" 0))) + (let ((track (dm:hull "tracks"))) + (setf (dm:field track "title") (getf metadata :title)) + (setf (dm:field track "artist") (getf metadata :artist)) + (setf (dm:field track "album") (getf metadata :album)) + (setf (dm:field track "duration") (getf metadata :duration)) + (setf (dm:field track "file-path") file-path) + (setf (dm:field track "format") (getf metadata :format)) + (setf (dm:field track "bitrate") (getf metadata :bitrate)) + (setf (dm:field track "added-date") (local-time:timestamp-to-unix (local-time:now))) + (setf (dm:field track "play-count") 0) + (dm:insert track) t)))) (defun scan-music-library (&optional (directory *music-library-path*))