Christophe Weblog Wiki Code Publications Music
update syncgroups parser for new server
[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
13 (defvar squeeze-mode-map
14   (let ((map (make-sparse-keymap)))
15     (define-key map (kbd "TAB") 'completion-at-point)
16     map))
17
18 (defun squeeze-unhex-string (string)
19   (with-temp-buffer
20     (let ((case-fold-search t)
21           (start 0))
22       (while (string-match "%[0-9a-f][0-9a-f]" string start)
23         (let* ((s (match-beginning 0))
24                (ch1 (url-unhex (elt string (+ s 1))))
25                (code (+ (* 16 ch1)
26                         (url-unhex (elt string (+ s 2))))))
27           (insert (substring string start s)
28                   (byte-to-string code))
29           (setq start (match-end 0))))
30       (insert (substring string start)))
31     (buffer-string)))
32
33 (defun squeeze-unhex-and-decode-utf8-string (string)
34   (decode-coding-string (squeeze-unhex-string string) 'utf-8))
35
36 (define-derived-mode squeeze-mode comint-mode "Squeeze"
37   "Major mode for interacting with the Squeezebox Server CLI.\\<squeeze-mode-map>"
38   (add-to-list 'completion-at-point-functions 'squeeze-complete-command-at-point)
39   (add-hook 'comint-preoutput-filter-functions 'squeeze-unhex-and-decode-utf8-string nil t)
40   (add-hook 'comint-preoutput-filter-functions 'squeeze-update-state nil t))
41
42 (defvar squeeze-control-mode-map
43   (let ((map (make-sparse-keymap)))
44     (define-key map (kbd "SPC") 'squeeze-control-toggle-power)
45     (define-key map (kbd "f") 'squeeze-control-play-favorite)
46     (define-key map (kbd "g") 'squeeze-control-refresh)
47     (define-key map (kbd "+") 'squeeze-control-volume-up)
48     (define-key map (kbd "-") 'squeeze-control-volume-down)
49     (define-key map (kbd "t") 'squeeze-control-toggle-syncgroup-display)
50     map))
51
52 (define-derived-mode squeeze-control-mode special-mode "SqueezeControl"
53   "Major mode for controlling Squeezebox Servers.\\<squeeze-control-mode-map>")
54
55 (defvar squeeze-control-inhibit-display nil)
56
57 (lexical-let ((buffer (get-buffer-create " *squeeze-update-state*")))
58   (defun squeeze-update-state (string)
59     ;; FIXME: we could make this a lot more elegant by using the
60     ;; buffer abstraction more
61     (if (cl-position ?\n string)
62         (let (done-something)
63           (with-current-buffer buffer
64             (insert string)
65             (setq string (buffer-string))
66             (erase-buffer))
67           (dolist (line (split-string string "\n"))
68             (when (squeeze-update-state-from-line line)
69               (setq done-something t)))
70           (when done-something
71             (unless squeeze-control-inhibit-display
72               (squeeze-control-display-players))))
73       (with-current-buffer buffer
74         (insert string)))
75     string))
76
77 (defconst squeeze-player-line-regexp
78   "^\\(\\(?:[0-9a-f]\\{2\\}%3A\\)\\{5\\}[0-9a-f]\\{2\\}\\) ")
79
80 (defun squeeze-find-player (id)
81   (dolist (player squeeze-players)
82     (when (string= id (squeeze-player-playerid player))
83       (return player))))
84
85 (defun squeeze-update-power (player state)
86   (if state
87       (setf (squeeze-player-power player) state)
88     (let ((current (squeeze-player-power player)))
89       (setf (squeeze-player-power player)
90             (cond ((string= current "0") "1")
91                   ((string= current "1") "0"))))))
92
93 (defun squeeze-update-mixer-volume (player value)
94   (let ((current (squeeze-player-volume player))
95         (number (string-to-number value)))
96     (if (string-match "^[-+]" value)
97         (setf (squeeze-player-volume player)
98               (and current (max 0 (min 100 (+ current number)))))
99       (setf (squeeze-player-volume player) number))))
100
101 (defun squeeze-update-state-from-line (string)
102   (cond
103    ((string-match "^players 0" string)
104     (setq squeeze-players (squeeze-parse-players-line string))
105     t)
106    ((string-match "^syncgroups" string)
107     (setq squeeze-syncgroups (squeeze-parse-syncgroups-line string))
108     t)
109    ((string-match squeeze-player-line-regexp string)
110     (let ((substring (substring string (match-end 0)))
111           (id (url-unhex-string (match-string 1 string))))
112       (cond
113        ((string-match "^power\\(?: \\([01]\\)\\)?" substring)
114         (let ((state (match-string 1 substring))
115               (player (squeeze-find-player id)))
116           (squeeze-update-power player state))
117         t)
118        ((string-match "^mixer volume \\(\\(?:-\\|%2B\\)?[0-9]*\\)" substring)
119         (let ((value (url-unhex-string (match-string 1 substring)))
120               (player (squeeze-find-player id)))
121           (squeeze-update-mixer-volume player value))
122         t))))))
123
124 (defface squeeze-player-face
125   '((t))
126   "Face for displaying players"
127   :group 'squeeze)
128 (defface squeeze-player-on-face
129   '((t :weight bold :inherit squeeze-player-face))
130   "Face for displaying players which are on"
131   :group 'squeeze)
132 (defface squeeze-player-off-face
133   '((t :weight light :inherit squeeze-player-face))
134   "Face for displaying players which are off"
135   :group 'squeeze)
136
137 (defface squeeze-mixer-face
138   '((t :weight bold))
139   "Face for displaying mixer information"
140   :group 'squeeze)
141 (defface squeeze-mixer-muted-face
142   '((t :weight light :inherit squeeze-mixer-face))
143   "Face for displaying mixer information when muted"
144   :group 'squeeze)
145 (defface squeeze-mixer-quiet-face
146   '((t :foreground "green3" :inherit squeeze-mixer-face))
147   "Face for quiet volume"
148   :group 'squeeze)
149 (defface squeeze-mixer-medium-face
150   '((t :foreground "gold" :inherit squeeze-mixer-face))
151   "Face for medium volume"
152   :group 'squeeze)
153 (defface squeeze-mixer-loud-face
154   '((t :foreground "OrangeRed1" :inherit squeeze-mixer-face))
155   "Face for loud volume"
156   :group 'squeeze)
157 (defface squeeze-mixer-muted-quiet-face
158   '((t :inherit (squeeze-mixer-muted-face squeeze-mixer-quiet-face)))
159   "Face for quiet volume when muted")
160 (defface squeeze-syncgroup-face
161   '((t :slant italic))
162   "Face for syncgroups"
163   :group 'squeeze)
164
165 (defun squeeze-mixer-compute-bar (vol width)
166   (let* ((exact (* width (/ vol 100.0)))
167          (nfull (floor exact))
168          (frac (- exact nfull))
169          (nblank (floor (- width exact))))
170     (format "%s%s%s"
171             (make-string nfull ?█)
172             (if (= width (+ nfull nblank))
173                 ""
174               (string (aref " ▏▎▍▌▋▊▉█" (floor (+ frac 0.0625) 0.125))))
175             (make-string nblank ? ))))
176
177 (defun squeeze-mixer-make-bar (vol width)
178   (let ((bar (squeeze-mixer-compute-bar vol width))
179         (lo (floor (* 0.65 width)))
180         (hi (floor (* 0.9 width))))
181     (concat "▕"
182             (propertize (substring bar 0 lo) 'face 'squeeze-mixer-quiet-face)
183             (propertize (substring bar lo hi) 'face 'squeeze-mixer-medium-face)
184             (propertize (substring bar hi) 'face 'squeeze-mixer-loud-face)
185             (propertize "▏" 'intangible t))))
186
187 (defvar squeeze-players ())
188 (defvar squeeze-syncgroups ())
189
190 (defun squeeze-send-string (control &rest arguments)
191   (let* ((process (get-buffer-process "*squeeze*"))
192          (string (apply #'format control arguments))
193          (length (length string)))
194     (unless (and (> length 0) (char-equal (aref string (1- length)) ?\n))
195       (setq string (format "%s\n" string)))
196     (if process
197         (comint-send-string process string)
198       (error "can't find squeeze process"))))
199
200 (defun squeeze-control-query-syncgroups ()
201   (interactive)
202   (squeeze-send-string "syncgroups ?"))
203
204 (defun squeeze-control-query-players ()
205   (interactive)
206   (squeeze-send-string "players 0"))
207
208 (defun squeeze-control-toggle-power (&optional id)
209   (interactive)
210   (unless id
211     (setq id (get-text-property (point) 'squeeze-playerid)))
212   (squeeze-send-string "%s power" id))
213
214 (defun squeeze-control-play-favorite (&optional favorite id)
215   (interactive "nFavourite: ")
216   (unless id
217     (setq id (get-text-property (point) 'squeeze-playerid)))
218   (squeeze-send-string "%s favorites playlist play item_id:%d" id favorite))
219
220 (defun squeeze-control-query-power (&optional id)
221   (interactive)
222   (unless id
223     (setq id (get-text-property (point) 'squeeze-playerid)))
224   (when id
225     (squeeze-send-string "%s power ?" id)))
226
227 (defun squeeze-control-volume-up (&optional id inc)
228   (interactive)
229   (unless inc (setq inc 5))
230   (unless id
231     (setq id (get-text-property (point) 'squeeze-playerid)))
232   (when id
233     (squeeze-send-string "%s mixer volume %+d" id inc)))
234
235 (defun squeeze-control-volume-down (&optional id inc)
236   (interactive)
237   (unless inc (setq inc 5))
238   (unless id
239     (setq id (get-text-property (point) 'squeeze-playerid)))
240   (when id
241     (squeeze-send-string "%s mixer volume %+d" id (- inc))))
242
243 (defun squeeze-control-volume-set (id val)
244   (interactive)
245   (squeeze-send-string "%s mixer volume %d" id val))
246
247 (defun squeeze-control-query-mixer-volume (&optional id)
248   (interactive)
249   (unless id
250     (setq id (get-text-property (point) 'squeeze-playerid)))
251   (when id
252     (squeeze-send-string "%s mixer volume ?" id)))
253
254 (defun squeeze-control-player-face (player)
255   (let ((power (squeeze-player-power player)))
256     (cond ((string= power "1") 'squeeze-player-on-face)
257           ((string= power "0") 'squeeze-player-off-face)
258           (t 'squeeze-player-face))))
259
260 (defun squeeze-control-listen ()
261   (squeeze-send-string "listen 1"))
262
263 (defun squeeze-accept-process-output ()
264   (while (accept-process-output (get-buffer-process "*squeeze*") 0.1 nil t)))
265
266 (defun squeeze-control-refresh ()
267   (interactive)
268   (let ((squeeze-control-inhibit-display t))
269     (squeeze-control-query-players)
270     (squeeze-accept-process-output)
271     (squeeze-control-query-syncgroups)
272     (dolist (player squeeze-players)
273       (squeeze-control-query-power (squeeze-player-playerid player))
274       (squeeze-control-query-mixer-volume (squeeze-player-playerid player))))
275   (squeeze-accept-process-output)
276   (squeeze-control-display-players))
277
278 (defvar squeeze-control-mixer-map
279   (let ((map (make-sparse-keymap)))
280     (define-key map (kbd "RET") 'squeeze-control-mixer-set-volume)
281     (define-key map [mouse-1] 'squeeze-control-mixer-mouse-1)
282     map))
283
284 (defun squeeze-control-compute-volume (pos)
285   (let* ((end (next-single-property-change pos 'keymap))
286          (start (previous-single-property-change end 'keymap)))
287     (/ (* 100 (- (point) start)) (- end start 1))))
288
289 (defun squeeze-control-mixer-mouse-1 (event)
290   (interactive "e")
291   (let* ((pos (cadadr event))
292          (val (squeeze-control-compute-volume pos))
293          (id (get-text-property pos 'squeeze-playerid)))
294     (squeeze-control-volume-set id val)))
295
296 (defun squeeze-control-mixer-set-volume ()
297   (interactive)
298   (let* ((val (squeeze-control-compute-volume (point)))
299          (id (get-text-property (point) 'squeeze-playerid)))
300     (squeeze-control-volume-set id val)))
301
302 (defvar squeeze-control-display-syncgroups nil)
303
304 (defun squeeze-control-toggle-syncgroup-display ()
305   (interactive)
306   (setf squeeze-control-display-syncgroups
307         (not squeeze-control-display-syncgroups))
308   (squeeze-control-display-players))
309
310 (defun squeeze-control-insert-player (player)
311   (insert (propertize (format "%20s" (squeeze-player-name player))
312                       'face (squeeze-control-player-face player)
313                       'squeeze-playerid (squeeze-player-playerid player)))
314   (when (squeeze-player-volume player)
315     (insert (propertize
316              (squeeze-mixer-make-bar (squeeze-player-volume player) 28)
317              'squeeze-playerid (squeeze-player-playerid player)
318              'keymap squeeze-control-mixer-map
319              'pointer 'hdrag
320              'rear-nonsticky '(keymap))))
321   (insert (propertize "\n" 'intangible t)))
322
323 (defun squeeze-control-display-players ()
324   (interactive)
325   (with-current-buffer (get-buffer-create "*squeeze-control*")
326     (let ((saved (point)))
327       (squeeze-control-mode)
328       (read-only-mode -1)
329       (erase-buffer)
330       (cond
331        (squeeze-control-display-syncgroups
332         (let ((syncgroups squeeze-syncgroups)
333               (seen))
334           (while syncgroups
335             (let ((names (getf syncgroups :sync_member_names))
336                   ;; new server version has sync_members and sync_member_names
337                   (members (split-string (getf syncgroups :sync_members) ",")))
338               (insert (propertize names 'face 'squeeze-syncgroup-face) "\n")
339               (dolist (member members)
340                 (let ((player (squeeze-find-player member)))
341                   (squeeze-control-insert-player player)
342                   (push player seen))))
343             (setq syncgroups (cddddr syncgroups)))
344           (insert (propertize "No syncgroup" 'face 'squeeze-syncgroup-face) "\n")
345           (dolist (player squeeze-players)
346             (unless (member player seen)
347               (squeeze-control-insert-player player)))))
348        (t
349         (dolist (player squeeze-players)
350           (squeeze-control-insert-player player))
351         (read-only-mode 1)))
352       (goto-char saved))))
353
354 (cl-defstruct (squeeze-player (:constructor squeeze-make-player))
355   playerindex playerid uuid ip name model isplayer displaytype canpoweroff connected power volume)
356
357 (defun squeeze-string-plistify (string start end)
358   (unless end
359     (setq end (length string)))
360   (save-match-data
361     (let (result)
362       (loop
363        (if (string-match "\\([a-z_]+\\)%3A\\([^ \n]+\\)" string start)
364            (let ((mend (match-end 0)))
365              (when (> mend end)
366                (return))
367              (push (intern (format ":%s" (substring string (match-beginning 1) (match-end 1)))) result)
368              (push (decode-coding-string
369                     (url-unhex-string (substring string (match-beginning 2) (match-end 2)))
370                     'utf-8)
371                    result)
372              (setq start mend))
373          (return)))
374       (nreverse result))))
375
376 (defun squeeze-parse-syncgroups-line (string)
377   (let ((syncgroupspos (string-match "^syncgroups " string))
378         (startpos (match-end 0)))
379     (when startpos
380       (squeeze-string-plistify string startpos (length string)))))
381
382 (defun squeeze-parse-count (string)
383   (save-match-data
384     (let ((countpos (string-match "count%3A\\([0-9]*\\)\\>" string)))
385       (if countpos
386           (string-to-number
387            (substring string (match-beginning 1) (match-end 1)))
388         (let ((kind
389                (progn (string-match "^\\([a-z]*\\) " string)
390                       (substring string (match-beginning 1) (match-end 1)))))
391           (message "no count found in %s line" kind)
392           nil)))))
393
394 (defun squeeze-parse-players-line (string)
395   (let ((count (squeeze-parse-count string))
396         (startpos (string-match "playerindex" string))
397         result endpos)
398     (when (> count 0)
399       (dotimes (i (1- count))
400         (setq endpos (progn (string-match " connected%3A[0-1] " string startpos)
401                             (match-end 0)))
402         (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos endpos)) result)
403         (setq startpos endpos))
404       (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos (length string))) result))
405     result))
406
407
408 (defun squeeze-complete-command-at-point ()
409   (save-excursion
410     (list (progn (backward-word) (point))
411           (progn (forward-word) (point))
412           (append
413            (mapcar 'squeeze-player-playerid squeeze-players)
414            '(;; General commands and queries
415              "login" "can" "version" "listen" "subscribe" "pref"
416              "logging" "getstring" "setsncredentials" "debug"
417              "exit" "shutdown"
418
419              ;; Player commands and queries
420              "player" "count" "id" "uuid" "name" "ip" "model" "isplayer"
421              "displaytype" "canpoweroff" "?" "signalstrength" "connected"
422              "sleep" "sync" "syncgroups" "power" "mixer" "volume" "muting"
423              "bass" "treble" "pitch" "show" "display" "linesperscreen"
424              "displaynow" "playerpref" "button" "ir" "irenable"
425              "connect" "client" "forget" "disconnect" "players"
426
427              ;; Database commands and queries
428              "rescan" "rescanprogress" "abortscan" "wipecache" "info"
429              "total" "genres" "artists" "albums" "songs" "years"
430              "musicfolder" "playlists" "tracks" "new" "rename" "delete"
431              "edit" "songinfo" "titles" "search" "pragma"
432
433              ;; Playlist commands and queries
434              "play" "stop" "pause" "mode" "time" "genre" "artist" "album"
435              "title" "duration" "remote" "current_title" "path" "playlist"
436              "add" "insert" "deleteitem" "move" "delete" "preview" "resume"
437              "save" "loadalbum" "addalbum" "loadtracks" "addtracks"
438              "insertalbum" "deletealbum" "clear" "zap" "name" "url"
439              "modified" "playlistsinfo" "index" "shuffle" "repeat"
440              "playlistcontrol"
441
442              ;; Compound queries
443              "serverstatus" "status" "displaystatus" "readdirectory"
444
445              ;; Notifications
446
447              ;; Alarm commands and queries
448              "alarm" "alarms"
449
450              ;; Plugins commands and queries
451              "favorites"
452              )))))
453
454 (defun squeeze-read-server-parameters (address port)
455   (let ((host (read-string "Host: " nil nil address))
456         (port (read-number "Port: " port)))
457     (cons host port)))
458
459 (defun squeeze (&optional address port)
460   (interactive)
461   (unless address (setq address squeeze-server-address))
462   (unless port (setq port squeeze-server-port))
463   (when current-prefix-arg
464     (let ((parameters (squeeze-read-server-parameters address port)))
465       (setq address (car parameters)
466             port (cdr parameters))))
467   (let ((buffer (make-comint-in-buffer "squeeze" nil (cons address port))))
468     (switch-to-buffer buffer)
469     (squeeze-mode)))
470
471 (defun squeeze-control (&optional address port)
472   (interactive)
473   (unless address (setq address squeeze-server-address))
474   (unless port (setq port squeeze-server-port))
475   (when current-prefix-arg
476     (let ((parameters (squeeze-read-server-parameters address port)))
477       (setq address (car parameters)
478             port (cdr parameters))))
479   (let ((current-prefix-arg nil))
480     (squeeze address port))
481   (let ((buffer (get-buffer-create "*squeeze-control*")))
482     (switch-to-buffer buffer)
483     (squeeze-control-listen)
484     (squeeze-control-refresh)
485     (squeeze-control-display-players)))
486
487 (provide 'squeeze)