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