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