refactor glenn's database feature into discrete files.
This commit is contained in:
parent
cb1d6e5596
commit
873b2903cc
|
|
@ -21,4 +21,6 @@
|
|||
:pathname "./"
|
||||
:components ((:file "app-utils")
|
||||
(:file "module")
|
||||
(:file "database")
|
||||
(:file "stream-media")
|
||||
(:file "asteroid")))
|
||||
|
|
|
|||
153
asteroid.lisp
153
asteroid.lisp
|
|
@ -12,163 +12,14 @@
|
|||
(:use #:cl #:radiance #:lass #:r-clip)
|
||||
(:domain "asteroid"))
|
||||
|
||||
;; Configuration
|
||||
;; Configuration -- this will be refactored to a dedicated
|
||||
;; configuration logic. Probably using 'ubiquity
|
||||
(defparameter *server-port* 8080)
|
||||
(defparameter *music-library-path*
|
||||
(merge-pathnames "music/library/"
|
||||
(asdf:system-source-directory :asteroid)))
|
||||
(defparameter *supported-formats* '("mp3" "flac" "ogg" "wav"))
|
||||
|
||||
;; Database initialization - must be in db:connected trigger
|
||||
(define-trigger db:connected ()
|
||||
"Initialize database collections when database connects"
|
||||
(unless (db:collection-exists-p "tracks")
|
||||
(db:create "tracks" '((title :text)
|
||||
(artist :text)
|
||||
(album :text)
|
||||
(duration :integer)
|
||||
(file-path :text)
|
||||
(format :text)
|
||||
(bitrate :integer)
|
||||
(added-date :integer)
|
||||
(play-count :integer))))
|
||||
|
||||
(unless (db:collection-exists-p "playlists")
|
||||
(db:create "playlists" '((name :text)
|
||||
(description :text)
|
||||
(created-date :integer)
|
||||
(track-ids :text))))
|
||||
|
||||
(format t "Database collections initialized~%"))
|
||||
|
||||
;; Music library scanning functions
|
||||
(defun supported-audio-file-p (pathname)
|
||||
"Check if file has a supported audio format extension"
|
||||
(let ((extension (string-downcase (pathname-type pathname))))
|
||||
(member extension *supported-formats* :test #'string=)))
|
||||
|
||||
(defun scan-directory-for-music (directory)
|
||||
"Recursively scan directory for supported audio files"
|
||||
(when (cl-fad:directory-exists-p directory)
|
||||
(remove-if-not #'supported-audio-file-p
|
||||
(cl-fad:list-directory directory :follow-symlinks nil))))
|
||||
|
||||
(defun extract-metadata-with-taglib (file-path)
|
||||
"Extract metadata using taglib library"
|
||||
(handler-case
|
||||
(let* ((audio-file (audio-streams:open-audio-file (namestring file-path)))
|
||||
(file-info (sb-posix:stat file-path))
|
||||
(format (string-downcase (pathname-type file-path))))
|
||||
(list :file-path (namestring file-path)
|
||||
:format format
|
||||
:size (sb-posix:stat-size file-info)
|
||||
:modified (sb-posix:stat-mtime file-info)
|
||||
:title (or (abstract-tag:title audio-file) (pathname-name file-path))
|
||||
:artist (or (abstract-tag:artist audio-file) "Unknown Artist")
|
||||
:album (or (abstract-tag:album audio-file) "Unknown Album")
|
||||
:duration (or (and (slot-exists-p audio-file 'audio-streams::duration)
|
||||
(slot-boundp audio-file 'audio-streams::duration)
|
||||
(round (audio-streams::duration audio-file)))
|
||||
0)
|
||||
:bitrate (or (and (slot-exists-p audio-file 'audio-streams::bit-rate)
|
||||
(slot-boundp audio-file 'audio-streams::bit-rate)
|
||||
(round (audio-streams::bit-rate audio-file)))
|
||||
0)))
|
||||
(error (e)
|
||||
(format t "Warning: Could not extract metadata from ~a: ~a~%" file-path e)
|
||||
;; Fallback to basic file metadata
|
||||
(extract-basic-metadata file-path))))
|
||||
|
||||
(defun extract-basic-metadata (file-path)
|
||||
"Extract basic file metadata (fallback when taglib fails)"
|
||||
(when (probe-file file-path)
|
||||
(let ((file-info (sb-posix:stat file-path)))
|
||||
(list :file-path (namestring file-path)
|
||||
:format (string-downcase (pathname-type file-path))
|
||||
:size (sb-posix:stat-size file-info)
|
||||
:modified (sb-posix:stat-mtime file-info)
|
||||
:title (pathname-name file-path)
|
||||
:artist "Unknown Artist"
|
||||
:album "Unknown Album"
|
||||
:duration 0
|
||||
:bitrate 0))))
|
||||
|
||||
(defun insert-track-to-database (metadata)
|
||||
"Insert track metadata into database"
|
||||
(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" (getf metadata :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))))
|
||||
|
||||
(defun scan-music-library (&optional (directory *music-library-path*))
|
||||
"Scan music library directory and add tracks to database"
|
||||
(format t "Scanning music library: ~a~%" directory)
|
||||
(let ((audio-files (scan-directory-for-music directory))
|
||||
(added-count 0))
|
||||
(dolist (file audio-files)
|
||||
(let ((metadata (extract-metadata-with-taglib file)))
|
||||
(when metadata
|
||||
(handler-case
|
||||
(progn
|
||||
(insert-track-to-database metadata)
|
||||
(incf added-count)
|
||||
(format t "Added: ~a~%" (getf metadata :file-path)))
|
||||
(error (e)
|
||||
(format t "Error adding ~a: ~a~%" file e))))))
|
||||
(format t "Library scan complete. Added ~a tracks.~%" added-count)
|
||||
added-count))
|
||||
|
||||
;; Initialize music directory structure
|
||||
(defun ensure-music-directories ()
|
||||
"Create music directory structure if it doesn't exist"
|
||||
(let ((base-dir (merge-pathnames "music/" (asdf:system-source-directory :asteroid))))
|
||||
(ensure-directories-exist (merge-pathnames "library/" base-dir))
|
||||
(ensure-directories-exist (merge-pathnames "incoming/" base-dir))
|
||||
(ensure-directories-exist (merge-pathnames "temp/" base-dir))
|
||||
(format t "Music directories initialized at ~a~%" base-dir)))
|
||||
|
||||
;; Simple file copy endpoint for manual uploads
|
||||
(define-page copy-files #@"/admin/copy-files" ()
|
||||
"Copy files from incoming directory to library"
|
||||
(handler-case
|
||||
(let ((incoming-dir (merge-pathnames "music/incoming/"
|
||||
(asdf:system-source-directory :asteroid)))
|
||||
(library-dir (merge-pathnames "music/library/"
|
||||
(asdf:system-source-directory :asteroid)))
|
||||
(files-copied 0))
|
||||
(ensure-directories-exist incoming-dir)
|
||||
(ensure-directories-exist library-dir)
|
||||
|
||||
;; Process all files in incoming directory
|
||||
(dolist (file (directory (merge-pathnames "*.*" incoming-dir)))
|
||||
(when (probe-file file)
|
||||
(let* ((filename (file-namestring file))
|
||||
(file-extension (string-downcase (or (pathname-type file) "")))
|
||||
(target-path (merge-pathnames filename library-dir)))
|
||||
(when (member file-extension *supported-formats* :test #'string=)
|
||||
(alexandria:copy-file file target-path)
|
||||
(delete-file file)
|
||||
(incf files-copied)
|
||||
;; Extract metadata and add to database
|
||||
(let ((metadata (extract-metadata-with-taglib target-path)))
|
||||
(insert-track-to-database metadata))))))
|
||||
|
||||
(setf (radiance:header "Content-Type") "application/json")
|
||||
(cl-json:encode-json-to-string
|
||||
`(("status" . "success")
|
||||
("message" . ,(format nil "Copied ~d files to library" files-copied))
|
||||
("files-copied" . ,files-copied))))
|
||||
(error (e)
|
||||
(setf (radiance:header "Content-Type") "application/json")
|
||||
(cl-json:encode-json-to-string
|
||||
`(("status" . "error")
|
||||
("message" . ,(format nil "Copy failed: ~a" e)))))))
|
||||
|
||||
;; API Routes
|
||||
(define-page admin-scan-library #@"/admin/scan-library" ()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
(in-package :asteroid)
|
||||
|
||||
;; Database initialization - must be in db:connected trigger because
|
||||
;; the system could load before the database is ready.
|
||||
|
||||
(define-trigger db:connected ()
|
||||
"Initialize database collections when database connects"
|
||||
(unless (db:collection-exists-p "tracks")
|
||||
(db:create "tracks" '((title :text)
|
||||
(artist :text)
|
||||
(album :text)
|
||||
(duration :integer)
|
||||
(file-path :text)
|
||||
(format :text)
|
||||
(bitrate :integer)
|
||||
(added-date :integer)
|
||||
(play-count :integer))))
|
||||
|
||||
(unless (db:collection-exists-p "playlists")
|
||||
(db:create "playlists" '((name :text)
|
||||
(description :text)
|
||||
(created-date :integer)
|
||||
(track-ids :text))))
|
||||
|
||||
(format t "Database collections initialized~%"))
|
||||
|
|
@ -120,6 +120,372 @@ body .player .controls button{
|
|||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
body .player-section{
|
||||
background: #1a1a1a;
|
||||
padding: 25px;
|
||||
border: 1px solid #333;
|
||||
margin: 20px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
body .track-browser{
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
body .search-input{
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #0a0a0a;
|
||||
color: #00ff00;
|
||||
border: 1px solid #333;
|
||||
font-family: Courier New, monospace;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
body .track-list{
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #333;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
body .track-item{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #333;
|
||||
-moz-transition: background-color 0.2s;
|
||||
-o-transition: background-color 0.2s;
|
||||
-webkit-transition: background-color 0.2s;
|
||||
-ms-transition: background-color 0.2s;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
body .track-item :hover{
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
body .track-info{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
body .track-info .track-title{
|
||||
color: #00ff00;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
body .track-info .track-meta{
|
||||
color: #888;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
body .track-actions{
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body .audio-player{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body .track-art{
|
||||
font-size: 3em;
|
||||
margin-right: 20px;
|
||||
color: #ff6600;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body .track-details .track-title{
|
||||
font-size: 1.4em;
|
||||
color: #00ff00;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
body .track-details .track-artist{
|
||||
font-size: 1.1em;
|
||||
color: #ff6600;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
body .track-details .track-album{
|
||||
color: #888;
|
||||
}
|
||||
|
||||
body .player-controls{
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
body .player-info{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
body .time-display{
|
||||
color: #00ff00;
|
||||
font-family: Courier New, monospace;
|
||||
}
|
||||
|
||||
body .volume-control{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body .volume-control label{
|
||||
color: #ff6600;
|
||||
}
|
||||
|
||||
body .volume-slider{
|
||||
width: 100px;
|
||||
height: 5px;
|
||||
background: #333;
|
||||
outline: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
body .btn{
|
||||
background: #333;
|
||||
color: #00ff00;
|
||||
border: 1px solid #555;
|
||||
padding: 8px 16px;
|
||||
margin: 3px;
|
||||
cursor: pointer;
|
||||
font-family: Courier New, monospace;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
-moz-transition: all 0.2s;
|
||||
-o-transition: all 0.2s;
|
||||
-webkit-transition: all 0.2s;
|
||||
-ms-transition: all 0.2s;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
body .btn :hover{
|
||||
background: #555;
|
||||
border-color: #777;
|
||||
}
|
||||
|
||||
body .btn-primary{
|
||||
background: #0066cc;
|
||||
border-color: #0088ff;
|
||||
}
|
||||
|
||||
body .btn-primary :hover{
|
||||
background: #0088ff;
|
||||
}
|
||||
|
||||
body .btn-success{
|
||||
background: #006600;
|
||||
border-color: #00aa00;
|
||||
}
|
||||
|
||||
body .btn-success :hover{
|
||||
background: #00aa00;
|
||||
}
|
||||
|
||||
body .btn-danger{
|
||||
background: #cc0000;
|
||||
border-color: #ff0000;
|
||||
}
|
||||
|
||||
body .btn-danger :hover{
|
||||
background: #ff0000;
|
||||
}
|
||||
|
||||
body .btn-info{
|
||||
background: #006666;
|
||||
border-color: #00aaaa;
|
||||
}
|
||||
|
||||
body .btn-info :hover{
|
||||
background: #00aaaa;
|
||||
}
|
||||
|
||||
body .btn-warning{
|
||||
background: #cc6600;
|
||||
border-color: #ff8800;
|
||||
}
|
||||
|
||||
body .btn-warning :hover{
|
||||
background: #ff8800;
|
||||
}
|
||||
|
||||
body .btn-secondary{
|
||||
background: #444;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
body .btn-secondary :hover{
|
||||
background: #666;
|
||||
}
|
||||
|
||||
body .btn-sm{
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
body .btn.active{
|
||||
background: #ff6600;
|
||||
border-color: #ff8800;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
body .playlist-controls{
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
body .playlist-input{
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: #0a0a0a;
|
||||
color: #00ff00;
|
||||
border: 1px solid #333;
|
||||
font-family: Courier New, monospace;
|
||||
}
|
||||
|
||||
body .playlist-list{
|
||||
border: 1px solid #333;
|
||||
background: #0a0a0a;
|
||||
min-height: 100px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
body .queue-controls{
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body .play-queue{
|
||||
border: 1px solid #333;
|
||||
background: #0a0a0a;
|
||||
min-height: 150px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
body .queue-item{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
body .queue-item :last-child{
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
body .empty-queue{
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
body .no-tracks{
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
body .no-playlists{
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
body .loading{
|
||||
text-align: center;
|
||||
color: #ff6600;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
body .error{
|
||||
text-align: center;
|
||||
color: #ff0000;
|
||||
padding: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
body .upload-section{
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
body .upload-controls{
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
body .upload-info{
|
||||
color: #888;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
body .upload-progress{
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
body .progress-bar{
|
||||
height: 20px;
|
||||
background: #ff6600;
|
||||
border-radius: 3px;
|
||||
-moz-transition: width 0.3s ease;
|
||||
-o-transition: width 0.3s ease;
|
||||
-webkit-transition: width 0.3s ease;
|
||||
-ms-transition: width 0.3s ease;
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
body .progress-text{
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #00ff00;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
body input{
|
||||
padding: 8px 12px;
|
||||
background: #1a1a1a;
|
||||
color: #00ff00;
|
||||
border: 1px solid #333;
|
||||
border-radius: 3px;
|
||||
font-family: Courier New, monospace;
|
||||
}
|
||||
|
||||
body body.player-page{
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
(in-package :asteroid)
|
||||
|
||||
;; Music library scanning functions
|
||||
(defun supported-audio-file-p (pathname)
|
||||
"Check if file has a supported audio format extension"
|
||||
(let ((extension (string-downcase (pathname-type pathname))))
|
||||
(member extension *supported-formats* :test #'string=)))
|
||||
|
||||
(defun scan-directory-for-music (directory)
|
||||
"Recursively scan directory for supported audio files"
|
||||
(when (cl-fad:directory-exists-p directory)
|
||||
(remove-if-not #'supported-audio-file-p
|
||||
(cl-fad:list-directory directory :follow-symlinks nil))))
|
||||
|
||||
(defun extract-metadata-with-taglib (file-path)
|
||||
"Extract metadata using taglib library"
|
||||
(handler-case
|
||||
(let* ((audio-file (audio-streams:open-audio-file (namestring file-path)))
|
||||
(file-info (sb-posix:stat file-path))
|
||||
(format (string-downcase (pathname-type file-path))))
|
||||
(list :file-path (namestring file-path)
|
||||
:format format
|
||||
:size (sb-posix:stat-size file-info)
|
||||
:modified (sb-posix:stat-mtime file-info)
|
||||
:title (or (abstract-tag:title audio-file) (pathname-name file-path))
|
||||
:artist (or (abstract-tag:artist audio-file) "Unknown Artist")
|
||||
:album (or (abstract-tag:album audio-file) "Unknown Album")
|
||||
:duration (or (and (slot-exists-p audio-file 'audio-streams::duration)
|
||||
(slot-boundp audio-file 'audio-streams::duration)
|
||||
(round (audio-streams::duration audio-file)))
|
||||
0)
|
||||
:bitrate (or (and (slot-exists-p audio-file 'audio-streams::bit-rate)
|
||||
(slot-boundp audio-file 'audio-streams::bit-rate)
|
||||
(round (audio-streams::bit-rate audio-file)))
|
||||
0)))
|
||||
(error (e)
|
||||
(format t "Warning: Could not extract metadata from ~a: ~a~%" file-path e)
|
||||
;; Fallback to basic file metadata
|
||||
(extract-basic-metadata file-path))))
|
||||
|
||||
(defun extract-basic-metadata (file-path)
|
||||
"Extract basic file metadata (fallback when taglib fails)"
|
||||
(when (probe-file file-path)
|
||||
(let ((file-info (sb-posix:stat file-path)))
|
||||
(list :file-path (namestring file-path)
|
||||
:format (string-downcase (pathname-type file-path))
|
||||
:size (sb-posix:stat-size file-info)
|
||||
:modified (sb-posix:stat-mtime file-info)
|
||||
:title (pathname-name file-path)
|
||||
:artist "Unknown Artist"
|
||||
:album "Unknown Album"
|
||||
:duration 0
|
||||
:bitrate 0))))
|
||||
|
||||
(defun insert-track-to-database (metadata)
|
||||
"Insert track metadata into database"
|
||||
(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" (getf metadata :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))))
|
||||
|
||||
(defun scan-music-library (&optional (directory *music-library-path*))
|
||||
"Scan music library directory and add tracks to database"
|
||||
(format t "Scanning music library: ~a~%" directory)
|
||||
(let ((audio-files (scan-directory-for-music directory))
|
||||
(added-count 0))
|
||||
(dolist (file audio-files)
|
||||
(let ((metadata (extract-metadata-with-taglib file)))
|
||||
(when metadata
|
||||
(handler-case
|
||||
(progn
|
||||
(insert-track-to-database metadata)
|
||||
(incf added-count)
|
||||
(format t "Added: ~a~%" (getf metadata :file-path)))
|
||||
(error (e)
|
||||
(format t "Error adding ~a: ~a~%" file e))))))
|
||||
(format t "Library scan complete. Added ~a tracks.~%" added-count)
|
||||
added-count))
|
||||
|
||||
;; Initialize music directory structure
|
||||
(defun ensure-music-directories ()
|
||||
"Create music directory structure if it doesn't exist"
|
||||
(let ((base-dir (merge-pathnames "music/" (asdf:system-source-directory :asteroid))))
|
||||
(ensure-directories-exist (merge-pathnames "library/" base-dir))
|
||||
(ensure-directories-exist (merge-pathnames "incoming/" base-dir))
|
||||
(ensure-directories-exist (merge-pathnames "temp/" base-dir))
|
||||
(format t "Music directories initialized at ~a~%" base-dir)))
|
||||
|
||||
;; Simple file copy endpoint for manual uploads
|
||||
(define-page copy-files #@"/admin/copy-files" ()
|
||||
"Copy files from incoming directory to library"
|
||||
(handler-case
|
||||
(let ((incoming-dir (merge-pathnames "music/incoming/"
|
||||
(asdf:system-source-directory :asteroid)))
|
||||
(library-dir (merge-pathnames "music/library/"
|
||||
(asdf:system-source-directory :asteroid)))
|
||||
(files-copied 0))
|
||||
(ensure-directories-exist incoming-dir)
|
||||
(ensure-directories-exist library-dir)
|
||||
|
||||
;; Process all files in incoming directory
|
||||
(dolist (file (directory (merge-pathnames "*.*" incoming-dir)))
|
||||
(when (probe-file file)
|
||||
(let* ((filename (file-namestring file))
|
||||
(file-extension (string-downcase (or (pathname-type file) "")))
|
||||
(target-path (merge-pathnames filename library-dir)))
|
||||
(when (member file-extension *supported-formats* :test #'string=)
|
||||
(alexandria:copy-file file target-path)
|
||||
(delete-file file)
|
||||
(incf files-copied)
|
||||
;; Extract metadata and add to database
|
||||
(let ((metadata (extract-metadata-with-taglib target-path)))
|
||||
(insert-track-to-database metadata))))))
|
||||
|
||||
(setf (radiance:header "Content-Type") "application/json")
|
||||
(cl-json:encode-json-to-string
|
||||
`(("status" . "success")
|
||||
("message" . ,(format nil "Copied ~d files to library" files-copied))
|
||||
("files-copied" . ,files-copied))))
|
||||
(error (e)
|
||||
(setf (radiance:header "Content-Type") "application/json")
|
||||
(cl-json:encode-json-to-string
|
||||
`(("status" . "error")
|
||||
("message" . ,(format nil "Copy failed: ~a" e)))))))
|
||||
Loading…
Reference in New Issue