Christophe Weblog Wiki Code Publications Music
slightly better `squeeze-update-state'
[squeeze-el.git] / squeeze.el
index 5ca2f7cb41492ec9eeadfbfef579396ee8be6e66..977813b28fc24c118fc0dafd600d7c7901250723 100644 (file)
     (define-key map (kbd "TAB") 'completion-at-point)
     map))
 
+(defun squeeze-unhex-and-decode-utf8-string (string)
+  (decode-coding-string (url-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)
     map))
 
 (define-derived-mode squeeze-control-mode special-mode "SqueezeControl"
 
 (defvar squeeze-control-inhibit-display nil)
 
-(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)))
-    (when done-something
-      (unless squeeze-control-inhibit-display
-        (squeeze-control-display-players))))
-  string)
+(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\\}\\) ")
@@ -72,6 +88,9 @@
    ((string-match "^players 0" string)
     (setq squeeze-players (squeeze-parse-players-line string))
     t)
+   ((string-match "^syncgroups" string)
+    (setq squeeze-syncgroups (squeeze-parse-syncgroups-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))))
 (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)))
          (nblank (floor (- width exact))))
     (format "%s%s%s"
             (make-string nfull ?█)
-            (cond ((= width (+ nfull nblank)) "")
-                  ((< frac 0.0625) " ")
-                  ((< frac 0.1875) "▏")
-                  ((< frac 0.3125) "▎")
-                  ((< frac 0.4375) "▍")
-                  ((< frac 0.5625) "▌")
-                  ((< frac 0.6875) "▋")
-                  ((< frac 0.8125) "▊")
-                  ((< frac 0.9375) "▉")
-                  (t "█"))
+            (if (= width (+ nfull nblank))
+                ""
+              (string (aref " ▏▎▍▌▋▊▉█" (floor (+ frac 0.0625) 0.125))))
             (make-string nblank ? ))))
 
 (defun squeeze-mixer-make-bar (vol width)
             (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 0\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)))
   (when id
-    (comint-send-string (get-buffer-process "*squeeze*") (format "%s power ?\n" id))))
+    (squeeze-send-string "%s power ?" id)))
 
 (defun squeeze-control-volume-up (&optional id inc)
   (interactive)
   (unless id
     (setq id (get-text-property (point) 'squeeze-playerid)))
   (when id
-    (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume %+d\n" id inc))))
+    (squeeze-send-string "%s mixer volume %+d" id inc)))
 
 (defun squeeze-control-volume-down (&optional id inc)
   (interactive)
   (unless id
     (setq id (get-text-property (point) 'squeeze-playerid)))
   (when id
-    (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume %+d\n" id (- inc)))))
+    (squeeze-send-string "%s mixer volume %+d" id (- inc))))
 
 (defun squeeze-control-volume-set (id val)
   (interactive)
-  (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume %d\n" id val)))
+  (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
-    (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume ?\n" 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)
   (let ((squeeze-control-inhibit-display t))
     (squeeze-control-query-players)
-    (accept-process-output (get-buffer-process "*squeeze*"))
+    (squeeze-accept-process-output)
+    (squeeze-control-query-syncgroups)
     (dolist (player squeeze-players)
       (squeeze-control-query-power (squeeze-player-playerid player))
-      (accept-process-output (get-buffer-process "*squeeze*"))
-      (squeeze-control-query-mixer-volume (squeeze-player-playerid player))
-      (accept-process-output (get-buffer-process "*squeeze*"))))
+      (squeeze-control-query-mixer-volume (squeeze-player-playerid player))))
+  (squeeze-accept-process-output)
   (squeeze-control-display-players))
 
 (defvar squeeze-control-mixer-map
          (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 (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)))
-      (read-only-mode 1))))
+      (cond
+       (squeeze-control-display-syncgroups
+        (let ((syncgroups squeeze-syncgroups)
+              (seen))
+          (while syncgroups
+            (let ((names (getf syncgroups :names))
+                  (members (split-string (getf syncgroups :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 volume)
 
 (defun squeeze-string-plistify (string start end)
+  (unless end
+    (setq end (length string)))
   (save-match-data
     (let (result)
       (loop
-       (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)
-      (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)))
+    (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)
+    (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))
+
 
 (defun squeeze-complete-command-at-point ()
   (save-excursion
             "serverstatus" "status" "displaystatus" "readdirectory"
 
             ;; Notifications
-            
+
             ;; Alarm commands and queries
             "alarm" "alarms"
 
             "favorites"
             ))))
 
-(defun squeeze ()
+(defun squeeze-read-server-parameters (address port)
+  (let ((host (read-string "Host: " nil nil address))
+        (port (read-number "Port: " port)))
+    (cons host port)))
+
+(defun squeeze (&optional address port)
   (interactive)
-  (let ((buffer (make-comint-in-buffer "squeeze" nil
-                                       (cons squeeze-server-address
-                                             squeeze-server-port))))
+  (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))))
+  (let ((buffer (make-comint-in-buffer "squeeze" nil (cons address port))))
     (switch-to-buffer buffer)
     (squeeze-mode)))
 
-(defun squeeze-control ()
+(defun squeeze-control (&optional address port)
   (interactive)
-  (squeeze)
+  (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))))
+  (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)))
+
+(provide 'squeeze)