Christophe Weblog Wiki Code Publications Music
attempt to do the right thing for common cases of unselected players
[squeeze-el.git] / squeeze.el
1 (defgroup squeeze nil
2   "Interact with Squeezebox media servers"
3   :prefix "squeeze-" 
4   :group 'applications)
5
6 (defcustom squeeze-server-address "localhost"
7   "Address for the Squeezebox server"
8   :group 'squeeze)
9 (defcustom squeeze-server-port 9090
10   "Port number for the Squeezebox server"
11   :group 'squeeze)
12 (defcustom squeeze-server-http-port 9000
13   "Port number for the Squeezebox HTTP server"
14   :group 'squeeze)
15 (defvar squeeze-mode-map
16   (let ((map (make-sparse-keymap)))
17     (define-key map (kbd "TAB") 'completion-at-point)
18     map))
19
20 (defun squeeze-unhex-string (string)
21   (with-temp-buffer
22     (let ((case-fold-search t)
23           (start 0))
24       (while (string-match "%[0-9a-f][0-9a-f]" string start)
25         (let* ((s (match-beginning 0))
26                (ch1 (url-unhex (elt string (+ s 1))))
27                (code (+ (* 16 ch1)
28                         (url-unhex (elt string (+ s 2))))))
29           (insert (substring string start s)
30                   (byte-to-string code))
31           (setq start (match-end 0))))
32       (insert (substring string start)))
33     (buffer-string)))
34
35 (defun squeeze-unhex-and-decode-utf8-string (string)
36   (decode-coding-string (squeeze-unhex-string string) 'utf-8))
37
38 (define-derived-mode squeeze-mode comint-mode "Squeeze"
39   "Major mode for interacting with the Squeezebox Server CLI.\\<squeeze-mode-map>"
40   (add-to-list 'completion-at-point-functions 'squeeze-complete-command-at-point)
41   (add-hook 'comint-preoutput-filter-functions 'squeeze-unhex-and-decode-utf8-string nil t)
42   (add-hook 'comint-preoutput-filter-functions 'squeeze-update-state nil t))
43
44 (defvar squeeze-control-mode-map
45   (let ((map (make-sparse-keymap)))
46     (define-key map (kbd "SPC") 'squeeze-control-toggle-power)
47     (define-key map (kbd "f") 'squeeze-control-play-favorite)
48     (define-key map (kbd "g") 'squeeze-control-refresh)
49     (define-key map (kbd "+") 'squeeze-control-volume-up)
50     (define-key map (kbd "-") 'squeeze-control-volume-down)
51     (define-key map (kbd "t") 'squeeze-control-toggle-syncgroup-display)
52     (define-key map (kbd ">") 'squeeze-control-next-track)
53     (define-key map (kbd "<") 'squeeze-control-previous-track)
54     (define-key map (kbd "s") 'squeeze-control-select-player)
55     (define-key map (kbd "a") 'squeeze-list-albums)
56     (define-key map (kbd "!") 'squeeze-control-reconnect)
57     map))
58
59 (defun squeeze-control-current-player ()
60   (or squeeze-control-current-player
61       (if (= (length squeeze-players) 1)
62           (setq squeeze-control-current-player (squeeze-player-playerid (car squeeze-players)))
63         (call-interactively 'squeeze-control-select-player))))
64
65 (defvar squeeze-control-current-player nil)
66
67 (defun squeeze-control-select-player (id)
68   (interactive
69    (list (or (get-text-property (point) 'squeeze-playerid)
70              (let ((name (completing-read "Select player: " (mapcar 'squeeze-player-name squeeze-players))))
71                (squeeze-player-playerid (squeeze-find-player-from-name name))))))
72   (setq squeeze-control-current-player id))
73
74 (defun squeeze-control-next-track ()
75   (interactive)
76   (squeeze-send-string "%s playlist index +1" (squeeze-control-current-player)))
77
78 (defun squeeze-control-previous-track ()
79   (interactive)
80   (squeeze-send-string "%s playlist index -1" (squeeze-control-current-player)))
81
82 (define-derived-mode squeeze-control-mode special-mode "SqueezeControl"
83   "Major mode for controlling Squeezebox Servers.\\<squeeze-control-mode-map>")
84
85 (defvar squeeze-control-inhibit-display nil)
86
87 (lexical-let ((buffer (get-buffer-create " *squeeze-update-state*")))
88   (defun squeeze-update-state (string)
89     ;; FIXME: we could make this a lot more elegant by using the
90     ;; buffer abstraction more
91     (if (cl-position ?\n string)
92         (let (done-something)
93           (with-current-buffer buffer
94             (insert string)
95             (setq string (buffer-string))
96             (erase-buffer))
97           (dolist (line (split-string string "\n"))
98             (when (squeeze-update-state-from-line line)
99               (setq done-something t)))
100           (when done-something
101             (unless squeeze-control-inhibit-display
102               (squeeze-control-display-players))))
103       (with-current-buffer buffer
104         (insert string)))
105     string))
106
107 (defconst squeeze-player-line-regexp
108   "^\\(\\(?:[0-9a-f]\\{2\\}%3A\\)\\{5\\}[0-9a-f]\\{2\\}\\) ")
109
110 (defun squeeze-find-player (id)
111   (dolist (player squeeze-players)
112     (when (string= id (squeeze-player-playerid player))
113       (return player))))
114
115 (defun squeeze-find-player-from-name (name)
116   (dolist (player squeeze-players)
117     (when (string= name (squeeze-player-name player))
118       (return player))))
119
120 (defun squeeze-update-power (player state)
121   (if state
122       (setf (squeeze-player-power player) state)
123     (let ((current (squeeze-player-power player)))
124       (setf (squeeze-player-power player)
125             (cond ((string= current "0") "1")
126                   ((string= current "1") "0"))))))
127
128 (defun squeeze-update-mixer-volume (player value)
129   (let ((current (squeeze-player-volume player))
130         (number (string-to-number value)))
131     (if (string-match "^[-+]" value)
132         (setf (squeeze-player-volume player)
133               (and current (max 0 (min 100 (+ current number)))))
134       (setf (squeeze-player-volume player) number))))
135
136 (require 'notifications)
137
138 (defun squeeze-update-state-from-line (string)
139   (cond
140    ((string-match "^players 0" string)
141     (setq squeeze-players (squeeze-parse-players-line string))
142     t)
143    ((string-match "^syncgroups" string)
144     (setq squeeze-syncgroups (squeeze-parse-syncgroups-line string))
145     t)
146    ((string-match "^albums" string)
147     (squeeze-parse-albums-line string)
148     t)
149    ((string-match squeeze-player-line-regexp string)
150     (let ((substring (substring string (match-end 0)))
151           (id (url-unhex-string (match-string 1 string))))
152       (cond
153        ((string-match "^playlist newsong \\(.*\\) \\([0-9]+\\)$" substring)
154         (let ((value (save-match-data (url-unhex-string (match-string 1 substring))))
155               (index (url-unhex-string (match-string 2 substring))))
156           (notifications-notify :title "Now playing" :body (encode-coding-string (format "%s: %s" index value) 'utf-8)))
157         t)
158        ((string-match "^power\\(?: \\([01]\\)\\)?" substring)
159         (let ((state (match-string 1 substring))
160               (player (squeeze-find-player id)))
161           (squeeze-update-power player state))
162         t)
163        ((string-match "^mixer volume \\(\\(?:-\\|%2B\\)?[0-9]*\\)" substring)
164         (let ((value (url-unhex-string (match-string 1 substring)))
165               (player (squeeze-find-player id)))
166           (squeeze-update-mixer-volume player value))
167         t))))))
168
169 (defface squeeze-player-face
170   '((t))
171   "Face for displaying players"
172   :group 'squeeze)
173 (defface squeeze-player-on-face
174   '((t :weight bold :inherit squeeze-player-face))
175   "Face for displaying players which are on"
176   :group 'squeeze)
177 (defface squeeze-player-off-face
178   '((t :weight light :inherit squeeze-player-face))
179   "Face for displaying players which are off"
180   :group 'squeeze)
181
182 (defface squeeze-mixer-face
183   '((t :weight bold))
184   "Face for displaying mixer information"
185   :group 'squeeze)
186 (defface squeeze-mixer-muted-face
187   '((t :weight light :inherit squeeze-mixer-face))
188   "Face for displaying mixer information when muted"
189   :group 'squeeze)
190 (defface squeeze-mixer-quiet-face
191   '((t :foreground "green3" :inherit squeeze-mixer-face))
192   "Face for quiet volume"
193   :group 'squeeze)
194 (defface squeeze-mixer-medium-face
195   '((t :foreground "gold" :inherit squeeze-mixer-face))
196   "Face for medium volume"
197   :group 'squeeze)
198 (defface squeeze-mixer-loud-face
199   '((t :foreground "OrangeRed1" :inherit squeeze-mixer-face))
200   "Face for loud volume"
201   :group 'squeeze)
202 (defface squeeze-mixer-muted-quiet-face
203   '((t :inherit (squeeze-mixer-muted-face squeeze-mixer-quiet-face)))
204   "Face for quiet volume when muted")
205 (defface squeeze-syncgroup-face
206   '((t :slant italic))
207   "Face for syncgroups"
208   :group 'squeeze)
209
210 (defun squeeze-mixer-compute-bar (vol width)
211   (let* ((exact (* width (/ vol 100.0)))
212          (nfull (floor exact))
213          (frac (- exact nfull))
214          (nblank (floor (- width exact))))
215     (format "%s%s%s"
216             (make-string nfull ?█)
217             (if (= width (+ nfull nblank))
218                 ""
219               (string (aref " ▏▎▍▌▋▊▉█" (floor (+ frac 0.0625) 0.125))))
220             (make-string nblank ? ))))
221
222 (defun squeeze-mixer-make-bar (vol width)
223   (let ((bar (squeeze-mixer-compute-bar vol width))
224         (lo (floor (* 0.65 width)))
225         (hi (floor (* 0.9 width))))
226     (concat "▕"
227             (propertize (substring bar 0 lo) 'face 'squeeze-mixer-quiet-face)
228             (propertize (substring bar lo hi) 'face 'squeeze-mixer-medium-face)
229             (propertize (substring bar hi) 'face 'squeeze-mixer-loud-face)
230             (propertize "▏" 'intangible t))))
231
232 (defvar squeeze-players ())
233 (defvar squeeze-syncgroups ())
234
235 (defun squeeze-send-string (control &rest arguments)
236   (let* ((process (get-buffer-process "*squeeze*"))
237          (string (apply #'format control arguments))
238          (length (length string)))
239     (unless (and (> length 0) (char-equal (aref string (1- length)) ?\n))
240       (setq string (format "%s\n" string)))
241     (if process
242         (comint-send-string process string)
243       (error "can't find squeeze process"))))
244
245 (defun squeeze-control-query-syncgroups ()
246   (interactive)
247   (squeeze-send-string "syncgroups ?"))
248
249 (defun squeeze-control-query-players ()
250   (interactive)
251   (squeeze-send-string "players 0"))
252
253 (defun squeeze-control-toggle-power (&optional id)
254   (interactive)
255   (unless id
256     (setq id (get-text-property (point) 'squeeze-playerid)))
257   (squeeze-send-string "%s power" id))
258
259 (defun squeeze-control-play-favorite (&optional favorite id)
260   (interactive "nFavourite: ")
261   (unless id
262     (setq id (get-text-property (point) 'squeeze-playerid)))
263   (squeeze-send-string "%s favorites playlist play item_id:%d" id favorite))
264
265 (defun squeeze-control-query-power (&optional id)
266   (interactive)
267   (unless id
268     (setq id (get-text-property (point) 'squeeze-playerid)))
269   (when id
270     (squeeze-send-string "%s power ?" id)))
271
272 (defun squeeze-control-volume-up (&optional id inc)
273   (interactive)
274   (unless inc (setq inc 5))
275   (unless id
276     (setq id (get-text-property (point) 'squeeze-playerid)))
277   (when id
278     (squeeze-send-string "%s mixer volume %+d" id inc)))
279
280 (defun squeeze-control-volume-down (&optional id inc)
281   (interactive)
282   (unless inc (setq inc 5))
283   (unless id
284     (setq id (get-text-property (point) 'squeeze-playerid)))
285   (when id
286     (squeeze-send-string "%s mixer volume %+d" id (- inc))))
287
288 (defun squeeze-control-volume-set (id val)
289   (interactive)
290   (squeeze-send-string "%s mixer volume %d" id val))
291
292 (defun squeeze-control-query-mixer-volume (&optional id)
293   (interactive)
294   (unless id
295     (setq id (get-text-property (point) 'squeeze-playerid)))
296   (when id
297     (squeeze-send-string "%s mixer volume ?" id)))
298
299 (defun squeeze-control-player-face (player)
300   (let ((power (squeeze-player-power player)))
301     (cond ((string= power "1") 'squeeze-player-on-face)
302           ((string= power "0") 'squeeze-player-off-face)
303           (t 'squeeze-player-face))))
304
305 (defun squeeze-control-listen ()
306   (squeeze-send-string "listen 1"))
307
308 (defun squeeze-accept-process-output ()
309   (while (accept-process-output (get-buffer-process "*squeeze*") 0.1 nil t)))
310
311 (defun squeeze-control-refresh ()
312   (interactive)
313   (let ((squeeze-control-inhibit-display t))
314     (squeeze-control-query-players)
315     (squeeze-accept-process-output)
316     (squeeze-control-query-syncgroups)
317     (dolist (player squeeze-players)
318       (squeeze-control-query-power (squeeze-player-playerid player))
319       (squeeze-control-query-mixer-volume (squeeze-player-playerid player))))
320   (squeeze-accept-process-output)
321   (squeeze-control-display-players))
322
323 (defvar squeeze-control-mixer-map
324   (let ((map (make-sparse-keymap)))
325     (define-key map (kbd "RET") 'squeeze-control-mixer-set-volume)
326     (define-key map [mouse-1] 'squeeze-control-mixer-mouse-1)
327     map))
328
329 (defun squeeze-control-compute-volume (pos)
330   (let* ((end (next-single-property-change pos 'keymap))
331          (start (previous-single-property-change end 'keymap)))
332     (/ (* 100 (- (point) start)) (- end start 1))))
333
334 (defun squeeze-control-mixer-mouse-1 (event)
335   (interactive "e")
336   (let* ((pos (cadadr event))
337          (val (squeeze-control-compute-volume pos))
338          (id (get-text-property pos 'squeeze-playerid)))
339     (squeeze-control-volume-set id val)))
340
341 (defun squeeze-control-mixer-set-volume ()
342   (interactive)
343   (let* ((val (squeeze-control-compute-volume (point)))
344          (id (get-text-property (point) 'squeeze-playerid)))
345     (squeeze-control-volume-set id val)))
346
347 (defvar squeeze-control-display-syncgroups nil)
348
349 (defun squeeze-control-toggle-syncgroup-display ()
350   (interactive)
351   (setf squeeze-control-display-syncgroups
352         (not squeeze-control-display-syncgroups))
353   (squeeze-control-display-players))
354
355 (defun squeeze-control-insert-player (player)
356   (insert (propertize (format "%20s" (squeeze-player-name player))
357                       'face (squeeze-control-player-face player)
358                       'squeeze-playerid (squeeze-player-playerid player)))
359   (when (squeeze-player-volume player)
360     (insert (propertize
361              (squeeze-mixer-make-bar (squeeze-player-volume player) 28)
362              'squeeze-playerid (squeeze-player-playerid player)
363              'keymap squeeze-control-mixer-map
364              'pointer 'hdrag
365              'rear-nonsticky '(keymap))))
366   (insert (propertize "\n" 'intangible t)))
367
368 (defun squeeze-control-display-players ()
369   (interactive)
370   (with-current-buffer (get-buffer-create "*squeeze-control*")
371     (let ((saved (point)))
372       (squeeze-control-mode)
373       (read-only-mode -1)
374       (erase-buffer)
375       (cond
376        (squeeze-control-display-syncgroups
377         (let ((syncgroups squeeze-syncgroups)
378               (seen))
379           (while syncgroups
380             (let ((names (getf syncgroups :sync_member_names))
381                   ;; new server version has sync_members and sync_member_names
382                   (members (split-string (getf syncgroups :sync_members) ",")))
383               (insert (propertize names 'face 'squeeze-syncgroup-face) "\n")
384               (dolist (member members)
385                 (let ((player (squeeze-find-player member)))
386                   (squeeze-control-insert-player player)
387                   (push player seen))))
388             (setq syncgroups (cddddr syncgroups)))
389           (insert (propertize "No syncgroup" 'face 'squeeze-syncgroup-face) "\n")
390           (dolist (player squeeze-players)
391             (unless (member player seen)
392               (squeeze-control-insert-player player)))))
393        (t
394         (dolist (player squeeze-players)
395           (squeeze-control-insert-player player))
396         (read-only-mode 1)))
397       (goto-char saved))))
398
399 (cl-defstruct (squeeze-player (:constructor squeeze-make-player))
400   playerindex playerid uuid ip name model isplayer displaytype canpoweroff connected power volume)
401
402 (defun squeeze-string-plistify (string start end)
403   (unless end
404     (setq end (length string)))
405   (save-match-data
406     (let (result)
407       (loop
408        (if (string-match "\\([a-z_]+\\)%3A\\([^ \n]+\\)" string start)
409            (let ((mend (match-end 0)))
410              (when (> mend end)
411                (return))
412              (push (intern (format ":%s" (substring string (match-beginning 1) (match-end 1)))) result)
413              (push (decode-coding-string
414                     (url-unhex-string (substring string (match-beginning 2) (match-end 2)))
415                     'utf-8)
416                    result)
417              (setq start mend))
418          (return)))
419       (nreverse result))))
420
421 (defun squeeze-parse-syncgroups-line (string)
422   (let ((syncgroupspos (string-match "^syncgroups " string))
423         (startpos (match-end 0)))
424     (when startpos
425       (squeeze-string-plistify string startpos (length string)))))
426
427 (defun squeeze-parse-count (string)
428   (save-match-data
429     (let ((countpos (string-match "count%3A\\([0-9]*\\)\\>" string)))
430       (if countpos
431           (string-to-number
432            (substring string (match-beginning 1) (match-end 1)))
433         (let ((kind
434                (progn (string-match "^\\([a-z]*\\) " string)
435                       (substring string (match-beginning 1) (match-end 1)))))
436           (message "no count found in %s line" kind)
437           nil)))))
438
439 (defun squeeze-parse-players-line (string)
440   (let ((count (squeeze-parse-count string))
441         (startpos (string-match "playerindex" string))
442         result endpos)
443     (when (> count 0)
444       (dotimes (i (1- count))
445         (setq endpos (progn (string-match " connected%3A[0-1] " string startpos)
446                             (match-end 0)))
447         (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos endpos)) result)
448         (setq startpos endpos))
449       (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos (length string))) result))
450     result))
451
452 (defcustom squeeze-artwork-directory "~/.emacs.d/squeeze/artwork/"
453   "Base directory for album and track artwork")
454
455 (defvar squeeze-albums nil)
456
457 (cl-defstruct (squeeze-album (:constructor squeeze-make-album))
458   id album artwork_track_id artist index)
459
460 (defun squeeze-parse-albums-line (string)
461   (let ((count (squeeze-parse-count string))
462         (countpos (string-match "\\_<count%3a" string))
463         (start-index-pos (progn (string-match "^albums \\([0-9]+\\)\\>" string)
464                                 (match-beginning 1)))
465         index start end)
466     (unless squeeze-albums
467       (setq squeeze-albums (make-vector count nil)))
468     (when start-index-pos
469       (setq index (string-to-number (substring string start-index-pos)))
470       (setq start (string-match "\\_<id%3a" string start-index-pos))
471       (while start
472         (setq end (string-match "\\_<\\(id%3a\\|count%3a\\)" string (1+ start)))
473         (aset squeeze-albums index
474               (apply 'squeeze-make-album
475                      :index index
476                      (squeeze-string-plistify string start end)))
477         (incf index)
478         (setq start (if (= end countpos) nil end))))))
479
480 (defun squeeze-get-albums ()
481   (squeeze-send-string "albums 0 1000 tags:lja")
482   (squeeze-accept-process-output))
483
484 (defvar squeeze-albums-mode-map
485   (let ((map (make-sparse-keymap)))
486     (define-key map (kbd "RET") 'squeeze-albums-load-album)
487     map))
488
489 (define-derived-mode squeeze-albums-mode tabulated-list-mode "SqueezeAlbums"
490   "Major mode for displaying Albums from Squeezebox Servers.\\<squeeze-albums-mode-map>"
491   (setq tabulated-list-format [("Cover" 10 nil) ("Title" 30 t)])
492   (add-hook 'tabulated-list-revert-hook 'squeeze-get-albums nil t)
493   (setq tabulated-list-entries 'squeeze-albums-tabulated-list-entries)
494   (tabbar-mode -1)
495   (setq tabulated-list-use-header-line nil)
496   (tabulated-list-init-header))
497
498 (defun squeeze-albums-artwork-callback (status artwork_track_id filename)
499   (unless status
500     (goto-char 1)
501     (let ((end (search-forward "\n\n")))
502       (delete-region 1 end)
503       (write-file (format "%s%s/%s" squeeze-artwork-directory artwork_track_id filename))
504       (kill-buffer))))
505
506 (defun squeeze-albums-tabulated-list-entry (x)
507   (let* ((ati (squeeze-album-artwork_track_id x))
508          (file (format "%s%s/cover_50x50_o.jpg" squeeze-artwork-directory ati)))
509     (unless (file-exists-p file)
510       (make-directory (format "%s%s" squeeze-artwork-directory ati) t)
511       (let ((url (format "http://%s:%s/music/%s/cover_50x50_o.jpg"
512                          squeeze-server-address squeeze-server-http-port ati)))
513         (url-queue-retrieve url 'squeeze-albums-artwork-callback
514                             (list ati "cover_50x50_o.jpg") t t)))
515     (list x (vector (propertize (format "%s" ati)
516                                 'display `(when (file-exists-p ,file)
517                                             image :type jpeg :file ,file))
518                     (squeeze-album-album x)))))
519
520 (defun squeeze-albums-tabulated-list-entries ()
521   (mapcar 'squeeze-albums-tabulated-list-entry (append squeeze-albums nil)))
522
523 (defun squeeze-complete-command-at-point ()
524   (save-excursion
525     (list (progn (backward-word) (point))
526           (progn (forward-word) (point))
527           (append
528            (mapcar 'squeeze-player-playerid squeeze-players)
529            '(;; General commands and queries
530              "login" "can" "version" "listen" "subscribe" "pref"
531              "logging" "getstring" "setsncredentials" "debug"
532              "exit" "shutdown"
533
534              ;; Player commands and queries
535              "player" "count" "id" "uuid" "name" "ip" "model" "isplayer"
536              "displaytype" "canpoweroff" "?" "signalstrength" "connected"
537              "sleep" "sync" "syncgroups" "power" "mixer" "volume" "muting"
538              "bass" "treble" "pitch" "show" "display" "linesperscreen"
539              "displaynow" "playerpref" "button" "ir" "irenable"
540              "connect" "client" "forget" "disconnect" "players"
541
542              ;; Database commands and queries
543              "rescan" "rescanprogress" "abortscan" "wipecache" "info"
544              "total" "genres" "artists" "albums" "songs" "years"
545              "musicfolder" "playlists" "tracks" "new" "rename" "delete"
546              "edit" "songinfo" "titles" "search" "pragma"
547
548              ;; Playlist commands and queries
549              "play" "stop" "pause" "mode" "time" "genre" "artist" "album"
550              "title" "duration" "remote" "current_title" "path" "playlist"
551              "add" "insert" "deleteitem" "move" "delete" "preview" "resume"
552              "save" "loadalbum" "addalbum" "loadtracks" "addtracks"
553              "insertalbum" "deletealbum" "clear" "zap" "name" "url"
554              "modified" "playlistsinfo" "index" "shuffle" "repeat"
555              "playlistcontrol"
556
557              ;; Compound queries
558              "serverstatus" "status" "displaystatus" "readdirectory"
559
560              ;; Notifications
561
562              ;; Alarm commands and queries
563              "alarm" "alarms"
564
565              ;; Plugins commands and queries
566              "favorites"
567              )))))
568
569 (defun squeeze-read-server-parameters (address port)
570   (let ((host (read-string "Host: " nil nil address))
571         (port (read-number "Port: " port)))
572     (cons host port)))
573
574 (cl-defmacro with-squeeze-parameters ((address port) &body body)
575   (declare (indent 1))
576   `(progn
577      (unless ,address (setq ,address squeeze-server-address))
578      (unless ,port (setq ,port squeeze-server-port))
579      (when current-prefix-arg
580        (let ((parameters (squeeze-read-server-parameters ,address ,port)))
581          (setq ,address (car parameters)
582                ,port (cdr parameters))))
583      ,@body))
584
585 (defun squeeze (&optional address port)
586   (interactive)
587   (with-squeeze-parameters (address port)
588     (let ((buffer (make-comint-in-buffer "squeeze" nil (cons address port))))
589       (switch-to-buffer buffer)
590       (squeeze-mode))))
591
592 (defun squeeze-control (&optional address port)
593   (interactive)
594   (with-squeeze-parameters (address port)
595     (let ((current-prefix-arg nil))
596       (squeeze address port))
597     (let ((buffer (get-buffer-create "*squeeze-control*")))
598       (switch-to-buffer buffer)
599       (squeeze-control-listen)
600       (squeeze-control-refresh)
601       (squeeze-control-display-players))))
602
603 (defun squeeze-control-reconnect (&optional address port)
604   (interactive)
605   (with-squeeze-parameters (address port)
606     (kill-buffer (get-buffer "*squeeze*"))
607     (let ((current-prefix-arg nil))
608       (squeeze-control address port))))
609
610 (defun squeeze-list-albums ()
611   (interactive)
612   (squeeze-get-albums)
613   (let ((buffer (get-buffer-create "*squeeze-albums*")))
614     (switch-to-buffer buffer)
615     (squeeze-albums-mode)
616     (tabulated-list-print)))
617
618 (defun squeeze-albums-load-album ()
619   (interactive)
620   (squeeze-send-string "%s playlistcontrol cmd:load album_id:%s"
621                        (squeeze-control-current-player)
622                        (squeeze-album-id (tabulated-list-get-id))))
623
624 (provide 'squeeze)