Christophe Weblog Wiki Code Publications Music
much better `squeeze-unhex-and-decode-utf8-string'
[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 :names))
336                   (members (split-string (getf syncgroups :members) ",")))
337               (insert (propertize names 'face 'squeeze-syncgroup-face) "\n")
338               (dolist (member members)
339                 (let ((player (squeeze-find-player member)))
340                   (squeeze-control-insert-player player)
341                   (push player seen))))
342             (setq syncgroups (cddddr syncgroups)))
343           (insert (propertize "No syncgroup" 'face 'squeeze-syncgroup-face) "\n")
344           (dolist (player squeeze-players)
345             (unless (member player seen)
346               (squeeze-control-insert-player player)))))
347        (t
348         (dolist (player squeeze-players)
349           (squeeze-control-insert-player player))
350         (read-only-mode 1)))
351       (goto-char saved))))
352
353 (cl-defstruct (squeeze-player (:constructor squeeze-make-player))
354   playerindex playerid uuid ip name model isplayer displaytype canpoweroff connected power volume)
355
356 (defun squeeze-string-plistify (string start end)
357   (unless end
358     (setq end (length string)))
359   (save-match-data
360     (let (result)
361       (loop
362        (if (string-match "\\([a-z_]+\\)%3A\\([^ \n]+\\)" string start)
363            (let ((mend (match-end 0)))
364              (when (> mend end)
365                (return))
366              (push (intern (format ":%s" (substring string (match-beginning 1) (match-end 1)))) result)
367              (push (decode-coding-string
368                     (url-unhex-string (substring string (match-beginning 2) (match-end 2)))
369                     'utf-8)
370                    result)
371              (setq start mend))
372          (return)))
373       (nreverse result))))
374
375 (defun squeeze-parse-syncgroups-line (string)
376   (let ((syncgroupspos (string-match "^syncgroups " string))
377         (startpos (match-end 0)))
378     (when startpos
379       (squeeze-string-plistify string startpos (length string)))))
380
381 (defun squeeze-parse-count (string)
382   (save-match-data
383     (let ((countpos (string-match "count%3A\\([0-9]*\\)\\>" string)))
384       (if countpos
385           (string-to-number
386            (substring string (match-beginning 1) (match-end 1)))
387         (let ((kind
388                (progn (string-match "^\\([a-z]*\\) " string)
389                       (substring string (match-beginning 1) (match-end 1)))))
390           (message "no count found in %s line" kind)
391           nil)))))
392
393 (defun squeeze-parse-players-line (string)
394   (let ((count (squeeze-parse-count string))
395         (startpos (string-match "playerindex" string))
396         result endpos)
397     (dotimes (i (1- count))
398       (setq endpos (progn (string-match " connected%3A[0-1] " string startpos)
399                           (match-end 0)))
400       (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos endpos)) result)
401       (setq startpos endpos))
402     (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos (length string))) result)
403     result))
404
405
406 (defun squeeze-complete-command-at-point ()
407   (save-excursion
408     (list (progn (backward-word) (point))
409           (progn (forward-word) (point))
410           '(;; General commands and queries
411             "login" "can" "version" "listen" "subscribe" "pref"
412             "logging" "getstring" "setsncredentials" "debug"
413             "exit" "shutdown"
414
415             ;; Player commands and queries
416             "player" "count" "id" "uuid" "name" "ip" "model" "isplayer"
417             "displaytype" "canpoweroff" "?" "signalstrength" "connected"
418             "sleep" "sync" "syncgroups" "power" "mixer" "volume" "muting"
419             "bass" "treble" "pitch" "show" "display" "linesperscreen"
420             "displaynow" "playerpref" "button" "ir" "irenable"
421             "connect" "client" "forget" "disconnect" "players"
422             
423             ;; Database commands and queries
424             "rescan" "rescanprogress" "abortscan" "wipecache" "info"
425             "total" "genres" "artists" "albums" "songs" "years"
426             "musicfolder" "playlists" "tracks" "new" "rename" "delete"
427             "edit" "songinfo" "titles" "search" "pragma"
428
429             ;; Playlist commands and queries
430             "play" "stop" "pause" "mode" "time" "genre" "artist" "album"
431             "title" "duration" "remote" "current_title" "path" "playlist"
432             "add" "insert" "deleteitem" "move" "delete" "preview" "resume"
433             "save" "loadalbum" "addalbum" "loadtracks" "addtracks"
434             "insertalbum" "deletealbum" "clear" "zap" "name" "url"
435             "modified" "playlistsinfo" "index" "shuffle" "repeat"
436             "playlistcontrol"
437
438             ;; Compound queries
439             "serverstatus" "status" "displaystatus" "readdirectory"
440
441             ;; Notifications
442
443             ;; Alarm commands and queries
444             "alarm" "alarms"
445
446             ;; Plugins commands and queries
447             "favorites"
448             ))))
449
450 (defun squeeze-read-server-parameters (address port)
451   (let ((host (read-string "Host: " nil nil address))
452         (port (read-number "Port: " port)))
453     (cons host port)))
454
455 (defun squeeze (&optional address port)
456   (interactive)
457   (unless address (setq address squeeze-server-address))
458   (unless port (setq port squeeze-server-port))
459   (when current-prefix-arg
460     (let ((parameters (squeeze-read-server-parameters address port)))
461       (setq address (car parameters)
462             port (cdr parameters))))
463   (let ((buffer (make-comint-in-buffer "squeeze" nil (cons address port))))
464     (switch-to-buffer buffer)
465     (squeeze-mode)))
466
467 (defun squeeze-control (&optional address port)
468   (interactive)
469   (unless address (setq address squeeze-server-address))
470   (unless port (setq port squeeze-server-port))
471   (when current-prefix-arg
472     (let ((parameters (squeeze-read-server-parameters address port)))
473       (setq address (car parameters)
474             port (cdr parameters))))
475   (let ((current-prefix-arg nil))
476     (squeeze address port))
477   (let ((buffer (get-buffer-create "*squeeze-control*")))
478     (switch-to-buffer buffer)
479     (squeeze-control-listen)
480     (squeeze-control-refresh)
481     (squeeze-control-display-players)))
482
483 (provide 'squeeze)