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