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