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