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