Compare commits
4 Commits
c198775083
...
cec3763403
| Author | SHA1 | Date |
|---|---|---|
|
|
cec3763403 | |
|
|
a1cfaf468c | |
|
|
9c3d4bcec4 | |
|
|
a1fa5b0b51 |
113
asteroid.lisp
113
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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))))))
|
||||
|
|
|
|||
|
|
@ -130,8 +130,8 @@ function renderLibraryPage() {
|
|||
return `
|
||||
<div class="track-item" data-track-id="${track.id}" data-index="${actualIndex}">
|
||||
<div class="track-info">
|
||||
<div class="track-title">${track.title[0] || 'Unknown Title'}</div>
|
||||
<div class="track-meta">${track.artist[0] || 'Unknown Artist'} • ${track.album[0] || 'Unknown Album'}</div>
|
||||
<div class="track-title">${track.title || 'Unknown Title'}</div>
|
||||
<div class="track-meta">${track.artist || 'Unknown Artist'} • ${track.album || 'Unknown Album'}</div>
|
||||
</div>
|
||||
<div class="track-actions">
|
||||
<button onclick="playTrack(${actualIndex})" class="btn btn-sm btn-success">▶️</button>
|
||||
|
|
@ -186,9 +186,9 @@ function changeLibraryTracksPerPage() {
|
|||
function filterTracks() {
|
||||
const query = document.getElementById('search-tracks').value.toLowerCase();
|
||||
const filtered = tracks.filter(track =>
|
||||
(track.title[0] || '').toLowerCase().includes(query) ||
|
||||
(track.artist[0] || '').toLowerCase().includes(query) ||
|
||||
(track.album[0] || '').toLowerCase().includes(query)
|
||||
(track.title || '').toLowerCase().includes(query) ||
|
||||
(track.artist || '').toLowerCase().includes(query) ||
|
||||
(track.album || '').toLowerCase().includes(query)
|
||||
);
|
||||
displayTracks(filtered);
|
||||
}
|
||||
|
|
@ -304,9 +304,9 @@ function updatePlayButton(text) {
|
|||
|
||||
function updatePlayerDisplay() {
|
||||
if (currentTrack) {
|
||||
document.getElementById('current-title').textContent = currentTrack.title[0] || 'Unknown Title';
|
||||
document.getElementById('current-artist').textContent = currentTrack.artist[0] || 'Unknown Artist';
|
||||
document.getElementById('current-album').textContent = currentTrack.album[0] || 'Unknown Album';
|
||||
document.getElementById('current-title').textContent = currentTrack.title || 'Unknown Title';
|
||||
document.getElementById('current-artist').textContent = currentTrack.artist || 'Unknown Artist';
|
||||
document.getElementById('current-album').textContent = currentTrack.album || 'Unknown Album';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -328,8 +328,8 @@ function updateQueueDisplay() {
|
|||
const queueHtml = playQueue.map((track, index) => `
|
||||
<div class="queue-item">
|
||||
<div class="track-info">
|
||||
<div class="track-title">${track.title[0] || 'Unknown Title'}</div>
|
||||
<div class="track-meta">${track.artist[0] || 'Unknown Artist'}</div>
|
||||
<div class="track-title">${track.title || 'Unknown Title'}</div>
|
||||
<div class="track-meta">${track.artist || 'Unknown Artist'}</div>
|
||||
</div>
|
||||
<button onclick="removeFromQueue(${index})" class="btn btn-sm btn-danger">✖️</button>
|
||||
</div>
|
||||
|
|
@ -366,8 +366,10 @@ async function createPlaylist() {
|
|||
});
|
||||
|
||||
const result = await response.json();
|
||||
// Handle RADIANCE API wrapper format
|
||||
const data = result.data || result;
|
||||
|
||||
if (result.status === 'success') {
|
||||
if (data.status === 'success') {
|
||||
alert(`Playlist "${name}" created successfully!`);
|
||||
document.getElementById('new-playlist-name').value = '';
|
||||
|
||||
|
|
@ -375,7 +377,7 @@ async function createPlaylist() {
|
|||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
loadPlaylists();
|
||||
} else {
|
||||
alert('Error creating playlist: ' + result.message);
|
||||
alert('Error creating playlist: ' + data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating playlist:', error);
|
||||
|
|
@ -404,24 +406,28 @@ async function saveQueueAsPlaylist() {
|
|||
});
|
||||
|
||||
const createResult = await createResponse.json();
|
||||
// Handle RADIANCE API wrapper format
|
||||
const createData = createResult.data || createResult;
|
||||
|
||||
if (createResult.status === 'success') {
|
||||
if (createData.status === 'success') {
|
||||
// Wait a moment for database to update
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Get the new playlist ID by fetching playlists
|
||||
const playlistsResponse = await fetch('/api/asteroid/playlists');
|
||||
const playlistsResult = await playlistsResponse.json();
|
||||
// Handle RADIANCE API wrapper format
|
||||
const playlistResultData = playlistsResult.data || playlistsResult;
|
||||
|
||||
if (playlistsResult.status === 'success' && playlistsResult.playlists.length > 0) {
|
||||
if (playlistResultData.status === 'success' && playlistResultData.playlists.length > 0) {
|
||||
// Find the playlist with matching name (most recent)
|
||||
const newPlaylist = playlistsResult.playlists.find(p => p.name === name) ||
|
||||
playlistsResult.playlists[playlistsResult.playlists.length - 1];
|
||||
const newPlaylist = playlistResultData.playlists.find(p => p.name === name) ||
|
||||
playlistResultData.playlists[playlistResultData.playlists.length - 1];
|
||||
|
||||
// Add all tracks from queue to playlist
|
||||
let addedCount = 0;
|
||||
for (const track of playQueue) {
|
||||
const trackId = track.id || (Array.isArray(track.id) ? track.id[0] : null);
|
||||
const trackId = track.id;
|
||||
|
||||
if (trackId) {
|
||||
const addFormData = new FormData();
|
||||
|
|
@ -435,7 +441,7 @@ async function saveQueueAsPlaylist() {
|
|||
|
||||
const addResult = await addResponse.json();
|
||||
|
||||
if (addResult.status === 'success') {
|
||||
if (addResult.data?.status === 'success') {
|
||||
addedCount++;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -446,10 +452,11 @@ async function saveQueueAsPlaylist() {
|
|||
alert(`Playlist "${name}" created with ${addedCount} tracks!`);
|
||||
loadPlaylists();
|
||||
} else {
|
||||
alert('Playlist created but could not add tracks. Error: ' + (playlistsResult.message || 'Unknown'));
|
||||
alert('Playlist created but could not add tracks. Error: ' + (playlistResultData.message || 'Unknown'));
|
||||
loadPlaylists();
|
||||
}
|
||||
} else {
|
||||
alert('Error creating playlist: ' + createResult.message);
|
||||
alert('Error creating playlist: ' + createData.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving queue as playlist:', error);
|
||||
|
|
@ -461,11 +468,11 @@ async function loadPlaylists() {
|
|||
try {
|
||||
const response = await fetch('/api/asteroid/playlists');
|
||||
const result = await response.json();
|
||||
// Handle RADIANCE API wrapper format
|
||||
const data = result.data || result;
|
||||
|
||||
if (result.data && result.data.status === 'success') {
|
||||
displayPlaylists(result.data.playlists || []);
|
||||
} else if (result.status === 'success') {
|
||||
displayPlaylists(result.playlists || []);
|
||||
if (data && data.status === 'success') {
|
||||
displayPlaylists(data.playlists || []);
|
||||
} else {
|
||||
displayPlaylists([]);
|
||||
}
|
||||
|
|
@ -502,9 +509,11 @@ async function loadPlaylist(playlistId) {
|
|||
try {
|
||||
const response = await fetch(`/api/asteroid/playlists/get?playlist-id=${playlistId}`);
|
||||
const result = await response.json();
|
||||
// Handle RADIANCE API wrapper format
|
||||
const data = result.data || result;
|
||||
|
||||
if (result.status === 'success' && result.playlist) {
|
||||
const playlist = result.playlist;
|
||||
if (data.status === 'success' && data.playlist) {
|
||||
const playlist = data.playlist;
|
||||
|
||||
// Clear current queue
|
||||
playQueue = [];
|
||||
|
|
@ -534,7 +543,7 @@ async function loadPlaylist(playlistId) {
|
|||
alert(`Playlist "${playlist.name}" is empty`);
|
||||
}
|
||||
} else {
|
||||
alert('Error loading playlist: ' + (result.message || 'Unknown error'));
|
||||
alert('Error loading playlist: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading playlist:', error);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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*))
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<audio id="audio-player" controls preload="none" style="width: 100%; margin: 20px 0;">
|
||||
<audio id="audio-player" controls preload="none" style="width: 100%; margin: 20px 0; display: none;">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue