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