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