Christophe Weblog Wiki Code Publications Music
attempt to do the right thing for common cases of unselected players
[squeeze-el.git] / squeeze.el
index 88bcc3a74ce0b452f99aa17149259400e5325a6e..3808a20f117ddbd86028122c7061c4a873336453 100644 (file)
 (defcustom squeeze-server-port 9090
   "Port number for the Squeezebox server"
   :group 'squeeze)
-
+(defcustom squeeze-server-http-port 9000
+  "Port number for the Squeezebox HTTP server"
+  :group 'squeeze)
 (defvar squeeze-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "TAB") 'completion-at-point)
     map))
 
+(defun squeeze-unhex-string (string)
+  (with-temp-buffer
+    (let ((case-fold-search t)
+          (start 0))
+      (while (string-match "%[0-9a-f][0-9a-f]" string start)
+        (let* ((s (match-beginning 0))
+               (ch1 (url-unhex (elt string (+ s 1))))
+               (code (+ (* 16 ch1)
+                        (url-unhex (elt string (+ s 2))))))
+          (insert (substring string start s)
+                  (byte-to-string code))
+          (setq start (match-end 0))))
+      (insert (substring string start)))
+    (buffer-string)))
+
+(defun squeeze-unhex-and-decode-utf8-string (string)
+  (decode-coding-string (squeeze-unhex-string string) 'utf-8))
+
 (define-derived-mode squeeze-mode comint-mode "Squeeze"
   "Major mode for interacting with the Squeezebox Server CLI.\\<squeeze-mode-map>"
-  (add-hook 'comint-preoutput-filter-functions 'url-unhex-string nil t)
+  (add-to-list 'completion-at-point-functions 'squeeze-complete-command-at-point)
+  (add-hook 'comint-preoutput-filter-functions 'squeeze-unhex-and-decode-utf8-string nil t)
   (add-hook 'comint-preoutput-filter-functions 'squeeze-update-state nil t))
 
 (defvar squeeze-control-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "SPC") 'squeeze-control-toggle-power)
+    (define-key map (kbd "f") 'squeeze-control-play-favorite)
     (define-key map (kbd "g") 'squeeze-control-refresh)
+    (define-key map (kbd "+") 'squeeze-control-volume-up)
+    (define-key map (kbd "-") 'squeeze-control-volume-down)
+    (define-key map (kbd "t") 'squeeze-control-toggle-syncgroup-display)
+    (define-key map (kbd ">") 'squeeze-control-next-track)
+    (define-key map (kbd "<") 'squeeze-control-previous-track)
+    (define-key map (kbd "s") 'squeeze-control-select-player)
+    (define-key map (kbd "a") 'squeeze-list-albums)
+    (define-key map (kbd "!") 'squeeze-control-reconnect)
     map))
 
+(defun squeeze-control-current-player ()
+  (or squeeze-control-current-player
+      (if (= (length squeeze-players) 1)
+          (setq squeeze-control-current-player (squeeze-player-playerid (car squeeze-players)))
+        (call-interactively 'squeeze-control-select-player))))
+
+(defvar squeeze-control-current-player nil)
+
+(defun squeeze-control-select-player (id)
+  (interactive
+   (list (or (get-text-property (point) 'squeeze-playerid)
+             (let ((name (completing-read "Select player: " (mapcar 'squeeze-player-name squeeze-players))))
+               (squeeze-player-playerid (squeeze-find-player-from-name name))))))
+  (setq squeeze-control-current-player id))
+
+(defun squeeze-control-next-track ()
+  (interactive)
+  (squeeze-send-string "%s playlist index +1" (squeeze-control-current-player)))
+
+(defun squeeze-control-previous-track ()
+  (interactive)
+  (squeeze-send-string "%s playlist index -1" (squeeze-control-current-player)))
+
 (define-derived-mode squeeze-control-mode special-mode "SqueezeControl"
   "Major mode for controlling Squeezebox Servers.\\<squeeze-control-mode-map>")
 
-(defun squeeze-update-state (string)
-  (let (done-something)
-    (dolist (line (split-string string "\n"))
-      (when (squeeze-update-state-from-line line)
-        (setq done-something t))))
-  (squeeze-control-display-players)
-  string)
+(defvar squeeze-control-inhibit-display nil)
+
+(lexical-let ((buffer (get-buffer-create " *squeeze-update-state*")))
+  (defun squeeze-update-state (string)
+    ;; FIXME: we could make this a lot more elegant by using the
+    ;; buffer abstraction more
+    (if (cl-position ?\n string)
+        (let (done-something)
+          (with-current-buffer buffer
+            (insert string)
+            (setq string (buffer-string))
+            (erase-buffer))
+          (dolist (line (split-string string "\n"))
+            (when (squeeze-update-state-from-line line)
+              (setq done-something t)))
+          (when done-something
+            (unless squeeze-control-inhibit-display
+              (squeeze-control-display-players))))
+      (with-current-buffer buffer
+        (insert string)))
+    string))
+
+(defconst squeeze-player-line-regexp
+  "^\\(\\(?:[0-9a-f]\\{2\\}%3A\\)\\{5\\}[0-9a-f]\\{2\\}\\) ")
+
+(defun squeeze-find-player (id)
+  (dolist (player squeeze-players)
+    (when (string= id (squeeze-player-playerid player))
+      (return player))))
+
+(defun squeeze-find-player-from-name (name)
+  (dolist (player squeeze-players)
+    (when (string= name (squeeze-player-name player))
+      (return player))))
+
+(defun squeeze-update-power (player state)
+  (if state
+      (setf (squeeze-player-power player) state)
+    (let ((current (squeeze-player-power player)))
+      (setf (squeeze-player-power player)
+            (cond ((string= current "0") "1")
+                  ((string= current "1") "0"))))))
+
+(defun squeeze-update-mixer-volume (player value)
+  (let ((current (squeeze-player-volume player))
+        (number (string-to-number value)))
+    (if (string-match "^[-+]" value)
+        (setf (squeeze-player-volume player)
+              (and current (max 0 (min 100 (+ current number)))))
+      (setf (squeeze-player-volume player) number))))
+
+(require 'notifications)
 
 (defun squeeze-update-state-from-line (string)
   (cond
    ((string-match "^players 0" string)
     (setq squeeze-players (squeeze-parse-players-line string))
     t)
-   ((string-match "^\\([0-9a-f][0-9a-f]%3A[0-9a-f][0-9a-f]%3A[0-9a-f][0-9a-f]%3A[0-9a-f][0-9a-f]%3A[0-9a-f][0-9a-f]%3A[0-9a-f][0-9a-f]\\) power\\( \\([01]\\)\\)?" string)
-    (let ((state (match-string 3 string))
-          (id (url-unhex-string (match-string 1 string)))
-          player)
-      (dolist (p squeeze-players)
-        (when (string= id (squeeze-player-playerid p))
-          (setq player p)
-          (return)))
-      (if state
-          (setf (squeeze-player-power player) state)
-        (let ((current (squeeze-player-power player)))
-          (setf (squeeze-player-power player)
-                (cond ((string= current "0") "1")
-                      ((string= current "1") "0"))))))
-    t)))
+   ((string-match "^syncgroups" string)
+    (setq squeeze-syncgroups (squeeze-parse-syncgroups-line string))
+    t)
+   ((string-match "^albums" string)
+    (squeeze-parse-albums-line string)
+    t)
+   ((string-match squeeze-player-line-regexp string)
+    (let ((substring (substring string (match-end 0)))
+          (id (url-unhex-string (match-string 1 string))))
+      (cond
+       ((string-match "^playlist newsong \\(.*\\) \\([0-9]+\\)$" substring)
+        (let ((value (save-match-data (url-unhex-string (match-string 1 substring))))
+              (index (url-unhex-string (match-string 2 substring))))
+          (notifications-notify :title "Now playing" :body (encode-coding-string (format "%s: %s" index value) 'utf-8)))
+        t)
+       ((string-match "^power\\(?: \\([01]\\)\\)?" substring)
+        (let ((state (match-string 1 substring))
+              (player (squeeze-find-player id)))
+          (squeeze-update-power player state))
+        t)
+       ((string-match "^mixer volume \\(\\(?:-\\|%2B\\)?[0-9]*\\)" substring)
+        (let ((value (url-unhex-string (match-string 1 substring)))
+              (player (squeeze-find-player id)))
+          (squeeze-update-mixer-volume player value))
+        t))))))
 
 (defface squeeze-player-face
   '((t))
   "Face for displaying players"
   :group 'squeeze)
 (defface squeeze-player-on-face
-  '((t :weight bold))
+  '((t :weight bold :inherit squeeze-player-face))
   "Face for displaying players which are on"
-  :inherit 'squeeze-player-face
   :group 'squeeze)
 (defface squeeze-player-off-face
-  '((t :weight light))
+  '((t :weight light :inherit squeeze-player-face))
   "Face for displaying players which are off"
-  :inherit 'squeeze-player-face
   :group 'squeeze)
 
+(defface squeeze-mixer-face
+  '((t :weight bold))
+  "Face for displaying mixer information"
+  :group 'squeeze)
+(defface squeeze-mixer-muted-face
+  '((t :weight light :inherit squeeze-mixer-face))
+  "Face for displaying mixer information when muted"
+  :group 'squeeze)
+(defface squeeze-mixer-quiet-face
+  '((t :foreground "green3" :inherit squeeze-mixer-face))
+  "Face for quiet volume"
+  :group 'squeeze)
+(defface squeeze-mixer-medium-face
+  '((t :foreground "gold" :inherit squeeze-mixer-face))
+  "Face for medium volume"
+  :group 'squeeze)
+(defface squeeze-mixer-loud-face
+  '((t :foreground "OrangeRed1" :inherit squeeze-mixer-face))
+  "Face for loud volume"
+  :group 'squeeze)
+(defface squeeze-mixer-muted-quiet-face
+  '((t :inherit (squeeze-mixer-muted-face squeeze-mixer-quiet-face)))
+  "Face for quiet volume when muted")
+(defface squeeze-syncgroup-face
+  '((t :slant italic))
+  "Face for syncgroups"
+  :group 'squeeze)
+
+(defun squeeze-mixer-compute-bar (vol width)
+  (let* ((exact (* width (/ vol 100.0)))
+         (nfull (floor exact))
+         (frac (- exact nfull))
+         (nblank (floor (- width exact))))
+    (format "%s%s%s"
+            (make-string nfull ?█)
+            (if (= width (+ nfull nblank))
+                ""
+              (string (aref " ▏▎▍▌▋▊▉█" (floor (+ frac 0.0625) 0.125))))
+            (make-string nblank ? ))))
+
+(defun squeeze-mixer-make-bar (vol width)
+  (let ((bar (squeeze-mixer-compute-bar vol width))
+        (lo (floor (* 0.65 width)))
+        (hi (floor (* 0.9 width))))
+    (concat "▕"
+            (propertize (substring bar 0 lo) 'face 'squeeze-mixer-quiet-face)
+            (propertize (substring bar lo hi) 'face 'squeeze-mixer-medium-face)
+            (propertize (substring bar hi) 'face 'squeeze-mixer-loud-face)
+            (propertize "▏" 'intangible t))))
+
 (defvar squeeze-players ())
+(defvar squeeze-syncgroups ())
+
+(defun squeeze-send-string (control &rest arguments)
+  (let* ((process (get-buffer-process "*squeeze*"))
+         (string (apply #'format control arguments))
+         (length (length string)))
+    (unless (and (> length 0) (char-equal (aref string (1- length)) ?\n))
+      (setq string (format "%s\n" string)))
+    (if process
+        (comint-send-string process string)
+      (error "can't find squeeze process"))))
+
+(defun squeeze-control-query-syncgroups ()
+  (interactive)
+  (squeeze-send-string "syncgroups ?"))
 
 (defun squeeze-control-query-players ()
   (interactive)
-  (comint-send-string (get-buffer-process "*squeeze*") (format "players ?\n")))
+  (squeeze-send-string "players 0"))
 
 (defun squeeze-control-toggle-power (&optional id)
   (interactive)
   (unless id
     (setq id (get-text-property (point) 'squeeze-playerid)))
-  (comint-send-string (get-buffer-process "*squeeze*") (format "%s power\n" id)))
+  (squeeze-send-string "%s power" id))
+
+(defun squeeze-control-play-favorite (&optional favorite id)
+  (interactive "nFavourite: ")
+  (unless id
+    (setq id (get-text-property (point) 'squeeze-playerid)))
+  (squeeze-send-string "%s favorites playlist play item_id:%d" id favorite))
 
 (defun squeeze-control-query-power (&optional id)
   (interactive)
   (unless id
     (setq id (get-text-property (point) 'squeeze-playerid)))
-  (comint-send-string (get-buffer-process "*squeeze*") (format "%s power ?\n" id)))
+  (when id
+    (squeeze-send-string "%s power ?" id)))
+
+(defun squeeze-control-volume-up (&optional id inc)
+  (interactive)
+  (unless inc (setq inc 5))
+  (unless id
+    (setq id (get-text-property (point) 'squeeze-playerid)))
+  (when id
+    (squeeze-send-string "%s mixer volume %+d" id inc)))
+
+(defun squeeze-control-volume-down (&optional id inc)
+  (interactive)
+  (unless inc (setq inc 5))
+  (unless id
+    (setq id (get-text-property (point) 'squeeze-playerid)))
+  (when id
+    (squeeze-send-string "%s mixer volume %+d" id (- inc))))
+
+(defun squeeze-control-volume-set (id val)
+  (interactive)
+  (squeeze-send-string "%s mixer volume %d" id val))
+
+(defun squeeze-control-query-mixer-volume (&optional id)
+  (interactive)
+  (unless id
+    (setq id (get-text-property (point) 'squeeze-playerid)))
+  (when id
+    (squeeze-send-string "%s mixer volume ?" id)))
 
 (defun squeeze-control-player-face (player)
   (let ((power (squeeze-player-power player)))
           (t 'squeeze-player-face))))
 
 (defun squeeze-control-listen ()
-  (comint-send-string (get-buffer-process "*squeeze*") (format "listen 1\n")))
+  (squeeze-send-string "listen 1"))
+
+(defun squeeze-accept-process-output ()
+  (while (accept-process-output (get-buffer-process "*squeeze*") 0.1 nil t)))
 
 (defun squeeze-control-refresh ()
   (interactive)
-  (squeeze-control-query-players)
-  (dolist (player squeeze-players)
-    (squeeze-control-query-power (squeeze-player-playerid player))))
+  (let ((squeeze-control-inhibit-display t))
+    (squeeze-control-query-players)
+    (squeeze-accept-process-output)
+    (squeeze-control-query-syncgroups)
+    (dolist (player squeeze-players)
+      (squeeze-control-query-power (squeeze-player-playerid player))
+      (squeeze-control-query-mixer-volume (squeeze-player-playerid player))))
+  (squeeze-accept-process-output)
+  (squeeze-control-display-players))
+
+(defvar squeeze-control-mixer-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "RET") 'squeeze-control-mixer-set-volume)
+    (define-key map [mouse-1] 'squeeze-control-mixer-mouse-1)
+    map))
+
+(defun squeeze-control-compute-volume (pos)
+  (let* ((end (next-single-property-change pos 'keymap))
+         (start (previous-single-property-change end 'keymap)))
+    (/ (* 100 (- (point) start)) (- end start 1))))
+
+(defun squeeze-control-mixer-mouse-1 (event)
+  (interactive "e")
+  (let* ((pos (cadadr event))
+         (val (squeeze-control-compute-volume pos))
+         (id (get-text-property pos 'squeeze-playerid)))
+    (squeeze-control-volume-set id val)))
+
+(defun squeeze-control-mixer-set-volume ()
+  (interactive)
+  (let* ((val (squeeze-control-compute-volume (point)))
+         (id (get-text-property (point) 'squeeze-playerid)))
+    (squeeze-control-volume-set id val)))
+
+(defvar squeeze-control-display-syncgroups nil)
+
+(defun squeeze-control-toggle-syncgroup-display ()
+  (interactive)
+  (setf squeeze-control-display-syncgroups
+        (not squeeze-control-display-syncgroups))
+  (squeeze-control-display-players))
+
+(defun squeeze-control-insert-player (player)
+  (insert (propertize (format "%20s" (squeeze-player-name player))
+                      'face (squeeze-control-player-face player)
+                      'squeeze-playerid (squeeze-player-playerid player)))
+  (when (squeeze-player-volume player)
+    (insert (propertize
+             (squeeze-mixer-make-bar (squeeze-player-volume player) 28)
+             'squeeze-playerid (squeeze-player-playerid player)
+             'keymap squeeze-control-mixer-map
+             'pointer 'hdrag
+             'rear-nonsticky '(keymap))))
+  (insert (propertize "\n" 'intangible t)))
 
 (defun squeeze-control-display-players ()
   (interactive)
-  (let ((players squeeze-players))
-    (with-current-buffer (get-buffer-create "*squeeze-control*")
+  (with-current-buffer (get-buffer-create "*squeeze-control*")
+    (let ((saved (point)))
       (squeeze-control-mode)
       (read-only-mode -1)
       (erase-buffer)
-      (dolist (player players)
-        (insert (propertize (squeeze-player-name player)
-                            'face (squeeze-control-player-face player)
-                            'squeeze-playerid (squeeze-player-playerid player))
-                "\n"))
-      (read-only-mode 1))))
+      (cond
+       (squeeze-control-display-syncgroups
+        (let ((syncgroups squeeze-syncgroups)
+              (seen))
+          (while syncgroups
+            (let ((names (getf syncgroups :sync_member_names))
+                  ;; new server version has sync_members and sync_member_names
+                  (members (split-string (getf syncgroups :sync_members) ",")))
+              (insert (propertize names 'face 'squeeze-syncgroup-face) "\n")
+              (dolist (member members)
+                (let ((player (squeeze-find-player member)))
+                  (squeeze-control-insert-player player)
+                  (push player seen))))
+            (setq syncgroups (cddddr syncgroups)))
+          (insert (propertize "No syncgroup" 'face 'squeeze-syncgroup-face) "\n")
+          (dolist (player squeeze-players)
+            (unless (member player seen)
+              (squeeze-control-insert-player player)))))
+       (t
+        (dolist (player squeeze-players)
+          (squeeze-control-insert-player player))
+        (read-only-mode 1)))
+      (goto-char saved))))
 
 (cl-defstruct (squeeze-player (:constructor squeeze-make-player))
-  playerindex playerid uuid ip name model isplayer displaytype canpoweroff connected power)
+  playerindex playerid uuid ip name model isplayer displaytype canpoweroff connected power volume)
 
 (defun squeeze-string-plistify (string start end)
+  (unless end
+    (setq end (length string)))
   (save-match-data
     (let (result)
       (loop
-       (message "start: %d" start)
-       (if (string-match "\\([a-z]+\\)%3A\\([^ \n]+\\)" string start)
+       (if (string-match "\\([a-z_]+\\)%3A\\([^ \n]+\\)" string start)
            (let ((mend (match-end 0)))
              (when (> mend end)
                (return))
              (push (intern (format ":%s" (substring string (match-beginning 1) (match-end 1)))) result)
-             (push (url-unhex-string (substring string (match-beginning 2) (match-end 2))) result)
+             (push (decode-coding-string
+                    (url-unhex-string (substring string (match-beginning 2) (match-end 2)))
+                    'utf-8)
+                   result)
              (setq start mend))
          (return)))
       (nreverse result))))
 
-(defun squeeze-parse-players-line (string)
-  (let ((countpos (string-match " count%3A\\([0-9]\\) " string))
+(defun squeeze-parse-syncgroups-line (string)
+  (let ((syncgroupspos (string-match "^syncgroups " string))
         (startpos (match-end 0)))
-    (unless countpos
-      (message "no count found in players line"))
-    (let ((count (parse-integer string (match-beginning 1) (match-end 1)))
-          result endpos)
+    (when startpos
+      (squeeze-string-plistify string startpos (length string)))))
+
+(defun squeeze-parse-count (string)
+  (save-match-data
+    (let ((countpos (string-match "count%3A\\([0-9]*\\)\\>" string)))
+      (if countpos
+          (string-to-number
+           (substring string (match-beginning 1) (match-end 1)))
+        (let ((kind
+               (progn (string-match "^\\([a-z]*\\) " string)
+                      (substring string (match-beginning 1) (match-end 1)))))
+          (message "no count found in %s line" kind)
+          nil)))))
+
+(defun squeeze-parse-players-line (string)
+  (let ((count (squeeze-parse-count string))
+        (startpos (string-match "playerindex" string))
+        result endpos)
+    (when (> count 0)
       (dotimes (i (1- count))
         (setq endpos (progn (string-match " connected%3A[0-1] " string startpos)
                             (match-end 0)))
         (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos endpos)) result)
         (setq startpos endpos))
-      (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos (length string))) result)
-      result)))
+      (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos (length string))) result))
+    result))
+
+(defcustom squeeze-artwork-directory "~/.emacs.d/squeeze/artwork/"
+  "Base directory for album and track artwork")
+
+(defvar squeeze-albums nil)
+
+(cl-defstruct (squeeze-album (:constructor squeeze-make-album))
+  id album artwork_track_id artist index)
+
+(defun squeeze-parse-albums-line (string)
+  (let ((count (squeeze-parse-count string))
+        (countpos (string-match "\\_<count%3a" string))
+        (start-index-pos (progn (string-match "^albums \\([0-9]+\\)\\>" string)
+                                (match-beginning 1)))
+        index start end)
+    (unless squeeze-albums
+      (setq squeeze-albums (make-vector count nil)))
+    (when start-index-pos
+      (setq index (string-to-number (substring string start-index-pos)))
+      (setq start (string-match "\\_<id%3a" string start-index-pos))
+      (while start
+        (setq end (string-match "\\_<\\(id%3a\\|count%3a\\)" string (1+ start)))
+        (aset squeeze-albums index
+              (apply 'squeeze-make-album
+                     :index index
+                     (squeeze-string-plistify string start end)))
+        (incf index)
+        (setq start (if (= end countpos) nil end))))))
+
+(defun squeeze-get-albums ()
+  (squeeze-send-string "albums 0 1000 tags:lja")
+  (squeeze-accept-process-output))
+
+(defvar squeeze-albums-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "RET") 'squeeze-albums-load-album)
+    map))
+
+(define-derived-mode squeeze-albums-mode tabulated-list-mode "SqueezeAlbums"
+  "Major mode for displaying Albums from Squeezebox Servers.\\<squeeze-albums-mode-map>"
+  (setq tabulated-list-format [("Cover" 10 nil) ("Title" 30 t)])
+  (add-hook 'tabulated-list-revert-hook 'squeeze-get-albums nil t)
+  (setq tabulated-list-entries 'squeeze-albums-tabulated-list-entries)
+  (tabbar-mode -1)
+  (setq tabulated-list-use-header-line nil)
+  (tabulated-list-init-header))
+
+(defun squeeze-albums-artwork-callback (status artwork_track_id filename)
+  (unless status
+    (goto-char 1)
+    (let ((end (search-forward "\n\n")))
+      (delete-region 1 end)
+      (write-file (format "%s%s/%s" squeeze-artwork-directory artwork_track_id filename))
+      (kill-buffer))))
+
+(defun squeeze-albums-tabulated-list-entry (x)
+  (let* ((ati (squeeze-album-artwork_track_id x))
+         (file (format "%s%s/cover_50x50_o.jpg" squeeze-artwork-directory ati)))
+    (unless (file-exists-p file)
+      (make-directory (format "%s%s" squeeze-artwork-directory ati) t)
+      (let ((url (format "http://%s:%s/music/%s/cover_50x50_o.jpg"
+                         squeeze-server-address squeeze-server-http-port ati)))
+        (url-queue-retrieve url 'squeeze-albums-artwork-callback
+                            (list ati "cover_50x50_o.jpg") t t)))
+    (list x (vector (propertize (format "%s" ati)
+                                'display `(when (file-exists-p ,file)
+                                            image :type jpeg :file ,file))
+                    (squeeze-album-album x)))))
+
+(defun squeeze-albums-tabulated-list-entries ()
+  (mapcar 'squeeze-albums-tabulated-list-entry (append squeeze-albums nil)))
 
 (defun squeeze-complete-command-at-point ()
   (save-excursion
     (list (progn (backward-word) (point))
           (progn (forward-word) (point))
-          '(;; General commands and queries
-            "login" "can" "version" "listen" "subscribe" "pref"
-            "logging" "getstring" "setsncredentials" "debug"
-            "exit" "shutdown"
-
-            ;; Player commands and queries
-            "player" "count" "id" "uuid" "name" "ip" "model" "isplayer"
-            "displaytype" "canpoweroff" "?" "signalstrength" "connected"
-            "sleep" "sync" "syncgroups" "power" "mixer" "volume" "muting"
-            "bass" "treble" "pitch" "show" "display" "linesperscreen"
-            "displaynow" "playerpref" "button" "ir" "irenable"
-            "connect" "client" "forget" "disconnect" "players"
-            
-            ;; Database commands and queries
-            "rescan" "rescanprogress" "abortscan" "wipecache" "info"
-            "total" "genres" "artists" "albums" "songs" "years"
-            "musicfolder" "playlists" "tracks" "new" "rename" "delete"
-            "edit" "songinfo" "titles" "search" "pragma"
-
-            ;; Playlist commands and queries
-            "play" "stop" "pause" "mode" "time" "genre" "artist" "album"
-            "title" "duration" "remote" "current_title" "path" "playlist"
-            "add" "insert" "deleteitem" "move" "delete" "preview" "resume"
-            "save" "loadalbum" "addalbum" "loadtracks" "addtracks"
-            "insertalbum" "deletealbum" "clear" "zap" "name" "url"
-            "modified" "playlistsinfo" "index" "shuffle" "repeat"
-            "playlistcontrol"
-
-            ;; Compound queries
-            "serverstatus" "status" "displaystatus" "readdirectory"
-
-            ;; Notifications
-            
-            ;; Alarm commands and queries
-            "alarm" "alarms"
-
-            ;; Plugins commands and queries
-            "favorites"
-            ))))
-
-(defun squeeze ()
-  (interactive)
-  (let ((buffer (make-comint-in-buffer "squeeze" nil
-                                       (cons squeeze-server-address
-                                             squeeze-server-port))))
-    (switch-to-buffer buffer)
-    (squeeze-mode)))
+          (append
+           (mapcar 'squeeze-player-playerid squeeze-players)
+           '(;; General commands and queries
+             "login" "can" "version" "listen" "subscribe" "pref"
+             "logging" "getstring" "setsncredentials" "debug"
+             "exit" "shutdown"
+
+             ;; Player commands and queries
+             "player" "count" "id" "uuid" "name" "ip" "model" "isplayer"
+             "displaytype" "canpoweroff" "?" "signalstrength" "connected"
+             "sleep" "sync" "syncgroups" "power" "mixer" "volume" "muting"
+             "bass" "treble" "pitch" "show" "display" "linesperscreen"
+             "displaynow" "playerpref" "button" "ir" "irenable"
+             "connect" "client" "forget" "disconnect" "players"
+
+             ;; Database commands and queries
+             "rescan" "rescanprogress" "abortscan" "wipecache" "info"
+             "total" "genres" "artists" "albums" "songs" "years"
+             "musicfolder" "playlists" "tracks" "new" "rename" "delete"
+             "edit" "songinfo" "titles" "search" "pragma"
+
+             ;; Playlist commands and queries
+             "play" "stop" "pause" "mode" "time" "genre" "artist" "album"
+             "title" "duration" "remote" "current_title" "path" "playlist"
+             "add" "insert" "deleteitem" "move" "delete" "preview" "resume"
+             "save" "loadalbum" "addalbum" "loadtracks" "addtracks"
+             "insertalbum" "deletealbum" "clear" "zap" "name" "url"
+             "modified" "playlistsinfo" "index" "shuffle" "repeat"
+             "playlistcontrol"
+
+             ;; Compound queries
+             "serverstatus" "status" "displaystatus" "readdirectory"
+
+             ;; Notifications
+
+             ;; Alarm commands and queries
+             "alarm" "alarms"
 
-(defun squeeze-control ()
+             ;; Plugins commands and queries
+             "favorites"
+             )))))
+
+(defun squeeze-read-server-parameters (address port)
+  (let ((host (read-string "Host: " nil nil address))
+        (port (read-number "Port: " port)))
+    (cons host port)))
+
+(cl-defmacro with-squeeze-parameters ((address port) &body body)
+  (declare (indent 1))
+  `(progn
+     (unless ,address (setq ,address squeeze-server-address))
+     (unless ,port (setq ,port squeeze-server-port))
+     (when current-prefix-arg
+       (let ((parameters (squeeze-read-server-parameters ,address ,port)))
+         (setq ,address (car parameters)
+               ,port (cdr parameters))))
+     ,@body))
+
+(defun squeeze (&optional address port)
+  (interactive)
+  (with-squeeze-parameters (address port)
+    (let ((buffer (make-comint-in-buffer "squeeze" nil (cons address port))))
+      (switch-to-buffer buffer)
+      (squeeze-mode))))
+
+(defun squeeze-control (&optional address port)
+  (interactive)
+  (with-squeeze-parameters (address port)
+    (let ((current-prefix-arg nil))
+      (squeeze address port))
+    (let ((buffer (get-buffer-create "*squeeze-control*")))
+      (switch-to-buffer buffer)
+      (squeeze-control-listen)
+      (squeeze-control-refresh)
+      (squeeze-control-display-players))))
+
+(defun squeeze-control-reconnect (&optional address port)
+  (interactive)
+  (with-squeeze-parameters (address port)
+    (kill-buffer (get-buffer "*squeeze*"))
+    (let ((current-prefix-arg nil))
+      (squeeze-control address port))))
+
+(defun squeeze-list-albums ()
   (interactive)
-  (squeeze)
-  (let ((buffer (get-buffer-create "*squeeze-control*")))
+  (squeeze-get-albums)
+  (let ((buffer (get-buffer-create "*squeeze-albums*")))
     (switch-to-buffer buffer)
-    (squeeze-control-listen)
-    (squeeze-control-refresh)
-    (squeeze-control-display-players)))
+    (squeeze-albums-mode)
+    (tabulated-list-print)))
+
+(defun squeeze-albums-load-album ()
+  (interactive)
+  (squeeze-send-string "%s playlistcontrol cmd:load album_id:%s"
+                       (squeeze-control-current-player)
+                       (squeeze-album-id (tabulated-list-get-id))))
+
+(provide 'squeeze)