Compare commits

..

No commits in common. "cec37634030820558b4f09a77e0d325e5fc52752" and "c19877508316632257e98c47c257d5cc68282a24" have entirely different histories.

8 changed files with 230 additions and 165 deletions

View File

@ -51,16 +51,16 @@
(require-authentication) (require-authentication)
(with-error-handling (with-error-handling
(let ((tracks (with-db-error-handling "select" (let ((tracks (with-db-error-handling "select"
(dm:get "tracks" (db:query :all))))) (db:select "tracks" (db:query :all)))))
(api-output `(("status" . "success") (api-output `(("status" . "success")
("tracks" . ,(mapcar (lambda (track) ("tracks" . ,(mapcar (lambda (track)
`(("id" . ,(dm:id track)) `(("id" . ,(gethash "_id" track))
("title" . ,(dm:field track "title")) ("title" . ,(first (gethash "title" track)))
("artist" . ,(dm:field track "artist")) ("artist" . ,(first (gethash "artist" track)))
("album" . ,(dm:field track "album")) ("album" . ,(first (gethash "album" track)))
("duration" . ,(dm:field track "duration")) ("duration" . ,(first (gethash "duration" track)))
("format" . ,(dm:field track "format")) ("format" . ,(first (gethash "format" track)))
("bitrate" . ,(dm:field track "bitrate")))) ("bitrate" . ,(first (gethash "bitrate" track)))))
tracks))))))) tracks)))))))
;; Playlist API endpoints ;; Playlist API endpoints
@ -73,19 +73,26 @@
(playlists (get-user-playlists user-id))) (playlists (get-user-playlists user-id)))
(api-output `(("status" . "success") (api-output `(("status" . "success")
("playlists" . ,(mapcar (lambda (playlist) ("playlists" . ,(mapcar (lambda (playlist)
(let* ((track-ids (dm:field playlist "track-ids")) (let ((name-val (gethash "name" playlist))
;; Calculate track count from comma-separated string (desc-val (gethash "description" playlist))
;; Handle nil, empty string, or list containing empty string (track-ids-val (gethash "track-ids" playlist))
(track-count (if (and track-ids (created-val (gethash "created-date" playlist))
(stringp track-ids) (id-val (gethash "_id" playlist)))
(not (string= track-ids ""))) ;; Calculate track count from comma-separated string
(length (cl-ppcre:split "," track-ids)) ;; Handle nil, empty string, or list containing empty string
0))) (let* ((track-ids-str (if (listp track-ids-val)
`(("id" . ,(dm:id playlist)) (first track-ids-val)
("name" . ,(dm:field playlist "name")) track-ids-val))
("description" . ,(dm:field playlist "description")) (track-count (if (and track-ids-str
("track-count" . ,track-count) (stringp track-ids-str)
("created-date" . ,(dm:field playlist "created-date"))))) (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))))))
playlists))))))) playlists)))))))
(define-api asteroid/playlists/create (name &optional description) () (define-api asteroid/playlists/create (name &optional description) ()
@ -117,19 +124,23 @@
(let* ((id (parse-integer playlist-id :junk-allowed t)) (let* ((id (parse-integer playlist-id :junk-allowed t))
(playlist (get-playlist-by-id id))) (playlist (get-playlist-by-id id)))
(if playlist (if playlist
(let* ((track-ids (dm:field playlist "tracks")) (let* ((track-ids-raw (gethash "tracks" playlist))
(track-ids (if (listp track-ids-raw) track-ids-raw (list track-ids-raw)))
(tracks (mapcar (lambda (track-id) (tracks (mapcar (lambda (track-id)
(dm:get-one "tracks" (db:query (:= '_id track-id)))) (let ((track-list (db:select "tracks" (db:query (:= "_id" track-id)))))
(when (> (length track-list) 0)
(first track-list))))
track-ids)) track-ids))
(valid-tracks (remove nil tracks))) (valid-tracks (remove nil tracks)))
(api-output `(("status" . "success") (api-output `(("status" . "success")
("playlist" . (("id" . ,id) ("playlist" . (("id" . ,id)
("name" . ,(dm:field playlist "name")) ("name" . ,(let ((n (gethash "name" playlist)))
(if (listp n) (first n) n)))
("tracks" . ,(mapcar (lambda (track) ("tracks" . ,(mapcar (lambda (track)
`(("id" . ,(dm:id track)) `(("id" . ,(gethash "_id" track))
("title" . ,(dm:field track "title")) ("title" . ,(gethash "title" track))
("artist" . ,(dm:field track "artist")) ("artist" . ,(gethash "artist" track))
("album" . ,(dm:field track "album")))) ("album" . ,(gethash "album" track))))
valid-tracks))))))) valid-tracks)))))))
(api-output `(("status" . "error") (api-output `(("status" . "error")
("message" . "Playlist not found")) ("message" . "Playlist not found"))
@ -141,15 +152,15 @@
(require-authentication) (require-authentication)
(with-error-handling (with-error-handling
(let ((tracks (with-db-error-handling "select" (let ((tracks (with-db-error-handling "select"
(dm:get "tracks" (db:query :all))))) (db:select "tracks" (db:query :all)))))
(api-output `(("status" . "success") (api-output `(("status" . "success")
("tracks" . ,(mapcar (lambda (track) ("tracks" . ,(mapcar (lambda (track)
`(("id" . ,(dm:id track)) `(("id" . ,(gethash "_id" track))
("title" . ,(dm:field track "title")) ("title" . ,(gethash "title" track))
("artist" . ,(dm:field track "artist")) ("artist" . ,(gethash "artist" track))
("album" . ,(dm:field track "album")) ("album" . ,(gethash "album" track))
("duration" . ,(dm:field track "duration")) ("duration" . ,(gethash "duration" track))
("format" . ,(dm:field track "format")))) ("format" . ,(gethash "format" track))))
tracks))))))) tracks)))))))
;; Stream Control API Endpoints ;; Stream Control API Endpoints
@ -162,9 +173,9 @@
("queue" . ,(mapcar (lambda (track-id) ("queue" . ,(mapcar (lambda (track-id)
(let ((track (get-track-by-id track-id))) (let ((track (get-track-by-id track-id)))
`(("id" . ,track-id) `(("id" . ,track-id)
("title" . ,(dm:field track "title")) ("title" . ,(gethash "title" track))
("artist" . ,(dm:field track "artist")) ("artist" . ,(gethash "artist" track))
("album" . ,(dm:field track "album"))))) ("album" . ,(gethash "album" track)))))
queue))))))) queue)))))))
(define-api asteroid/stream/queue/add (track-id &optional (position "end")) () (define-api asteroid/stream/queue/add (track-id &optional (position "end")) ()
@ -224,7 +235,17 @@
(defun get-track-by-id (track-id) (defun get-track-by-id (track-id)
"Get a track by its ID - handles type mismatches" "Get a track by its ID - handles type mismatches"
(dm:get-one "tracks" (db:query (:= '_id track-id)))) ;; 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)))))
(defun get-mime-type-for-format (format) (defun get-mime-type-for-format (format)
"Get MIME type for audio format" "Get MIME type for audio format"
@ -242,8 +263,8 @@
(track (get-track-by-id id))) (track (get-track-by-id id)))
(unless track (unless track
(signal-not-found "track" id)) (signal-not-found "track" id))
(let* ((file-path (dm:field track "file-path")) (let* ((file-path (first (gethash "file-path" track)))
(format (dm:field track "format")) (format (first (gethash "format" track)))
(file (probe-file file-path))) (file (probe-file file-path)))
(unless file (unless file
(error 'not-found-error (error 'not-found-error
@ -255,8 +276,8 @@
(setf (radiance:header "Accept-Ranges") "bytes") (setf (radiance:header "Accept-Ranges") "bytes")
(setf (radiance:header "Cache-Control") "public, max-age=3600") (setf (radiance:header "Cache-Control") "public, max-age=3600")
;; Increment play count ;; Increment play count
(setf (dm:field track "play-count") (1+ (dm:field track "play-count"))) (db:update "tracks" (db:query (:= '_id id))
(data-model-save track) `(("play-count" ,(1+ (first (gethash "play-count" track))))))
;; Return file contents ;; Return file contents
(alexandria:read-file-into-byte-vector file))))) (alexandria:read-file-into-byte-vector file)))))
@ -312,8 +333,8 @@
(api-output `(("status" . "success") (api-output `(("status" . "success")
("message" . "Playback started") ("message" . "Playback started")
("track" . (("id" . ,id) ("track" . (("id" . ,id)
("title" . ,(dm:field track "title")) ("title" . ,(first (gethash "title" track)))
("artist" . ,(dm:field track "artist")))) ("artist" . ,(first (gethash "artist" track)))))
("player" . ,(get-player-status))))))) ("player" . ,(get-player-status)))))))
(define-api asteroid/player/pause () () (define-api asteroid/player/pause () ()
@ -503,7 +524,7 @@
"Admin dashboard" "Admin dashboard"
(require-authentication) (require-authentication)
(let ((track-count (handler-case (let ((track-count (handler-case
(length (dm:get "tracks" (db:query :all))) (length (db:select "tracks" (db:query :all)))
(error () 0)))) (error () 0))))
(clip:process-to-string (clip:process-to-string
(load-template "admin") (load-template "admin")

View File

@ -157,7 +157,7 @@
Usage: Usage:
(with-db-error-handling \"select\" (with-db-error-handling \"select\"
(dm:get 'tracks (db:query :all)))" (db:select 'tracks (db:query :all)))"
`(handler-case `(handler-case
(progn ,@body) (progn ,@body)
(error (e) (error (e)

View File

@ -20,7 +20,6 @@
(db:create "playlists" '((name :text) (db:create "playlists" '((name :text)
(description :text) (description :text)
(created-date :integer) (created-date :integer)
(user-id :integer)
(track-ids :text)))) (track-ids :text))))
(unless (db:collection-exists-p "USERS") (unless (db:collection-exists-p "USERS")

View File

@ -10,72 +10,94 @@
(unless (db:collection-exists-p "playlists") (unless (db:collection-exists-p "playlists")
(error "Playlists collection does not exist in database")) (error "Playlists collection does not exist in database"))
(let ((playlist (dm:hull "playlists"))) (let ((playlist-data `(("user-id" ,user-id)
(setf (dm:field playlist "user-id") user-id) ("name" ,name)
(setf (dm:field playlist "name") name) ("description" ,(or description ""))
(setf (dm:field playlist "description") (or description "")) ("track-ids" "") ; Empty string for text field
(setf (dm:field playlist "track-ids") "") ; Empty string for text field ("created-date" ,(local-time:timestamp-to-unix (local-time:now))))))
(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 "Creating playlist with user-id: ~a (type: ~a)~%" user-id (type-of user-id))
(format t "Playlist data: ~a~%" (data-model-as-alist playlist)) (format t "Playlist data: ~a~%" playlist-data)
(dm:insert playlist) (db:insert "playlists" playlist-data)
t)) t))
(defun get-user-playlists (user-id) (defun get-user-playlists (user-id)
"Get all playlists for a user" "Get all playlists for a user"
(format t "Querying playlists with user-id: ~a (type: ~a)~%" user-id (type-of user-id)) (format t "Querying playlists with user-id: ~a (type: ~a)~%" user-id (type-of user-id))
(let ((all-playlists (dm:get "playlists" (db:query :all)))) (let ((all-playlists (db:select "playlists" (db:query :all))))
(format t "Total playlists in database: ~a~%" (length all-playlists)) (format t "Total playlists in database: ~a~%" (length all-playlists))
(when (> (length all-playlists) 0) (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)~%" (format t "First playlist user-id: ~a (type: ~a)~%"
first-playlist-user (gethash "user-id" first-playlist)
(type-of first-playlist-user)))) (type-of (gethash "user-id" first-playlist)))))
;; Filter manually since DB stores user-id as a list (2) instead of 2 ;; Filter manually since DB stores user-id as a list (2) instead of 2
(remove-if-not (lambda (playlist) (remove-if-not (lambda (playlist)
(let ((stored-user-id (dm:field playlist "user-id"))) (let ((stored-user-id (gethash "user-id" playlist)))
(equal stored-user-id user-id))) (or (equal stored-user-id user-id)
(and (listp stored-user-id)
(equal (first stored-user-id) user-id)))))
all-playlists))) all-playlists)))
(defun get-playlist-by-id (playlist-id) (defun get-playlist-by-id (playlist-id)
"Get a specific playlist by ID" "Get a specific playlist by ID"
(format t "get-playlist-by-id called with: ~a (type: ~a)~%" playlist-id (type-of playlist-id)) (format t "get-playlist-by-id called with: ~a (type: ~a)~%" playlist-id (type-of playlist-id))
(dm:get-one "playlists" (db:query (:= '_id 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)))))
(defun add-track-to-playlist (playlist-id track-id) (defun add-track-to-playlist (playlist-id track-id)
"Add a track to a playlist" "Add a track to a playlist"
(db:with-transaction () (let ((playlist (get-playlist-by-id playlist-id)))
(let ((playlist (get-playlist-by-id playlist-id))) (when playlist
(when playlist (let* ((current-track-ids-raw (gethash "track-ids" playlist))
(let* ((current-track-ids (dm:field playlist "track-ids")) ;; Handle database storing as list - extract string
;; Parse comma-separated string into list (current-track-ids (if (listp current-track-ids-raw)
(tracks-list (if (and current-track-ids (first current-track-ids-raw)
(stringp current-track-ids) current-track-ids-raw))
(not (string= current-track-ids ""))) ;; Parse comma-separated string into list
(mapcar #'parse-integer (tracks-list (if (and current-track-ids
(cl-ppcre:split "," current-track-ids)) (stringp current-track-ids)
nil)) (not (string= current-track-ids "")))
(new-tracks (append tracks-list (list track-id))) (mapcar #'parse-integer
;; Convert back to comma-separated string (cl-ppcre:split "," current-track-ids))
(track-ids-str (format nil "~{~a~^,~}" new-tracks))) nil))
(format t "Adding track ~a to playlist ~a~%" track-id playlist-id) (new-tracks (append tracks-list (list track-id)))
(format t "Current track-ids raw: ~a (type: ~a)~%" current-track-ids-raw (type-of current-track-ids-raw)) ;; Convert back to comma-separated string
(format t "Current track-ids: ~a~%" current-track-ids) (track-ids-str (format nil "~{~a~^,~}" new-tracks)))
(format t "Tracks list: ~a~%" tracks-list) (format t "Adding track ~a to playlist ~a~%" track-id playlist-id)
(format t "New tracks: ~a~%" new-tracks) (format t "Current track-ids raw: ~a (type: ~a)~%" current-track-ids-raw (type-of current-track-ids-raw))
(format t "Track IDs string: ~a~%" track-ids-str) (format t "Current track-ids: ~a~%" current-track-ids)
;; Update using track-ids field (defined in schema) (format t "Tracks list: ~a~%" tracks-list)
(setf (dm:field playlist "track-ids") track-ids-str) (format t "New tracks: ~a~%" new-tracks)
(data-model-save playlist) (format t "Track IDs string: ~a~%" track-ids-str)
(format t "Update complete~%") ;; Update using track-ids field (defined in schema)
t))))) (db:update "playlists"
(db:query (:= "_id" playlist-id))
`(("track-ids" ,track-ids-str)))
(format t "Update complete~%")
t))))
(defun remove-track-from-playlist (playlist-id track-id) (defun remove-track-from-playlist (playlist-id track-id)
"Remove a track from a playlist" "Remove a track from a playlist"
(let ((playlist (get-playlist-by-id playlist-id))) (let ((playlist (get-playlist-by-id playlist-id)))
(when playlist (when playlist
(let* ((current-track-ids (dm:field playlist "track-ids")) (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 ;; Parse comma-separated string into list
(tracks-list (if (and current-track-ids (tracks-list (if (and current-track-ids
(stringp current-track-ids) (stringp current-track-ids)
@ -86,11 +108,28 @@
(new-tracks (remove track-id tracks-list :test #'equal)) (new-tracks (remove track-id tracks-list :test #'equal))
;; Convert back to comma-separated string ;; Convert back to comma-separated string
(track-ids-str (format nil "~{~a~^,~}" new-tracks))) (track-ids-str (format nil "~{~a~^,~}" new-tracks)))
(setf (dm:field playlist "track-ids") track-ids-str) (db:update "playlists"
(data-model-save playlist) (db:query (:= "_id" playlist-id))
`(("track-ids" ,track-ids-str)))
t)))) t))))
(defun delete-playlist (playlist-id) (defun delete-playlist (playlist-id)
"Delete a playlist" "Delete a playlist"
(dm:delete "playlists" (db:query (:= '_id playlist-id))) (db:remove "playlists" (db:query (:= "_id" playlist-id)))
t) 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))))))

View File

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

View File

@ -45,8 +45,11 @@
"Add all tracks from a playlist to the stream queue" "Add all tracks from a playlist to the stream queue"
(let ((playlist (get-playlist-by-id playlist-id))) (let ((playlist (get-playlist-by-id playlist-id)))
(when playlist (when playlist
(let* ((track-ids-str (dm:field playlist "track-ids")) (let* ((track-ids-raw (gethash "track-ids" playlist))
(track-ids (if (and track-ids-str (track-ids-str (if (listp track-ids-raw)
(first track-ids-raw)
track-ids-raw))
(track-ids (if (and track-ids-str
(stringp track-ids-str) (stringp track-ids-str)
(not (string= track-ids-str ""))) (not (string= track-ids-str "")))
(mapcar #'parse-integer (mapcar #'parse-integer
@ -62,7 +65,10 @@
"Get the file path for a track by ID" "Get the file path for a track by ID"
(let ((track (get-track-by-id track-id))) (let ((track (get-track-by-id track-id)))
(when track (when track
(dm:field track "file-path")))) (let ((file-path (gethash "file-path" track)))
(if (listp file-path)
(first file-path)
file-path)))))
(defun convert-to-docker-path (host-path) (defun convert-to-docker-path (host-path)
"Convert host file path to Docker container path" "Convert host file path to Docker container path"
@ -95,10 +101,11 @@
(asdf:system-source-directory :asteroid)))) (asdf:system-source-directory :asteroid))))
(if (null *stream-queue*) (if (null *stream-queue*)
;; If queue is empty, generate from all tracks (fallback) ;; If queue is empty, generate from all tracks (fallback)
(let ((all-tracks (dm:get "tracks" (db:query :all)))) (let ((all-tracks (db:select "tracks" (db:query :all))))
(generate-m3u-playlist (generate-m3u-playlist
(mapcar (lambda (track) (mapcar (lambda (track)
(dm:id track)) (let ((id (gethash "_id" track)))
(if (listp id) (first id) id)))
all-tracks) all-tracks)
playlist-path)) playlist-path))
;; Generate from queue ;; Generate from queue
@ -108,8 +115,11 @@
"Export a user playlist to an M3U file" "Export a user playlist to an M3U file"
(let ((playlist (get-playlist-by-id playlist-id))) (let ((playlist (get-playlist-by-id playlist-id)))
(when playlist (when playlist
(let* ((track-ids-str (dm:field playlist "track-ids")) (let* ((track-ids-raw (gethash "track-ids" playlist))
(track-ids (if (and track-ids-str (track-ids-str (if (listp track-ids-raw)
(first track-ids-raw)
track-ids-raw))
(track-ids (if (and track-ids-str
(stringp track-ids-str) (stringp track-ids-str)
(not (string= track-ids-str ""))) (not (string= track-ids-str "")))
(mapcar #'parse-integer (mapcar #'parse-integer
@ -118,6 +128,7 @@
(generate-m3u-playlist track-ids output-path))))) (generate-m3u-playlist track-ids output-path)))))
;;; Stream History Management ;;; Stream History Management
(defun add-to-stream-history (track-id) (defun add-to-stream-history (track-id)
"Add a track to the stream history" "Add a track to the stream history"
(push track-id *stream-history*) (push track-id *stream-history*)
@ -134,11 +145,12 @@
(defun build-smart-queue (genre &optional (count 20)) (defun build-smart-queue (genre &optional (count 20))
"Build a smart queue based on genre" "Build a smart queue based on genre"
(let ((tracks (dm:get "tracks" (db:query :all)))) (let ((tracks (db:select "tracks" (db:query :all))))
;; For now, just add random tracks ;; For now, just add random tracks
;; TODO: Implement genre filtering when we have genre metadata ;; TODO: Implement genre filtering when we have genre metadata
(let ((track-ids (mapcar (lambda (track) (let ((track-ids (mapcar (lambda (track)
(dm:id track)) (let ((id (gethash "_id" track)))
(if (listp id) (first id) id)))
tracks))) tracks)))
(setf *stream-queue* (subseq (alexandria:shuffle track-ids) (setf *stream-queue* (subseq (alexandria:shuffle track-ids)
0 0
@ -148,16 +160,18 @@
(defun build-queue-from-artist (artist-name &optional (count 20)) (defun build-queue-from-artist (artist-name &optional (count 20))
"Build a queue from tracks by a specific artist" "Build a queue from tracks by a specific artist"
(let ((tracks (dm:get "tracks" (db:query :all)))) (let ((tracks (db:select "tracks" (db:query :all))))
(let ((matching-tracks (let ((matching-tracks
(remove-if-not (remove-if-not
(lambda (track) (lambda (track)
(let ((artist (dm:field track "artist"))) (let ((artist (gethash "artist" track)))
(when artist (when artist
(search artist-name artist :test #'char-equal)))) (let ((artist-str (if (listp artist) (first artist) artist)))
(search artist-name artist-str :test #'char-equal)))))
tracks))) tracks)))
(let ((track-ids (mapcar (lambda (track) (let ((track-ids (mapcar (lambda (track)
(dm:id track)) (let ((id (gethash "_id" track)))
(if (listp id) (first id) id)))
matching-tracks))) matching-tracks)))
(setf *stream-queue* (subseq track-ids 0 (min count (length track-ids)))) (setf *stream-queue* (subseq track-ids 0 (min count (length track-ids))))
(regenerate-stream-playlist) (regenerate-stream-playlist)
@ -178,7 +192,7 @@
(let* ((m3u-path (merge-pathnames "stream-queue.m3u" (let* ((m3u-path (merge-pathnames "stream-queue.m3u"
(asdf:system-source-directory :asteroid))) (asdf:system-source-directory :asteroid)))
(track-ids '()) (track-ids '())
(all-tracks (dm:get "tracks" (db:query :all)))) (all-tracks (db:select "tracks" (db:query :all))))
(when (probe-file m3u-path) (when (probe-file m3u-path)
(with-open-file (stream m3u-path :direction :input) (with-open-file (stream m3u-path :direction :input)
@ -192,12 +206,14 @@
;; Find track by file path ;; Find track by file path
(let ((track (find-if (let ((track (find-if
(lambda (trk) (lambda (trk)
(let ((file-path (dm:field trk "file-path"))) (let ((fp (gethash "file-path" trk)))
(string= file-path host-path))) (let ((file-path (if (listp fp) (first fp) fp)))
(string= file-path host-path))))
all-tracks))) all-tracks)))
(when track (when track
(push (dm:id track) track-ids)))))))) (let ((id (gethash "_id" track)))
(push (if (listp id) (first id) id) track-ids)))))))))
;; Reverse to maintain order from file ;; Reverse to maintain order from file
(setf track-ids (nreverse track-ids)) (setf track-ids (nreverse track-ids))
(setf *stream-queue* track-ids) (setf *stream-queue* track-ids)

View File

@ -62,17 +62,16 @@
(defun track-exists-p (file-path) (defun track-exists-p (file-path)
"Check if a track with the given file path already exists in the database" "Check if a track with the given file path already exists in the database"
;; Try direct query first ;; Try direct query first
(let ((existing (dm:get "tracks" (db:query (:= "file-path" file-path))))) (let ((existing (db:select "tracks" (db:query (:= "file-path" file-path)))))
(if (> (length existing) 0) (if (> (length existing) 0)
t t
;; If not found, search manually (file-path might be stored as list) ;; If not found, search manually (file-path might be stored as list)
(let ((all-tracks (dm:get "tracks" (db:query :all)))) (let ((all-tracks (db:select "tracks" (db:query :all))))
(some (lambda (track) (some (lambda (track)
(let ((stored-path (dm:field track "file-path"))) (let ((stored-path (gethash "file-path" track)))
(or (equal stored-path file-path) (or (equal stored-path file-path)
(and (listp stored-path) (equal (first stored-path) file-path))))) (and (listp stored-path) (equal (first stored-path) file-path)))))
all-tracks) all-tracks)))))
))))
(defun insert-track-to-database (metadata) (defun insert-track-to-database (metadata)
"Insert track metadata into database if it doesn't already exist" "Insert track metadata into database if it doesn't already exist"
@ -84,17 +83,17 @@
(let ((file-path (getf metadata :file-path))) (let ((file-path (getf metadata :file-path)))
(if (track-exists-p file-path) (if (track-exists-p file-path)
nil nil
(let ((track (dm:hull "tracks"))) (progn
(setf (dm:field track "title") (getf metadata :title)) (db:insert "tracks"
(setf (dm:field track "artist") (getf metadata :artist)) (list (list "title" (getf metadata :title))
(setf (dm:field track "album") (getf metadata :album)) (list "artist" (getf metadata :artist))
(setf (dm:field track "duration") (getf metadata :duration)) (list "album" (getf metadata :album))
(setf (dm:field track "file-path") file-path) (list "duration" (getf metadata :duration))
(setf (dm:field track "format") (getf metadata :format)) (list "file-path" file-path)
(setf (dm:field track "bitrate") (getf metadata :bitrate)) (list "format" (getf metadata :format))
(setf (dm:field track "added-date") (local-time:timestamp-to-unix (local-time:now))) (list "bitrate" (getf metadata :bitrate))
(setf (dm:field track "play-count") 0) (list "added-date" (local-time:timestamp-to-unix (local-time:now)))
(dm:insert track) (list "play-count" 0)))
t)))) t))))
(defun scan-music-library (&optional (directory *music-library-path*)) (defun scan-music-library (&optional (directory *music-library-path*))

View File

@ -82,7 +82,7 @@
</div> </div>
</div> </div>
<audio id="audio-player" controls preload="none" style="width: 100%; margin: 20px 0; display: none;"> <audio id="audio-player" controls preload="none" style="width: 100%; margin: 20px 0;">
Your browser does not support the audio element. Your browser does not support the audio element.
</audio> </audio>