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