Compare commits
2 Commits
a8b681621d
...
1186214770
| Author | SHA1 | Date |
|---|---|---|
|
|
1186214770 | |
|
|
00bcf46c27 |
|
|
@ -133,17 +133,10 @@
|
|||
(playlists (get-user-playlists user-id)))
|
||||
(api-output `(("status" . "success")
|
||||
("playlists" . ,(mapcar (lambda (playlist)
|
||||
(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)))
|
||||
(let ((track-count (get-playlist-track-count (dm:id playlist))))
|
||||
`(("id" . ,(dm:id playlist))
|
||||
("name" . ,(dm:field playlist "name"))
|
||||
("description" . ,(dm:field playlist "description"))
|
||||
("description" . ,(or (dm:field playlist "description") ""))
|
||||
("track-count" . ,track-count)
|
||||
("created-date" . ,(dm:field playlist "created-date")))))
|
||||
playlists)))))))
|
||||
|
|
@ -177,7 +170,7 @@
|
|||
(let* ((id (parse-integer playlist-id :junk-allowed t))
|
||||
(playlist (get-playlist-by-id id)))
|
||||
(if playlist
|
||||
(let* ((track-ids (dm:field playlist "tracks"))
|
||||
(let* ((track-ids (get-playlist-tracks id))
|
||||
(tracks (mapcar (lambda (track-id)
|
||||
(dm:get-one "tracks" (db:query (:= '_id track-id))))
|
||||
track-ids))
|
||||
|
|
@ -185,6 +178,8 @@
|
|||
(api-output `(("status" . "success")
|
||||
("playlist" . (("id" . ,id)
|
||||
("name" . ,(dm:field playlist "name"))
|
||||
("description" . ,(or (dm:field playlist "description") ""))
|
||||
("track-count" . ,(length valid-tracks))
|
||||
("tracks" . ,(mapcar (lambda (track)
|
||||
`(("id" . ,(dm:id track))
|
||||
("title" . ,(dm:field track "title"))
|
||||
|
|
@ -195,6 +190,31 @@
|
|||
("message" . "Playlist not found"))
|
||||
:status 404)))))
|
||||
|
||||
(define-api asteroid/playlists/delete (playlist-id) ()
|
||||
"Delete a playlist"
|
||||
(require-authentication)
|
||||
(with-error-handling
|
||||
(let* ((id (parse-integer playlist-id :junk-allowed t))
|
||||
(user-id (get-current-user-id)))
|
||||
(if (delete-playlist id user-id)
|
||||
(api-output `(("status" . "success")
|
||||
("message" . "Playlist deleted")))
|
||||
(api-output `(("status" . "error")
|
||||
("message" . "Could not delete playlist (not found or not owned by you)"))
|
||||
:status 403)))))
|
||||
|
||||
(define-api asteroid/playlists/remove-track (playlist-id track-id) ()
|
||||
"Remove a track from a playlist"
|
||||
(require-authentication)
|
||||
(with-error-handling
|
||||
(let ((pl-id (parse-integer playlist-id :junk-allowed t))
|
||||
(tr-id (parse-integer track-id :junk-allowed t)))
|
||||
(if (remove-track-from-playlist pl-id tr-id)
|
||||
(api-output `(("status" . "success")
|
||||
("message" . "Track removed from playlist")))
|
||||
(api-output `(("status" . "error")
|
||||
("message" . "Could not remove track")))))))
|
||||
|
||||
;; Recently played tracks API endpoint
|
||||
(define-api asteroid/recently-played () ()
|
||||
"Get the last 3 played tracks with AllMusic links"
|
||||
|
|
@ -558,28 +578,26 @@
|
|||
|
||||
(define-page stream-track #@"/tracks/(.*)/stream" (:uri-groups (track-id))
|
||||
"Stream audio file by track ID"
|
||||
(with-error-handling
|
||||
(let* ((id (parse-integer track-id))
|
||||
(track (get-track-by-id id)))
|
||||
(unless track
|
||||
(signal-not-found "track" id))
|
||||
(let* ((file-path (dm:field track "file-path"))
|
||||
(format (dm:field track "format"))
|
||||
(file (probe-file file-path)))
|
||||
(unless file
|
||||
(error 'not-found-error
|
||||
:message "Audio file not found on disk"
|
||||
:resource-type "file"
|
||||
:resource-id file-path))
|
||||
;; Set appropriate headers for audio streaming
|
||||
(setf (radiance:header "Content-Type") (get-mime-type-for-format format))
|
||||
(setf (radiance:header "Accept-Ranges") "bytes")
|
||||
(setf (radiance:header "Cache-Control") "public, max-age=3600")
|
||||
;; Increment play count
|
||||
(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)))))
|
||||
(let* ((id (parse-integer track-id :junk-allowed t))
|
||||
(track (when id (get-track-by-id id))))
|
||||
(if (not track)
|
||||
(progn
|
||||
(setf (radiance:header "Content-Type") "text/plain")
|
||||
"Track not found")
|
||||
(let* ((file-path (dm:field track "file-path"))
|
||||
(format (dm:field track "format"))
|
||||
(file (probe-file file-path)))
|
||||
(if (not file)
|
||||
(progn
|
||||
(setf (radiance:header "Content-Type") "text/plain")
|
||||
"Audio file not found")
|
||||
(progn
|
||||
;; Set appropriate headers for audio streaming
|
||||
(setf (radiance:header "Content-Type") (get-mime-type-for-format format))
|
||||
(setf (radiance:header "Accept-Ranges") "bytes")
|
||||
(setf (radiance:header "Cache-Control") "public, max-age=3600")
|
||||
;; Return file contents
|
||||
(alexandria:read-file-into-byte-vector file)))))))
|
||||
|
||||
;; Player state management
|
||||
(defvar *current-track* nil "Currently playing track")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,19 @@
|
|||
(in-package :asteroid)
|
||||
|
||||
;; Database connection parameters for direct postmodern queries
|
||||
(defun get-db-connection-params ()
|
||||
"Get database connection parameters for postmodern"
|
||||
(list (or (uiop:getenv "ASTEROID_DB_NAME") "asteroid")
|
||||
(or (uiop:getenv "ASTEROID_DB_USER") "asteroid")
|
||||
(or (uiop:getenv "ASTEROID_DB_PASSWORD") "asteroid_db_2025")
|
||||
(or (uiop:getenv "ASTEROID_DB_HOST") "localhost")
|
||||
:port (parse-integer (or (uiop:getenv "ASTEROID_DB_PORT") "5432"))))
|
||||
|
||||
(defmacro with-db (&body body)
|
||||
"Execute body with database connection"
|
||||
`(postmodern:with-connection (get-db-connection-params)
|
||||
,@body))
|
||||
|
||||
;; Database initialization - must be in db:connected trigger because
|
||||
;; the system could load before the database is ready.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,7 @@
|
|||
|
||||
(in-package #:asteroid)
|
||||
|
||||
;;; Use postmodern for direct SQL queries
|
||||
;;; Connection params from environment or defaults matching config/radiance-postgres.lisp
|
||||
(defun get-db-connection-params ()
|
||||
"Get database connection parameters"
|
||||
(list (or (uiop:getenv "ASTEROID_DB_NAME") "asteroid")
|
||||
(or (uiop:getenv "ASTEROID_DB_USER") "asteroid")
|
||||
(or (uiop:getenv "ASTEROID_DB_PASSWORD") "asteroid_db_2025")
|
||||
"localhost"
|
||||
:port 5432))
|
||||
|
||||
(defmacro with-db (&body body)
|
||||
"Execute body with database connection"
|
||||
`(postmodern:with-connection (get-db-connection-params)
|
||||
,@body))
|
||||
;;; Note: get-db-connection-params and with-db are defined in database.lisp
|
||||
|
||||
;;; Configuration
|
||||
(defvar *stats-polling-interval* 60
|
||||
|
|
|
|||
|
|
@ -93,15 +93,15 @@
|
|||
|
||||
;; Restore user quality preference
|
||||
(let ((selector (ps:chain document (get-element-by-id "live-stream-quality")))
|
||||
(stream-quality (ps:chain (ps:@ local-storage (get-item "stream-quality")) "aac")))
|
||||
(when (and selector (not (== (ps:@ selector value) stream-quality)))
|
||||
(stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac")))
|
||||
(when (and selector (not (= (ps:@ selector value) stream-quality)))
|
||||
(setf (ps:@ selector value) stream-quality)
|
||||
(ps:chain selector (dispatch-event (new "Event" "change"))))))))
|
||||
|
||||
;; Frame redirection logic
|
||||
(defun redirect-when-frame ()
|
||||
(let ((path (ps:@ window location pathname))
|
||||
(is-frameset-page (not (== (ps:@ window parent) (ps:@ window self))))
|
||||
(let* ((path (ps:@ window location pathname))
|
||||
(is-frameset-page (not (= (ps:@ window parent) (ps:@ window self))))
|
||||
(is-content-frame (ps:chain path (includes "player-content"))))
|
||||
|
||||
(when (and is-frameset-page (not is-content-frame))
|
||||
|
|
@ -133,13 +133,12 @@
|
|||
(add-event-listener "input" update-volume))
|
||||
|
||||
;; Audio player events
|
||||
(when *audio-player*
|
||||
(ps:chain *audio-player*
|
||||
(add-event-listener "loadedmetadata" update-time-display)
|
||||
(add-event-listener "timeupdate" update-time-display)
|
||||
(add-event-listener "ended" handle-track-end)
|
||||
(add-event-listener "play" (lambda () (update-play-button "⏸️ Pause")))
|
||||
(add-event-listener "pause" (lambda () (update-play-button "▶️ Play")))))
|
||||
(when (and *audio-player* (ps:chain *audio-player* add-event-listener))
|
||||
(ps:chain *audio-player* (add-event-listener "loadedmetadata" update-time-display))
|
||||
(ps:chain *audio-player* (add-event-listener "timeupdate" update-time-display))
|
||||
(ps:chain *audio-player* (add-event-listener "ended" handle-track-end))
|
||||
(ps:chain *audio-player* (add-event-listener "play" (lambda () (update-play-button "⏸️ Pause"))))
|
||||
(ps:chain *audio-player* (add-event-listener "pause" (lambda () (update-play-button "▶️ Play")))))
|
||||
|
||||
;; Playlist controls
|
||||
(ps:chain (ps:chain document (get-element-by-id "create-playlist"))
|
||||
|
|
@ -162,17 +161,17 @@
|
|||
(then (lambda (result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (== (ps:@ data status) "success")
|
||||
(if (= (ps:@ data status) "success")
|
||||
(progn
|
||||
(setf *tracks* (or (ps:@ data tracks) (array)))
|
||||
(display-tracks *tracks*))
|
||||
(progn
|
||||
(ps:chain console (error "Error loading tracks:" (ps:@ data error)))
|
||||
(setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-html)
|
||||
(setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-h-t-m-l)
|
||||
"<div class=\"error\">Error loading tracks</div>"))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error loading tracks:" error))
|
||||
(setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-html)
|
||||
(setf (ps:chain (ps:chain document (get-element-by-id "track-list")) inner-h-t-m-l)
|
||||
"<div class=\"error\">Error loading tracks</div>")))))
|
||||
|
||||
;; Display tracks in library
|
||||
|
|
@ -186,14 +185,14 @@
|
|||
(let ((container (ps:chain document (get-element-by-id "track-list")))
|
||||
(pagination-controls (ps:chain document (get-element-by-id "library-pagination-controls"))))
|
||||
|
||||
(if (== (ps:@ *filtered-library-tracks* length) 0)
|
||||
(if (= (ps:@ *filtered-library-tracks* length) 0)
|
||||
(progn
|
||||
(setf (ps:@ container inner-html) "<div class=\"no-tracks\">No tracks found</div>")
|
||||
(setf (ps:@ container inner-h-t-m-l) "<div class=\"no-tracks\">No tracks found</div>")
|
||||
(setf (ps:@ pagination-controls style display) "none")
|
||||
(return)))
|
||||
|
||||
;; Calculate pagination
|
||||
(let ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*)))
|
||||
(let* ((total-pages (ceiling (/ (ps:@ *filtered-library-tracks* length) *library-tracks-per-page*)))
|
||||
(start-index (* (- *library-current-page* 1) *library-tracks-per-page*))
|
||||
(end-index (+ start-index *library-tracks-per-page*))
|
||||
(tracks-to-show (ps:chain *filtered-library-tracks* (slice start-index end-index))))
|
||||
|
|
@ -203,20 +202,21 @@
|
|||
(map (lambda (track page-index)
|
||||
;; Find the actual index in the full tracks array
|
||||
(let ((actual-index (ps:chain *tracks*
|
||||
(find-index (lambda (trk) (== (ps:@ trk id) (ps:@ track id)))))))
|
||||
(find-index (lambda (trk) (= (ps:@ trk id) (ps:@ track id)))))))
|
||||
(+ "<div class=\"track-item\" data-track-id=\"" (ps:@ track id) "\" data-index=\"" actual-index "\">"
|
||||
"<div class=\"track-info\">"
|
||||
"<div class=\"track-title\">" (or (ps:@ track title) "Unknown Title") "</div>"
|
||||
"<div class=\"track-meta\">" (or (ps:@ track artist) "Unknown Artist") " • " (or (ps:@ track album) "Unknown Album") "</div>"
|
||||
"</div>"
|
||||
"<div class=\"track-actions\">"
|
||||
"<button onclick=\"playTrack(" actual-index ")\" class=\"btn btn-sm btn-success\">▶️</button>"
|
||||
"<button onclick=\"addToQueue(" actual-index ")\" class=\"btn btn-sm btn-info\">➕</button>"
|
||||
"<button onclick=\"playTrack(" actual-index ")\" class=\"btn btn-sm btn-success\" title=\"Play\">▶️</button>"
|
||||
"<button onclick=\"addToQueue(" actual-index ")\" class=\"btn btn-sm btn-info\" title=\"Add to queue\">➕</button>"
|
||||
"<button onclick=\"showAddToPlaylistMenu(" (ps:@ track id) ", event)\" class=\"btn btn-sm btn-secondary\" title=\"Add to playlist\">📋</button>"
|
||||
"</div>"
|
||||
"</div>"))))
|
||||
(join ""))))
|
||||
|
||||
(setf (ps:@ container inner-html) tracks-html)
|
||||
(setf (ps:@ container inner-h-t-m-l) tracks-html)
|
||||
|
||||
;; Update pagination controls
|
||||
(setf (ps:chain (ps:chain document (get-element-by-id "library-page-info")) text-content)
|
||||
|
|
@ -249,7 +249,7 @@
|
|||
|
||||
(defun change-library-tracks-per-page ()
|
||||
(setf *library-tracks-per-page*
|
||||
(parseInt (ps:chain (ps:chain document (get-element-by-id "library-tracks-per-page")) value)))
|
||||
(parse-int (ps:chain (ps:chain document (get-element-by-id "library-tracks-per-page")) value)))
|
||||
(setf *library-current-page* 1)
|
||||
(render-library-page))
|
||||
|
||||
|
|
@ -312,13 +312,13 @@
|
|||
;; Play from queue
|
||||
(let ((next-track (ps:chain *play-queue* (shift))))
|
||||
(play-track (ps:chain *tracks*
|
||||
(find-index (lambda (trk) (== (ps:@ trk id) (ps:@ next-track id))))))
|
||||
(find-index (lambda (trk) (= (ps:@ trk id) (ps:@ next-track id))))))
|
||||
(update-queue-display))
|
||||
;; Play next track in library
|
||||
(let ((next-index (if *is-shuffled*
|
||||
(floor (* (random) (ps:@ *tracks* length)))
|
||||
(mod (+ *current-track-index* 1) (ps:@ *tracks* length)))))
|
||||
(play-track next-index)))))
|
||||
(play-track next-index))))
|
||||
|
||||
;; Handle track end
|
||||
(defun handle-track-end ()
|
||||
|
|
@ -344,7 +344,7 @@
|
|||
|
||||
;; Update volume
|
||||
(defun update-volume ()
|
||||
(let ((volume (/ (parseInt (ps:chain (ps:chain document (get-element-by-id "volume-slider")) value)) 100)))
|
||||
(let ((volume (/ (parse-int (ps:chain (ps:chain document (get-element-by-id "volume-slider")) value)) 100)))
|
||||
(when *audio-player*
|
||||
(setf (ps:@ *audio-player* volume) volume))))
|
||||
|
||||
|
|
@ -386,8 +386,8 @@
|
|||
;; Update queue display
|
||||
(defun update-queue-display ()
|
||||
(let ((container (ps:chain document (get-element-by-id "play-queue"))))
|
||||
(if (== (ps:@ *play-queue* length) 0)
|
||||
(setf (ps:@ container inner-html) "<div class=\"empty-queue\">Queue is empty</div>")
|
||||
(if (= (ps:@ *play-queue* length) 0)
|
||||
(setf (ps:@ container inner-h-t-m-l) "<div class=\"empty-queue\">Queue is empty</div>")
|
||||
(let ((queue-html (ps:chain *play-queue*
|
||||
(map (lambda (track index)
|
||||
(+ "<div class=\"queue-item\">"
|
||||
|
|
@ -398,7 +398,7 @@
|
|||
"<button onclick=\"removeFromQueue(" index ")\" class=\"btn btn-sm btn-danger\">✖️</button>"
|
||||
"</div>")))
|
||||
(join ""))))
|
||||
(setf (ps:@ container inner-html) queue-html))))
|
||||
(setf (ps:@ container inner-h-t-m-l) queue-html)))))
|
||||
|
||||
;; Remove track from queue
|
||||
(defun remove-from-queue (index)
|
||||
|
|
@ -410,28 +410,113 @@
|
|||
(setf *play-queue* (array))
|
||||
(update-queue-display))
|
||||
|
||||
;; Store playlists for the add-to-playlist menu
|
||||
(defvar *user-playlists* (array))
|
||||
|
||||
;; Show add to playlist dropdown menu
|
||||
(defun show-add-to-playlist-menu (track-id event)
|
||||
(ps:chain event (stop-propagation))
|
||||
;; Remove any existing menu
|
||||
(let ((existing-menu (ps:chain document (get-element-by-id "playlist-dropdown-menu"))))
|
||||
(when existing-menu
|
||||
(ps:chain existing-menu (remove))))
|
||||
|
||||
;; Fetch playlists and show menu
|
||||
(ps:chain (fetch "/api/asteroid/playlists")
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(let* ((data (or (ps:@ result data) result))
|
||||
(playlists (or (ps:@ data playlists) (array)))
|
||||
(menu (ps:chain document (create-element "div"))))
|
||||
(setf *user-playlists* playlists)
|
||||
(setf (ps:@ menu id) "playlist-dropdown-menu")
|
||||
(setf (ps:@ menu class-name) "playlist-dropdown-menu")
|
||||
(setf (ps:@ menu style position) "fixed")
|
||||
(setf (ps:@ menu style left) (+ (ps:@ event client-x) "px"))
|
||||
(setf (ps:@ menu style top) (+ (ps:@ event client-y) "px"))
|
||||
(setf (ps:@ menu style z-index) "1000")
|
||||
(setf (ps:@ menu style background) "#1a1a2e")
|
||||
(setf (ps:@ menu style border) "1px solid #00ff00")
|
||||
(setf (ps:@ menu style border-radius) "4px")
|
||||
(setf (ps:@ menu style padding) "5px 0")
|
||||
(setf (ps:@ menu style min-width) "150px")
|
||||
|
||||
(if (= (ps:@ playlists length) 0)
|
||||
(setf (ps:@ menu inner-h-t-m-l)
|
||||
"<div style=\"padding: 8px 12px; color: #888;\">No playlists yet</div>")
|
||||
(setf (ps:@ menu inner-h-t-m-l)
|
||||
(ps:chain playlists
|
||||
(map (lambda (playlist)
|
||||
(+ "<div class=\"playlist-menu-item\" onclick=\"addTrackToPlaylist("
|
||||
(ps:@ playlist id) ", " track-id
|
||||
")\" style=\"padding: 8px 12px; cursor: pointer; color: #00ff00;\" "
|
||||
"onmouseover=\"this.style.background='#2a2a4e'\" "
|
||||
"onmouseout=\"this.style.background='transparent'\">"
|
||||
(ps:@ playlist name) " (" (ps:@ playlist "track-count") ")"
|
||||
"</div>")))
|
||||
(join ""))))
|
||||
|
||||
(ps:chain document body (append-child menu))
|
||||
|
||||
;; Close menu when clicking elsewhere
|
||||
(let ((close-handler (lambda (e)
|
||||
(when (not (ps:chain menu (contains (ps:@ e target))))
|
||||
(ps:chain menu (remove))
|
||||
(ps:chain document (remove-event-listener "click" close-handler))))))
|
||||
(set-timeout (lambda ()
|
||||
(ps:chain document (add-event-listener "click" close-handler)))
|
||||
100)))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error loading playlists for menu:" error))))))
|
||||
|
||||
;; Add track to a specific playlist
|
||||
(defun add-track-to-playlist (playlist-id track-id)
|
||||
;; Close the menu
|
||||
(let ((menu (ps:chain document (get-element-by-id "playlist-dropdown-menu"))))
|
||||
(when menu (ps:chain menu (remove))))
|
||||
|
||||
(let ((form-data (new -Form-data)))
|
||||
(ps:chain form-data (append "playlist-id" playlist-id))
|
||||
(ps:chain form-data (append "track-id" track-id))
|
||||
(ps:chain (fetch "/api/asteroid/playlists/add-track"
|
||||
(ps:create :method "POST" :body form-data))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (= (ps:@ data status) "success")
|
||||
(progn
|
||||
;; Find playlist name for feedback
|
||||
(let ((playlist (ps:chain *user-playlists*
|
||||
(find (lambda (p) (= (ps:@ p id) playlist-id))))))
|
||||
(alert (+ "Track added to \"" (if playlist (ps:@ playlist name) "playlist") "\"")))
|
||||
(load-playlists))
|
||||
(alert (+ "Error: " (ps:@ data message)))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error adding track to playlist:" error))
|
||||
(alert "Error adding track to playlist"))))))
|
||||
|
||||
;; Create playlist
|
||||
(defun create-playlist ()
|
||||
(let ((name (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value (trim))))
|
||||
(when (not (== name ""))
|
||||
(let ((form-data (new "FormData")))
|
||||
(when (not (= name ""))
|
||||
(let ((form-data (new -Form-data)))
|
||||
(ps:chain form-data (append "name" name))
|
||||
(ps:chain form-data (append "description" ""))
|
||||
|
||||
(ps:chain (fetch "/api/asteroid/playlists/create"
|
||||
(ps:create :method "POST" :body form-data))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (response)
|
||||
(ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (== (ps:@ data status) "success")
|
||||
(if (= (ps:@ data status) "success")
|
||||
(progn
|
||||
(alert (+ "Playlist \"" name "\" created successfully!"))
|
||||
(setf (ps:chain (ps:chain document (get-element-by-id "new-playlist-name")) value) "")
|
||||
|
||||
;; Wait a moment then reload playlists
|
||||
(ps:chain (new "Promise" (lambda (resolve) (setTimeout resolve 500)))
|
||||
(then (lambda () (load-playlists)))))
|
||||
(set-timeout load-playlists 500))
|
||||
(alert (+ "Error creating playlist: " (ps:@ data message)))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error creating playlist:" error))
|
||||
|
|
@ -453,54 +538,55 @@
|
|||
(then (lambda (create-result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((create-data (or (ps:@ create-result data) create-result)))
|
||||
(if (== (ps:@ create-data status) "success")
|
||||
(if (= (ps:@ create-data status) "success")
|
||||
(progn
|
||||
;; Wait a moment for database to update
|
||||
(ps:chain (new "Promise" (lambda (resolve) (setTimeout resolve 500)))
|
||||
(then (lambda ()
|
||||
;; Get the new playlist ID by fetching playlists
|
||||
(ps:chain (fetch "/api/asteroid/playlists")
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (playlists-result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((playlist-result-data (or (ps:@ playlists-result data) playlists-result)))
|
||||
(if (and (== (ps:@ playlist-result-data status) "success")
|
||||
(> (ps:@ playlist-result-data playlists length) 0))
|
||||
(progn
|
||||
;; Find the playlist with matching name (most recent)
|
||||
(let ((new-playlist (or (ps:chain (ps:@ playlist-result-data playlists)
|
||||
(find (lambda (p) (== (ps:@ p name) name))))
|
||||
(aref (ps:@ playlist-result-data playlists)
|
||||
(- (ps:@ playlist-result-data playlists length) 1)))))
|
||||
|
||||
;; Add all tracks from queue to playlist
|
||||
(let ((added-count 0))
|
||||
(ps:chain *play-queue*
|
||||
(for-each (lambda (track)
|
||||
(let ((track-id (ps:@ track id)))
|
||||
(when track-id
|
||||
(let ((add-form-data (new "FormData")))
|
||||
(ps:chain add-form-data (append "playlist-id" (ps:@ new-playlist id)))
|
||||
(ps:chain add-form-data (append "track-id" track-id))
|
||||
|
||||
(ps:chain (fetch "/api/asteroid/playlists/add-track"
|
||||
(ps:create :method "POST" :body add-form-data))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (add-result)
|
||||
(when (== (ps:@ add-result data status) "success")
|
||||
(setf added-count (+ added-count 1)))))
|
||||
(catch (lambda (err)
|
||||
(ps:chain console (log "Error adding track:" err)))))))))))
|
||||
|
||||
(alert (+ "Playlist \"" name "\" created with " added-count " tracks!"))
|
||||
(load-playlists))))
|
||||
(progn
|
||||
(alert (+ "Playlist created but could not add tracks. Error: "
|
||||
(or (ps:@ playlist-result-data message) "Unknown")))
|
||||
(load-playlists))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error fetching playlists:" error))
|
||||
(alert "Playlist created but could not add tracks"))))))))
|
||||
;; Wait a moment for database to update, then fetch playlists
|
||||
(set-timeout
|
||||
(lambda ()
|
||||
;; Get the new playlist ID by fetching playlists
|
||||
(ps:chain (fetch "/api/asteroid/playlists")
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (playlists-result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((playlist-result-data (or (ps:@ playlists-result data) playlists-result)))
|
||||
(if (and (= (ps:@ playlist-result-data status) "success")
|
||||
(> (ps:@ playlist-result-data playlists length) 0))
|
||||
(progn
|
||||
;; Find the playlist with matching name (most recent)
|
||||
(let ((new-playlist (or (ps:chain (ps:@ playlist-result-data playlists)
|
||||
(find (lambda (p) (= (ps:@ p name) name))))
|
||||
(aref (ps:@ playlist-result-data playlists)
|
||||
(- (ps:@ playlist-result-data playlists length) 1)))))
|
||||
|
||||
;; Add all tracks from queue to playlist
|
||||
(let ((added-count 0))
|
||||
(ps:chain *play-queue*
|
||||
(for-each (lambda (track)
|
||||
(let ((track-id (ps:@ track id)))
|
||||
(when track-id
|
||||
(let ((add-form-data (new -Form-data)))
|
||||
(ps:chain add-form-data (append "playlist-id" (ps:@ new-playlist id)))
|
||||
(ps:chain add-form-data (append "track-id" track-id))
|
||||
|
||||
(ps:chain (fetch "/api/asteroid/playlists/add-track"
|
||||
(ps:create :method "POST" :body add-form-data))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (add-result)
|
||||
(when (= (ps:@ add-result data status) "success")
|
||||
(setf added-count (+ added-count 1)))))
|
||||
(catch (lambda (err)
|
||||
(ps:chain console (log "Error adding track:" err)))))))))))
|
||||
|
||||
(alert (+ "Playlist \"" name "\" created with " added-count " tracks!"))
|
||||
(load-playlists))))
|
||||
(progn
|
||||
(alert (+ "Playlist created but could not add tracks. Error: "
|
||||
(or (ps:@ playlist-result-data message) "Unknown")))
|
||||
(load-playlists))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error fetching playlists:" error))
|
||||
(alert "Playlist created but could not add tracks")))))
|
||||
500))
|
||||
(alert (+ "Error creating playlist: " (ps:@ create-data message)))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error saving queue as playlist:" error))
|
||||
|
|
@ -510,16 +596,21 @@
|
|||
;; Load playlists from API
|
||||
(defun load-playlists ()
|
||||
(ps:chain
|
||||
(ps:chain (fetch "/api/asteroid/playlists"))
|
||||
(fetch "/api/asteroid/playlists")
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(ps:chain console (log "Playlists API result:" result))
|
||||
(let ((playlists (cond
|
||||
((and (ps:@ result data) (== (ps:@ result data status) "success"))
|
||||
((and (ps:@ result data) (= (ps:@ result data status) "success"))
|
||||
(ps:chain console (log "Found playlists in result.data.playlists"))
|
||||
(or (ps:@ result data playlists) (array)))
|
||||
((== (ps:@ result status) "success")
|
||||
((= (ps:@ result status) "success")
|
||||
(ps:chain console (log "Found playlists in result.playlists"))
|
||||
(or (ps:@ result playlists) (array)))
|
||||
(t
|
||||
(ps:chain console (log "No playlists found in response"))
|
||||
(array)))))
|
||||
(ps:chain console (log "Playlists to display:" playlists))
|
||||
(display-playlists playlists))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error loading playlists:" error))
|
||||
|
|
@ -529,22 +620,69 @@
|
|||
(defun display-playlists (playlists)
|
||||
(let ((container (ps:chain document (get-element-by-id "playlists-container"))))
|
||||
|
||||
(if (or (not playlists) (== (ps:@ playlists length) 0))
|
||||
(setf (ps:@ container inner-html) "<div class=\"no-playlists\">No playlists created yet.</div>")
|
||||
(if (or (not playlists) (= (ps:@ playlists length) 0))
|
||||
(setf (ps:@ container inner-h-t-m-l) "<div class=\"no-playlists\">No playlists created yet.</div>")
|
||||
(let ((playlists-html (ps:chain playlists
|
||||
(map (lambda (playlist)
|
||||
(+ "<div class=\"playlist-item\">"
|
||||
(+ "<div class=\"playlist-item\" data-playlist-id=\"" (ps:@ playlist id) "\">"
|
||||
"<div class=\"playlist-info\">"
|
||||
"<div class=\"playlist-name\">" (ps:@ playlist name) "</div>"
|
||||
"<div class=\"playlist-meta\">" (ps:@ playlist "track-count") " tracks</div>"
|
||||
"</div>"
|
||||
"<div class=\"playlist-actions\">"
|
||||
"<button onclick=\"loadPlaylist(" (ps:@ playlist id) ")\" class=\"btn btn-sm btn-info\">📂 Load</button>"
|
||||
"<button onclick=\"viewPlaylist(" (ps:@ playlist id) ")\" class=\"btn btn-sm btn-secondary\" title=\"View tracks\">👁️</button>"
|
||||
"<button onclick=\"loadPlaylist(" (ps:@ playlist id) ")\" class=\"btn btn-sm btn-info\" title=\"Load to queue\">📂</button>"
|
||||
"<button onclick=\"deletePlaylist(" (ps:@ playlist id) ", '" (ps:chain (ps:@ playlist name) (replace (ps:regex "/'/g") "\\\\'")) "')\" class=\"btn btn-sm btn-danger\" title=\"Delete playlist\">🗑️</button>"
|
||||
"</div>"
|
||||
"</div>"))
|
||||
(join "")))))
|
||||
"</div>")))
|
||||
(join ""))))
|
||||
|
||||
(setf (ps:@ container inner-html) playlists-html)))))
|
||||
(setf (ps:@ container inner-h-t-m-l) playlists-html)))))
|
||||
|
||||
;; Delete playlist
|
||||
(defun delete-playlist (playlist-id playlist-name)
|
||||
(when (confirm (+ "Are you sure you want to delete playlist \"" playlist-name "\"?"))
|
||||
(let ((form-data (new -Form-data)))
|
||||
(ps:chain form-data (append "playlist-id" playlist-id))
|
||||
(ps:chain (fetch "/api/asteroid/playlists/delete"
|
||||
(ps:create :method "POST" :body form-data))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (= (ps:@ data status) "success")
|
||||
(progn
|
||||
(alert (+ "Playlist \"" playlist-name "\" deleted"))
|
||||
(load-playlists))
|
||||
(alert (+ "Error deleting playlist: " (ps:@ data message)))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error deleting playlist:" error))
|
||||
(alert "Error deleting playlist")))))))
|
||||
|
||||
;; View playlist contents
|
||||
(defun view-playlist (playlist-id)
|
||||
(ps:chain
|
||||
(fetch (+ "/api/asteroid/playlists/get?playlist-id=" playlist-id))
|
||||
(then (lambda (response) (ps:chain response (json))))
|
||||
(then (lambda (result)
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (and (= (ps:@ data status) "success") (ps:@ data playlist))
|
||||
(let* ((playlist (ps:@ data playlist))
|
||||
(tracks (or (ps:@ playlist tracks) (array)))
|
||||
(track-list (if (> (ps:@ tracks length) 0)
|
||||
(ps:chain tracks
|
||||
(map (lambda (track index)
|
||||
(+ (+ index 1) ". "
|
||||
(or (ps:@ track artist) "Unknown") " - "
|
||||
(or (ps:@ track title) "Unknown"))))
|
||||
(join "\\n"))
|
||||
"No tracks in playlist")))
|
||||
(alert (+ "Playlist: " (ps:@ playlist name) "\\n"
|
||||
"Tracks: " (ps:@ playlist "track-count") "\\n\\n"
|
||||
track-list)))
|
||||
(alert "Could not load playlist")))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error viewing playlist:" error))
|
||||
(alert "Error viewing playlist")))))
|
||||
|
||||
;; Load playlist into queue
|
||||
(defun load-playlist (playlist-id)
|
||||
|
|
@ -554,35 +692,38 @@
|
|||
(then (lambda (result)
|
||||
;; Handle RADIANCE API wrapper format
|
||||
(let ((data (or (ps:@ result data) result)))
|
||||
(if (and (== (ps:@ data status) "success") (ps:@ data playlist))
|
||||
(if (and (= (ps:@ data status) "success") (ps:@ data playlist))
|
||||
(let ((playlist (ps:@ data playlist)))
|
||||
|
||||
;; Clear current queue
|
||||
(setf *play-queue* (array))
|
||||
|
||||
;; Add all playlist tracks to queue
|
||||
(when (and (ps:@ playlist tracks) (> (ps:@ playlist tracks length) 0))
|
||||
(ps:chain (ps:@ playlist tracks)
|
||||
(for-each (lambda (track)
|
||||
;; Find the full track object from our tracks array
|
||||
(let ((full-track (ps:chain *tracks*
|
||||
(find (lambda (trk) (== (ps:@ trk id) (ps:@ track id)))))))
|
||||
(when full-track
|
||||
(setf (aref *play-queue* (ps:@ *play-queue* length)) full-track)))))
|
||||
|
||||
(update-queue-display)
|
||||
(alert (+ "Loaded " (ps:@ *play-queue* length) " tracks from \"" (ps:@ playlist name) "\" into queue!"))
|
||||
|
||||
;; Optionally start playing the first track
|
||||
(when (> (ps:@ *play-queue* length) 0)
|
||||
(let ((first-track (ps:chain *play-queue* (shift)))
|
||||
(track-index (ps:chain *tracks*
|
||||
(find-index (lambda (trk) (== (ps:@ trk id) (ps:@ first-track id))))))
|
||||
)
|
||||
(when (>= track-index 0)
|
||||
(play-track track-index))))))
|
||||
(when (or (not (ps:@ playlist tracks)) (== (ps:@ playlist tracks length) 0))
|
||||
(alert (+ "Playlist \"" (ps:@ playlist name) "\" is empty"))))
|
||||
(if (and (ps:@ playlist tracks) (> (ps:@ playlist tracks length) 0))
|
||||
(progn
|
||||
(ps:chain (ps:@ playlist tracks)
|
||||
(for-each (lambda (track)
|
||||
;; Find the full track object from our tracks array
|
||||
(let ((full-track (ps:chain *tracks*
|
||||
(find (lambda (trk) (= (ps:@ trk id) (ps:@ track id)))))))
|
||||
(when full-track
|
||||
(setf (aref *play-queue* (ps:@ *play-queue* length)) full-track))))))
|
||||
|
||||
(update-queue-display)
|
||||
(let ((loaded-count (ps:@ *play-queue* length)))
|
||||
(alert (+ "Loaded " loaded-count " tracks from \"" (ps:@ playlist name) "\" into queue!"))
|
||||
|
||||
;; Optionally start playing the first track
|
||||
(when (> loaded-count 0)
|
||||
(let* ((first-track (aref *play-queue* 0))
|
||||
(track-index (ps:chain *tracks*
|
||||
(find-index (lambda (trk) (= (ps:@ trk id) (ps:@ first-track id)))))))
|
||||
;; Remove first track from queue since we're playing it
|
||||
(ps:chain *play-queue* (shift))
|
||||
(update-queue-display)
|
||||
(when (>= track-index 0)
|
||||
(play-track track-index))))))
|
||||
(alert (+ "Playlist \"" (ps:@ playlist name) "\" is empty"))))
|
||||
(alert (+ "Error loading playlist: " (or (ps:@ data message) "Unknown error")))))))
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (error "Error loading playlist:" error))
|
||||
|
|
@ -631,23 +772,24 @@
|
|||
;; Update now playing information
|
||||
(defun update-now-playing ()
|
||||
(ps:chain
|
||||
(ps:chain (fetch "/api/asteroid/partial/now-playing"))
|
||||
(fetch "/api/asteroid/partial/now-playing")
|
||||
(then (lambda (response)
|
||||
(let ((content-type (ps:chain response (headers) (get "content-type"))))
|
||||
(let ((content-type (ps:chain response headers (get "content-type"))))
|
||||
(if (ps:chain content-type (includes "text/html"))
|
||||
(ps:chain response (text))
|
||||
(progn
|
||||
(ps:chain console (log "Error connecting to stream"))
|
||||
"")))))
|
||||
(then (lambda (data)
|
||||
(setf (ps:chain (ps:chain document (get-element-by-id "now-playing")) inner-html) data)))
|
||||
(setf (ps:chain document (get-element-by-id "now-playing") inner-h-t-m-l) data)))
|
||||
|
||||
(catch (lambda (error)
|
||||
(ps:chain console (log "Could not fetch stream status:" error))))))
|
||||
|
||||
;; Initial update after 1 second
|
||||
(ps:chain (setTimeout update-now-playing 1000))
|
||||
(set-timeout update-now-playing 1000)
|
||||
;; Update live stream info every 10 seconds
|
||||
(ps:chain (set-interval update-now-playing 10000))
|
||||
(set-interval update-now-playing 10000)
|
||||
|
||||
;; Make functions globally accessible for onclick handlers
|
||||
(defvar window (ps:@ window))
|
||||
|
|
@ -659,7 +801,11 @@
|
|||
(setf (ps:@ window library-next-page) library-next-page)
|
||||
(setf (ps:@ window library-go-to-last-page) library-go-to-last-page)
|
||||
(setf (ps:@ window change-library-tracks-per-page) change-library-tracks-per-page)
|
||||
(setf (ps:@ window load-playlist) load-playlist)))
|
||||
(setf (ps:@ window load-playlist) load-playlist)
|
||||
(setf (ps:@ window delete-playlist) delete-playlist)
|
||||
(setf (ps:@ window view-playlist) view-playlist)
|
||||
(setf (ps:@ window show-add-to-playlist-menu) show-add-to-playlist-menu)
|
||||
(setf (ps:@ window add-track-to-playlist) add-track-to-playlist)))
|
||||
"Compiled JavaScript for web player - generated at load time")
|
||||
|
||||
(defun generate-player-js ()
|
||||
|
|
|
|||
|
|
@ -14,10 +14,9 @@
|
|||
(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
|
||||
;; Note: track-ids column removed - using playlist_tracks junction table instead
|
||||
;; Let database default handle created-date (CURRENT_TIMESTAMP)
|
||||
(format t "Creating playlist with user-id: ~a (type: ~a)~%" user-id (type-of user-id))
|
||||
(format t "Playlist data: ~a~%" (data-model-as-alist playlist))
|
||||
(dm:insert playlist)
|
||||
t))
|
||||
|
||||
|
|
@ -44,53 +43,72 @@
|
|||
(dm:get-one "playlists" (db:query (:= '_id playlist-id))))
|
||||
|
||||
(defun add-track-to-playlist (playlist-id track-id)
|
||||
"Add a track to a playlist"
|
||||
(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)))))
|
||||
"Add a track to a playlist using the playlist_tracks junction table"
|
||||
(format t "Adding track ~a to playlist ~a~%" track-id playlist-id)
|
||||
(handler-case
|
||||
(postmodern:with-connection (get-db-connection-params)
|
||||
;; Get the next position for this playlist
|
||||
(let* ((max-pos-result (postmodern:query
|
||||
(format nil "SELECT COALESCE(MAX(position), 0) FROM playlist_tracks WHERE playlist_id = ~a"
|
||||
playlist-id)
|
||||
:single))
|
||||
(next-position (1+ (or max-pos-result 0))))
|
||||
(postmodern:execute
|
||||
(format nil "INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES (~a, ~a, ~a)"
|
||||
playlist-id track-id next-position))
|
||||
(format t "Added track at position ~a~%" next-position)
|
||||
t))
|
||||
(error (e)
|
||||
(format t "Error adding track to playlist: ~a~%" e)
|
||||
nil)))
|
||||
|
||||
(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 (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 (remove track-id tracks-list :test #'equal))
|
||||
;; Convert back to comma-separated string
|
||||
(track-ids-str (format nil "~{~a~^,~}" new-tracks)))
|
||||
(setf (dm:field playlist "track-ids") track-ids-str)
|
||||
(data-model-save playlist)
|
||||
t))))
|
||||
"Remove a track from a playlist using the playlist_tracks junction table"
|
||||
(format t "Removing track ~a from playlist ~a~%" track-id playlist-id)
|
||||
(handler-case
|
||||
(postmodern:with-connection (get-db-connection-params)
|
||||
(postmodern:execute
|
||||
(format nil "DELETE FROM playlist_tracks WHERE playlist_id = ~a AND track_id = ~a"
|
||||
playlist-id track-id))
|
||||
(format t "Track removed~%")
|
||||
t)
|
||||
(error (e)
|
||||
(format t "Error removing track from playlist: ~a~%" e)
|
||||
nil)))
|
||||
|
||||
(defun delete-playlist (playlist-id)
|
||||
"Delete a playlist"
|
||||
(dm:delete "playlists" (db:query (:= '_id playlist-id)))
|
||||
t)
|
||||
(defun get-playlist-tracks (playlist-id)
|
||||
"Get all track IDs for a playlist from the junction table, ordered by position"
|
||||
(handler-case
|
||||
(postmodern:with-connection (get-db-connection-params)
|
||||
(let ((results (postmodern:query
|
||||
(format nil "SELECT track_id FROM playlist_tracks WHERE playlist_id = ~a ORDER BY position"
|
||||
playlist-id))))
|
||||
(mapcar #'first results)))
|
||||
(error (e)
|
||||
(format t "Error getting playlist tracks: ~a~%" e)
|
||||
nil)))
|
||||
|
||||
(defun get-playlist-track-count (playlist-id)
|
||||
"Get the number of tracks in a playlist"
|
||||
(handler-case
|
||||
(postmodern:with-connection (get-db-connection-params)
|
||||
(let ((result (postmodern:query
|
||||
(format nil "SELECT COUNT(*) FROM playlist_tracks WHERE playlist_id = ~a"
|
||||
playlist-id)
|
||||
:single)))
|
||||
(format t "Track count for playlist ~a: ~a (type: ~a)~%" playlist-id result (type-of result))
|
||||
;; Ensure we return an integer
|
||||
(if (integerp result)
|
||||
result
|
||||
(if result (parse-integer (format nil "~a" result) :junk-allowed t) 0))))
|
||||
(error (e)
|
||||
(format t "Error getting playlist track count: ~a~%" e)
|
||||
0)))
|
||||
|
||||
(defun delete-playlist (playlist-id user-id)
|
||||
"Delete a playlist (only if owned by user)"
|
||||
(let ((playlist (get-playlist-by-id playlist-id)))
|
||||
(when (and playlist (equal (dm:field playlist "user-id") user-id))
|
||||
;; Junction table entries will be deleted by CASCADE
|
||||
(dm:delete "playlists" (db:query (:= '_id playlist-id)))
|
||||
t)))
|
||||
|
|
|
|||
Loading…
Reference in New Issue