Mercurial > core / lisp/lib/aud/mpd.lisp
changeset 698: |
96958d3eb5b0 |
parent: |
ea3b643a27a3
|
author: |
Richard Westhaver <ellis@rwest.io> |
date: |
Fri, 04 Oct 2024 22:04:59 -0400 |
permissions: |
-rw-r--r-- |
description: |
fixes |
1 ;;; aud/mpd.lisp --- MPD Interface for Lisp 3 ;; based on https://github.com/stassats/mpd 7 ;; The original code hasn't been updated in quite some time. Here 8 ;; we've added in some missing slots, fixed a typo, removed the 9 ;; dependency on usocket library and extended the functionality 16 (define-condition mpd-error (error) 17 ((text :initarg :text :reader text 19 (:report (lambda (condition stream) 20 (princ (text condition) stream)))) 22 (macrolet ((define-conditions (names) 25 `(define-condition ,name (mpd-error) ())) 27 (define-conditions (bad-argument incorrect-password 28 not-permitted unknown-command not-exist 29 playlist-size-exceed already-updating exist))) 31 (defparameter *error-ids-alist* 33 (3 . incorrect-password) 37 (51 . playlist-size-exceed) 38 (54 . already-updating) 42 (defparameter *tag-types* 43 '(:artist :album :title :track :name :genre :date 44 :composer :performer :comment :disc :filename :any) 45 "Types of tags for using in `search' and `find'")) 48 `(member ,@*tag-types*)) 52 :initform nil :initarg :file :accessor file) 54 :initform nil :initarg :title :accessor title) 56 :initform nil :initarg :artist :accessor artist) 58 :initform nil :initarg :albumartist :accessor albumartist) 60 :initform nil :initarg :album :accessor album) 62 :initform nil :initarg :genre :accessor genre) 64 :initform nil :initarg :date :accessor date) 66 :initform nil :initarg :performer :accessor performer) 68 :initform nil :initarg :composer :accessor composer) 70 :initform nil :initarg :disc :accessor disc) 72 :initform nil :initarg :track :accessor track-number) 74 :initform nil :initarg :time :accessor duration) 76 :initform nil :initarg :last-modified :accessor last-modified))) 78 (defclass playlist (track) 80 :initform 0 :initarg :pos :accessor position-in-playlist 83 :initform nil :initarg :duration) 84 (format :initform nil :initarg :format) 86 :initform 0 :initarg :id :accessor id 91 :reader volume :initarg :volume :initform nil) 93 :reader repeat :initarg :repeat :initform nil) 95 :reader randomized :initarg :random :initform nil) 97 :reader playlist-version :initarg :playlist :initform nil) 99 :reader playlist-length :initarg :playlistlength :initform nil) 101 :reader xfade :initarg :xfade :initform nil) 103 :reader state :initarg :state :initform nil) 105 :reader partition :initarg :partition :initform nil) 107 :reader audio :initarg :audio :initform nil) 109 :reader bitrate :initarg :bitrate :initform nil) 111 :reader duration :initarg :duration :initform nil) 113 :reader %time :initarg :time :initform nil) 115 :reader songid :initarg :songid :initform nil) 116 (song :reader song :initarg :song :initform nil) 118 :reader nextsongid :initarg :nextsongid :initform nil) 120 :reader nextsong :initarg :nextsong :initform nil) 122 :reader elapsed :initarg :elapsed :initform nil) 124 :reader mixrampdb :initarg :mixrampdb :initform nil) 126 :reader consume :initarg :consume :initform nil) 128 :reader single :initarg :single :initform nil))) 132 :reader artists :initarg :artists :initform nil) 134 :reader albums :initarg :albums :initform nil) 136 :reader songs :initarg :songs :initform nil) 138 :reader uptime :initarg :uptime :initform nil) 140 :reader playtime :initarg :playtime :initform nil) 142 :reader db-playtime :initarg :db_playtime :initform nil) 144 :reader db-update :initarg :db_update :initform nil))) 146 (macrolet ((generate-commands (class names) 148 ,@(mapcar (lambda (name) 149 `(defmethod ,name ((stream socket)) 150 (,name (,class stream)))) 152 (generate-commands status 153 (volume repeat randomized playlist-version playlist-length 154 xfade state audio bitrate duration songid song)) 155 (generate-commands stats 156 (artists albums songs uptime playtime db-playtime db-update))) 158 (defparameter *integer-keys* 159 '(:id :pos :volume :playlist :playlistlength 160 :xfade :song :songid :bitrate :playtime 161 :artists :albums :songs :uptime :db_playtime :db_update 163 "List of keys which values must be integers.") 165 (defparameter *value-processing-functions* 166 '(:time parse-time :state to-keyword 167 :random string-not-zerop :repeat string-not-zerop 168 :outputenabled string-not-zerop)) 170 (defmethod print-object ((object track) stream) 171 (print-unreadable-object (object stream :type t :identity t) 172 (with-slots (artist title album) object 173 (format stream "~A - ~A (~A)" artist title album)))) 176 (defvar *default-host* (or (sb-posix:getenv "MPD_HOST") "localhost")) 177 (defvar *default-port* (or (when-let ((port (sb-posix:getenv "MPD_PORT"))) (parse-integer port)) 6600)) 179 (defun connect (&key (host *default-host*) (port *default-port*) password) 181 (let ((connection (socket-connect (make-instance 'inet-socket :type :stream) (get-address-by-name host) port))) 182 (prog1 (values connection 183 (read-answer (socket-make-stream connection :input t :output t))) 185 (password connection password))))) 187 (defun read-answer (stream) 188 (loop for line = (read-line stream) 189 until (string= line "OK" :end1 2) 191 when (string= line "ACK" :end1 3) 192 do (throw-error line))) 194 (defun throw-error (text) 195 ;; Error format: `ACK [<error id>@<position>] {<comand name>} <description>' 196 (let* ((error-id (parse-integer text :start 5 :junk-allowed t)) 197 (delimiter (position #\] text)) 198 (condition (cdr (assoc error-id *error-ids-alist*)))) 199 (error condition :text (subseq text (+ delimiter 2))))) 202 (defmacro with-mpc ((var &rest options) &body body) 203 `(let ((,var (connect ,@options))) 206 (disconnect ,var))))) 211 (not-exist () (sb-ext:run-program "mpd" nil :search t :directory (user-homedir-pathname) :wait nil)))) 213 (defun send-command (connection command) 214 "Send command to MPD." 215 (let ((stream (socket-make-stream connection :input t))) 216 (unless (open-stream-p stream) 217 (error 'mpd-error :text (format nil "The stream ~A is not opened." stream))) 218 (write-line command stream) 219 (finish-output stream) 220 (read-answer stream))) 224 (defun to-keyword (name) 225 (intern (string-upcase name) :keyword)) 227 (defun split-value (string) 228 "Split a string `key: value' into (list :key value)." 229 (let ((column (position #\: string))) 230 (process-value (to-keyword (subseq string 0 column)) 231 (subseq string (+ 2 column))))) 233 (defun split-values (strings) 234 "Transform a list of strings 'key: value' into the plist." 235 (mapcan #'split-value strings)) 237 (defun process-value (key value) 239 (funcall (value-processing-function key) value))) 241 (defun value-processing-function (key) 242 (if (member key *integer-keys*) 244 (getf *value-processing-functions* key #'identity))) 246 (defun parse-time (time) 247 "\"10:20\" -> (10 20); \"10\" -> 10" 248 (multiple-value-bind (first stop) 249 (parse-integer time :junk-allowed t) 250 (if (= stop (length time)) 253 (parse-integer time :start (1+ stop)))))) 255 (defun string-not-zerop (string) 256 (not (string= string "0"))) 258 (defun filter-keys (strings) 259 "Transform a list of strings 'key: value' into a list of values." 260 (mapcar (lambda (entry) 261 (subseq entry (+ 2 (position #\: entry)))) 264 (defun make-class (data type) 265 "Make a new instance of the class playlist with initargs from 266 the list of strings `key: value'." 267 (apply 'make-instance type (split-values data))) 269 (defun parse-list (list &optional class) 270 "Make a list of new instances of the class `class' with initargs from 271 a list of strings `key: value'. Each track is separeted by the `file' key." 273 (flet ((create-track () 275 (list (apply 'make-instance class track))))) 278 (let ((pair (split-value x))) 280 (:file (prog1 (create-track) 282 ((:directory :playlist) 284 (t (nconc track pair) 291 (defun process-string (string) 292 "Check for emtpy strings, and escape strings when needed." 295 (string-trim '(#\Space #\Tab #\Newline) string))) 296 (when (zerop (length string)) 297 (error 'mpd-error :text "Zero length argument.")) 298 (if (position #\Space string) 299 (prin1-to-string string) 304 (defmacro send (&rest commands) 305 "Macro for using inside `defcommand'." 306 `(send-command connection 307 (format nil "~{~A~^ ~}" 308 (remove nil (list ,@commands))))) 310 (defmacro defcommand (name parameters &body body) 311 `(defun ,name (connection ,@parameters) 314 (defmacro defmethod-command (name parameters &body body) 315 `(defmethod ,name (connection ,@parameters) 318 (defmacro check-args (type &rest args) 319 "Check string and integer arguments." 320 (if (or (eq type 'string) 322 (member 'string type))) 325 `((check-type ,arg ,type "a string") 326 (setf ,arg (process-string ,arg)))) 330 `(check-type ,arg ,type)) 334 (defcommand password (password) 336 (check-args string password) 337 (send "password" password)) 339 (defcommand disconnect () 341 (socket-close connection)) 343 (defcommand now-playing () 344 "Return instance of playlist with current song." 345 (let ((track (send "currentsong"))) 347 (make-class track 'playlist)))) 349 (defcommand disable-output (id) 350 (check-args unsigned-byte id) 351 (send "disableoutput" id)) 353 (defcommand enable-output (id) 354 (check-args unsigned-byte id) 355 (send "enableoutput" id)) 362 "Stop MPD in a safe way." 365 (defcommand status () 366 "Return status of MPD." 367 (make-class (send "status") 'status)) 371 (make-class (send "stats") 'stats)) 373 (defcommand outputs () 374 "Return information about all outputs." 375 (split-values (send "outputs"))) 377 (defcommand commands () 378 "Return list of available commands." 379 (filter-keys (send "commands"))) 381 (defcommand not-commands () 382 "Return list of commands to which the current user does not have access." 384 (send "notcommands"))) 389 "Toggle pause / resume playing." 392 (defcommand play (&optional song-number) 393 (check-args (or unsigned-byte null) song-number) 394 "Begin playing the playlist starting from song-number, default is 0." 395 (send "play" song-number)) 402 "Play next track in the playlist." 405 (defcommand previous () 406 "Play previous track in the playlist." 409 (defcommand crossfade (seconds) 410 (check-args unsigned-byte seconds) 411 "Sets crossfading between songs." 412 (send "crossfade" seconds)) 416 (defcommand list-playlist (name) 417 "List files in the playlist `name'" 418 (check-args string name) 419 (filter-keys (send "listplaylist" name))) 421 (defcommand list-playlist-info (name) 422 "List metadata of tracks in the playlist `name'" 423 (check-args string name) 424 (parse-list (send "listplaylistinfo" name) 'playlist)) 427 "Clear the current playlist." 430 (defcommand save-playlist (filename) 431 "Save the current playlist to the file in the playlist directory." 432 (check-args string filename) 433 (send "save" filename)) 435 (defcommand load-playlist (filename) 436 "Load playlist from file." 437 (check-args string filename) 438 (send "load" filename)) 440 (defcommand rename-playlist (name new-name) 442 (check-args string name new-name) 443 (unless (equal name new-name) 444 (send "rename" name new-name))) 446 (defcommand playlist-info (&optional id) 447 "Return content of the current playlist." 448 (check-args (or unsigned-byte null) id) 450 (make-class (send "playlistinfo" id) 'playlist) 451 (parse-list (send "playlistinfo") 'playlist))) 453 (defcommand playlist-changes (version) 454 "Return changed songs currently in the playlist since `version'." 455 (check-args unsigned-byte version) 456 (parse-list (send "plchanges" version) 'playlist)) 458 (defcommand add-to-playlist (name path) 459 "Add `path' to the playlist `name'." 460 (check-args string name path) 461 (send "playlistadd" name path)) 463 (defcommand clear-playlist (name) 464 "Clear playlist `name'." 465 (check-args string name) 466 (send "playlistclear")) 468 (defcommand delete-from-playlist (name song-id) 469 "Delete `song-id' from playlist `name'." 470 (check-args string name) 471 (check-args unsigned-byte song-id) 472 (send "playlistdelete" name song-id)) 474 (defcommand move-in-playlist (name song-id position) 475 "Move `song-id' in playlist `name' to `position'." 476 (check-args string name) 477 (check-args unsigned-byte song-id position) 478 (send "playlistmove" name song-id position)) 480 (defcommand find-in-current-playlist (scope query) 481 "Search for songs in the current playlist with strict matching." 482 (check-args string scope query) 483 (send "playlistfind" scope query)) 485 (defcommand search-in-current-playlist (scope query) 486 "Search case-insensitively with partial matches for songs in the current playlist" 487 (check-args string scope query) 488 (send "playlistsearch" scope query)) 490 (defgeneric add (connection what) 491 (:documentation "Add file or directory to the current playlist.")) 493 (defmethod-command add ((what track)) 494 (add connection (file what))) 496 (defmethod-command add ((what string)) 497 (check-args string what) 500 (defgeneric add-id (connection what) 501 (:documentation "Like add, but returns a id.")) 503 (defmethod-command add-id ((what track)) 504 (add connection (file what))) 506 (defmethod-command add-id ((what string)) 507 (check-args string what) 508 (car (filter-keys (send "addid" what)))) 510 (defcommand move (from to) 511 "Move track from `from' to `to' in the playlist." 512 (check-args unsigned-byte from to) 514 (send "move" from to))) 516 (defgeneric move-id (connection id to) 517 (:documentation "Move track with `id' to `to' in the playlist.")) 519 (defmethod-command move-id ((track playlist) (to integer)) 520 (move-id connection (id track) to)) 522 (defmethod-command move-id ((id integer) (to integer)) 523 (check-args unsigned-byte id to) 524 (send "moveid" id to)) 526 (defcommand swap (first second) 527 "Swap positions of two tracks." 528 (check-args unsigned-byte first second) 529 (unless (= first second) 530 (send "swap" first second))) 532 (defgeneric swap-id (connection first second) 533 (:documentation "Swap positions of two tracks by id.")) 535 (defmethod-command swap-id ((first playlist) (second playlist)) 536 (swap-id connection (id first) (id second))) 538 (defmethod-command swap-id ((first integer) (second integer)) 539 (check-args unsigned-byte first second) 540 (send "swap" first second)) 542 (defcommand delete-track (number) 543 "Delete track from playlist." 544 (check-args unsigned-byte number) 545 (send "delete" number)) 547 (defgeneric delete-id (connection id) 548 (:documentation "Delete track with `id' from playlist.")) 550 (defmethod-command delete-id ((id playlist)) 551 (delete-id connection (id id))) 553 (defmethod-command delete-id ((id integer)) 554 (check-args unsigned-byte id) 555 (send "deleteid" id)) 557 (defcommand shuffle () 558 "Shuffle the current playlist." 563 (defcommand update (&optional path) 564 "Scan directory for music files and add them to the database." 565 (check-args string path) 566 (send "update" path)) 568 (defcommand find-tracks (type what) 569 "Find tracks in the database with a case sensitive, exact match." 570 (check-args tag-type type) 571 (check-args string what) 572 (parse-list (send "find" type what) 'track)) 574 (defcommand list-metadata (metadata-1 &optional metadata-2 search-term) 575 "List all metadata of `metadata-1'. 576 If `metadata-2' & `search-term' are supplied, 577 then list all `metadata-1' in which `metadata-2' has value `search-term'." 578 (check-args (or string null) search-term) 579 (send "list" metadata-1 metadata-2 search-term)) 581 (defcommand search-tracks (type what) 582 "Find tracks in the database with a case sensitive, inexact match." 583 (check-args tag-type type) 584 (check-args string what) 585 (parse-list (send "search" type what) 'track)) 587 (defcommand list-all-info (&optional path) 588 "Lists all information about files in `path' recursively. Default path is /." 589 (parse-list (send "listallinfo" path) 'track)) 591 (defcommand list-all (&optional path) 592 "Lists all files in `path' recursively. Default path is /." 593 (check-args (or string null) path) 594 (filter-keys (send "listall" path))) 596 (defcommand list-info (&optional path) 597 "Show contents of directory." 598 (check-args (or string null) path) 599 (parse-list (send "lsinfo" path) 'track)) 601 (defcommand count-tracks (scope query) 602 "Number of songs and their total playtime matching `query'. 603 Return: (number playtime)." 604 (check-args string query) 605 (filter-keys (send "count" scope query))) 607 (defcommand tag-types () 608 "Get a list of available metadata types." 609 (filter-keys (send "tagtypes"))) 611 (defcommand url-handlers () 612 "Get a list of available URL handlers." 613 (filter-keys (send "urlhandlers"))) 615 (defun (setf volume) (value connection) 616 "Set the volume to the value between 0-100." 617 (check-type value (integer 0 100) "an integer in range 0-100") 618 (send "setvol" value)) 620 (defun (setf randomized) (value connection) 621 "NIL---turn off random mode, non-nil---turn on random mode." 622 (send "random" (if value 1 0))) 624 (defun (setf repeat) (value connection) 625 "NIL---turn off repeat mode, non-nil---turn on repeat mode." 626 (send "repeat" (if value 1 0))) 628 (defcommand seek (song time) 629 "Skip to a specified point in a song on the playlist." 630 (send "seek" song time)) 632 (defgeneric seek-id (connection song time) 633 (:documentation "Skip to a specified point in a song on the playlist.")) 635 (defmethod-command seek-id ((song playlist) (time integer)) 636 (seek-id connection (id song) time)) 638 (defmethod-command seek-id ((song integer) (time integer)) 639 (check-args unsigned-byte song time) 640 (send "seekid" song time))