changelog shortlog graph tags branches changeset files revisions annotate raw help

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
2 
3 ;; based on https://github.com/stassats/mpd
4 
5 ;;; Commentary:
6 
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
10 ;; slightly.
11 
12 
13 ;;; Code:
14 (in-package :aud/mpd)
15 ;;; Classes
16 (define-condition mpd-error (error)
17  ((text :initarg :text :reader text
18  :initform nil))
19  (:report (lambda (condition stream)
20  (princ (text condition) stream))))
21 
22 (macrolet ((define-conditions (names)
23  `(progn ,@(mapcar
24  (lambda (name)
25  `(define-condition ,name (mpd-error) ()))
26  names))))
27  (define-conditions (bad-argument incorrect-password
28  not-permitted unknown-command not-exist
29  playlist-size-exceed already-updating exist)))
30 
31 (defparameter *error-ids-alist*
32  '((2 . bad-argument)
33  (3 . incorrect-password)
34  (4 . not-permitted)
35  (5 . unknown-command)
36  (50 . not-exist)
37  (51 . playlist-size-exceed)
38  (54 . already-updating)
39  (56 . exist)))
40 
41 (eval-always
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'"))
46 
47 (deftype tag-type ()
48  `(member ,@*tag-types*))
49 
50 (defclass track ()
51  ((file
52  :initform nil :initarg :file :accessor file)
53  (title
54  :initform nil :initarg :title :accessor title)
55  (artist
56  :initform nil :initarg :artist :accessor artist)
57  (albumartist
58  :initform nil :initarg :albumartist :accessor albumartist)
59  (album
60  :initform nil :initarg :album :accessor album)
61  (genre
62  :initform nil :initarg :genre :accessor genre)
63  (date
64  :initform nil :initarg :date :accessor date)
65  (performer
66  :initform nil :initarg :performer :accessor performer)
67  (composer
68  :initform nil :initarg :composer :accessor composer)
69  (disc
70  :initform nil :initarg :disc :accessor disc)
71  (track
72  :initform nil :initarg :track :accessor track-number)
73  (time
74  :initform nil :initarg :time :accessor duration)
75  (last-modified
76  :initform nil :initarg :last-modified :accessor last-modified)))
77 
78 (defclass playlist (track)
79  ((pos
80  :initform 0 :initarg :pos :accessor position-in-playlist
81  :type integer)
82  (duration
83  :initform nil :initarg :duration)
84  (format :initform nil :initarg :format)
85  (id
86  :initform 0 :initarg :id :accessor id
87  :type integer)))
88 
89 (defclass status ()
90  ((volume
91  :reader volume :initarg :volume :initform nil)
92  (repeat
93  :reader repeat :initarg :repeat :initform nil)
94  (random
95  :reader randomized :initarg :random :initform nil)
96  (playlist
97  :reader playlist-version :initarg :playlist :initform nil)
98  (playlist-length
99  :reader playlist-length :initarg :playlistlength :initform nil)
100  (xfade
101  :reader xfade :initarg :xfade :initform nil)
102  (state
103  :reader state :initarg :state :initform nil)
104  (partition
105  :reader partition :initarg :partition :initform nil)
106  (audio
107  :reader audio :initarg :audio :initform nil)
108  (bitrate
109  :reader bitrate :initarg :bitrate :initform nil)
110  (duration
111  :reader duration :initarg :duration :initform nil)
112  (time
113  :reader %time :initarg :time :initform nil)
114  (songid
115  :reader songid :initarg :songid :initform nil)
116  (song :reader song :initarg :song :initform nil)
117  (nextsongid
118  :reader nextsongid :initarg :nextsongid :initform nil)
119  (nextsong
120  :reader nextsong :initarg :nextsong :initform nil)
121  (elapsed
122  :reader elapsed :initarg :elapsed :initform nil)
123  (mixrampdb
124  :reader mixrampdb :initarg :mixrampdb :initform nil)
125  (consume
126  :reader consume :initarg :consume :initform nil)
127  (single
128  :reader single :initarg :single :initform nil)))
129 
130 (defclass stats ()
131  ((artists
132  :reader artists :initarg :artists :initform nil)
133  (albums
134  :reader albums :initarg :albums :initform nil)
135  (songs
136  :reader songs :initarg :songs :initform nil)
137  (uptime
138  :reader uptime :initarg :uptime :initform nil)
139  (playtime
140  :reader playtime :initarg :playtime :initform nil)
141  (db-playtime
142  :reader db-playtime :initarg :db_playtime :initform nil)
143  (db-update
144  :reader db-update :initarg :db_update :initform nil)))
145 
146 (macrolet ((generate-commands (class names)
147  `(progn
148  ,@(mapcar (lambda (name)
149  `(defmethod ,name ((stream socket))
150  (,name (,class stream))))
151  names))))
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)))
157 
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
162  :outputid)
163  "List of keys which values must be integers.")
164 
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))
169 
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))))
174 
175 ;;; MPD
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))
178 
179 (defun connect (&key (host *default-host*) (port *default-port*) password)
180  "Connect to MPD."
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)))
184  (when password
185  (password connection password)))))
186 
187 (defun read-answer (stream)
188  (loop for line = (read-line stream)
189  until (string= line "OK" :end1 2)
190  collect line
191  when (string= line "ACK" :end1 3)
192  do (throw-error line)))
193 
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)))))
200 
201 (eval-always
202  (defmacro with-mpc ((var &rest options) &body body)
203  `(let ((,var (connect ,@options)))
204  (unwind-protect
205  (progn ,@body)
206  (disconnect ,var)))))
207 
208 (defun ensure-mpd ()
209  (handler-case
210  (with-mpc (c) t)
211  (not-exist () (sb-ext:run-program "mpd" nil :search t :directory (user-homedir-pathname) :wait nil))))
212 
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)))
221 
222 ;;; Parsing
223 
224 (defun to-keyword (name)
225  (intern (string-upcase name) :keyword))
226 
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)))))
232 
233 (defun split-values (strings)
234  "Transform a list of strings 'key: value' into the plist."
235  (mapcan #'split-value strings))
236 
237 (defun process-value (key value)
238  (list key
239  (funcall (value-processing-function key) value)))
240 
241 (defun value-processing-function (key)
242  (if (member key *integer-keys*)
243  #'parse-integer
244  (getf *value-processing-functions* key #'identity)))
245 
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))
251  first
252  (list first
253  (parse-integer time :start (1+ stop))))))
254 
255 (defun string-not-zerop (string)
256  (not (string= string "0")))
257 
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))))
262  strings))
263 
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)))
268 
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."
272  (let (track)
273  (flet ((create-track ()
274  (when track
275  (list (apply 'make-instance class track)))))
276  (nconc
277  (mapcan (lambda (x)
278  (let ((pair (split-value x)))
279  (case (car pair)
280  (:file (prog1 (create-track)
281  (setf track pair)))
282  ((:directory :playlist)
283  (list pair))
284  (t (nconc track pair)
285  nil))))
286  list)
287  (create-track)))))
288 
289 ;;;
290 
291 (defun process-string (string)
292  "Check for emtpy strings, and escape strings when needed."
293  (when string
294  (let ((string
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)
300  string))))
301 
302 ;;; Macros
303 
304 (defmacro send (&rest commands)
305  "Macro for using inside `defcommand'."
306  `(send-command connection
307  (format nil "~{~A~^ ~}"
308  (remove nil (list ,@commands)))))
309 
310 (defmacro defcommand (name parameters &body body)
311  `(defun ,name (connection ,@parameters)
312  ,@body))
313 
314 (defmacro defmethod-command (name parameters &body body)
315  `(defmethod ,name (connection ,@parameters)
316  ,@body))
317 
318 (defmacro check-args (type &rest args)
319  "Check string and integer arguments."
320  (if (or (eq type 'string)
321  (and (listp type)
322  (member 'string type)))
323  `(progn ,@(mapcan
324  (lambda (arg)
325  `((check-type ,arg ,type "a string")
326  (setf ,arg (process-string ,arg))))
327  args))
328  `(progn ,@(mapcar
329  (lambda (arg)
330  `(check-type ,arg ,type))
331  args))))
332 
333 ;;; Commands
334 (defcommand password (password)
335  "Authentication."
336  (check-args string password)
337  (send "password" password))
338 
339 (defcommand disconnect ()
340  "Close connection."
341  (socket-close connection))
342 
343 (defcommand now-playing ()
344  "Return instance of playlist with current song."
345  (let ((track (send "currentsong")))
346  (when track
347  (make-class track 'playlist))))
348 
349 (defcommand disable-output (id)
350  (check-args unsigned-byte id)
351  (send "disableoutput" id))
352 
353 (defcommand enable-output (id)
354  (check-args unsigned-byte id)
355  (send "enableoutput" id))
356 
357 (defcommand ping ()
358  "Send ping to MPD."
359  (send "ping"))
360 
361 (defcommand kill ()
362  "Stop MPD in a safe way."
363  (send "kill"))
364 
365 (defcommand status ()
366  "Return status of MPD."
367  (make-class (send "status") 'status))
368 
369 (defcommand stats ()
370  "Return statisics."
371  (make-class (send "stats") 'stats))
372 
373 (defcommand outputs ()
374  "Return information about all outputs."
375  (split-values (send "outputs")))
376 
377 (defcommand commands ()
378  "Return list of available commands."
379  (filter-keys (send "commands")))
380 
381 (defcommand not-commands ()
382  "Return list of commands to which the current user does not have access."
383  (filter-keys
384  (send "notcommands")))
385 
386 ;;; Control
387 
388 (defcommand pause ()
389  "Toggle pause / resume playing."
390  (send "pause"))
391 
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))
396 
397 (defcommand stop ()
398  "Stop playing."
399  (send "stop"))
400 
401 (defcommand next ()
402  "Play next track in the playlist."
403  (send "next"))
404 
405 (defcommand previous ()
406  "Play previous track in the playlist."
407  (send "previous"))
408 
409 (defcommand crossfade (seconds)
410  (check-args unsigned-byte seconds)
411  "Sets crossfading between songs."
412  (send "crossfade" seconds))
413 
414 ;; Playlist
415 
416 (defcommand list-playlist (name)
417  "List files in the playlist `name'"
418  (check-args string name)
419  (filter-keys (send "listplaylist" name)))
420 
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))
425 
426 (defcommand clear ()
427  "Clear the current playlist."
428  (send "clear"))
429 
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))
434 
435 (defcommand load-playlist (filename)
436  "Load playlist from file."
437  (check-args string filename)
438  (send "load" filename))
439 
440 (defcommand rename-playlist (name new-name)
441  "Rename playlist."
442  (check-args string name new-name)
443  (unless (equal name new-name)
444  (send "rename" name new-name)))
445 
446 (defcommand playlist-info (&optional id)
447  "Return content of the current playlist."
448  (check-args (or unsigned-byte null) id)
449  (if id
450  (make-class (send "playlistinfo" id) 'playlist)
451  (parse-list (send "playlistinfo") 'playlist)))
452 
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))
457 
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))
462 
463 (defcommand clear-playlist (name)
464  "Clear playlist `name'."
465  (check-args string name)
466  (send "playlistclear"))
467 
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))
473 
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))
479 
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))
484 
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))
489 
490 (defgeneric add (connection what)
491  (:documentation "Add file or directory to the current playlist."))
492 
493 (defmethod-command add ((what track))
494  (add connection (file what)))
495 
496 (defmethod-command add ((what string))
497  (check-args string what)
498  (send "add" what))
499 
500 (defgeneric add-id (connection what)
501  (:documentation "Like add, but returns a id."))
502 
503 (defmethod-command add-id ((what track))
504  (add connection (file what)))
505 
506 (defmethod-command add-id ((what string))
507  (check-args string what)
508  (car (filter-keys (send "addid" what))))
509 
510 (defcommand move (from to)
511  "Move track from `from' to `to' in the playlist."
512  (check-args unsigned-byte from to)
513  (unless (= from to)
514  (send "move" from to)))
515 
516 (defgeneric move-id (connection id to)
517  (:documentation "Move track with `id' to `to' in the playlist."))
518 
519 (defmethod-command move-id ((track playlist) (to integer))
520  (move-id connection (id track) to))
521 
522 (defmethod-command move-id ((id integer) (to integer))
523  (check-args unsigned-byte id to)
524  (send "moveid" id to))
525 
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)))
531 
532 (defgeneric swap-id (connection first second)
533  (:documentation "Swap positions of two tracks by id."))
534 
535 (defmethod-command swap-id ((first playlist) (second playlist))
536  (swap-id connection (id first) (id second)))
537 
538 (defmethod-command swap-id ((first integer) (second integer))
539  (check-args unsigned-byte first second)
540  (send "swap" first second))
541 
542 (defcommand delete-track (number)
543  "Delete track from playlist."
544  (check-args unsigned-byte number)
545  (send "delete" number))
546 
547 (defgeneric delete-id (connection id)
548  (:documentation "Delete track with `id' from playlist."))
549 
550 (defmethod-command delete-id ((id playlist))
551  (delete-id connection (id id)))
552 
553 (defmethod-command delete-id ((id integer))
554  (check-args unsigned-byte id)
555  (send "deleteid" id))
556 
557 (defcommand shuffle ()
558  "Shuffle the current playlist."
559  (send "shuffle"))
560 
561 ;;; Database
562 
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))
567 
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))
573 
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))
580 
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))
586 
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))
590 
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)))
595 
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))
600 
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)))
606 
607 (defcommand tag-types ()
608  "Get a list of available metadata types."
609  (filter-keys (send "tagtypes")))
610 
611 (defcommand url-handlers ()
612  "Get a list of available URL handlers."
613  (filter-keys (send "urlhandlers")))
614 
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))
619 
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)))
623 
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)))
627 
628 (defcommand seek (song time)
629  "Skip to a specified point in a song on the playlist."
630  (send "seek" song time))
631 
632 (defgeneric seek-id (connection song time)
633  (:documentation "Skip to a specified point in a song on the playlist."))
634 
635 (defmethod-command seek-id ((song playlist) (time integer))
636  (seek-id connection (id song) time))
637 
638 (defmethod-command seek-id ((song integer) (time integer))
639  (check-args unsigned-byte song time)
640  (send "seekid" song time))