Christophe Weblog Wiki Code Publications Music
c64eaef2ec337ddaf540cfdeec57f4e882cb649f
[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     (if (string-match "^[+-]" value)
65         (when current
66           (setf (squeeze-player-volume player) (+ current (string-to-number value))))
67       (setf (squeeze-player-volume player) (string-to-number value)))))
68
69 (defun squeeze-update-state-from-line (string)
70   (cond
71    ((string-match "^players 0" string)
72     (setq squeeze-players (squeeze-parse-players-line string))
73     t)
74    ((string-match squeeze-player-line-regexp string)
75     (let ((substring (substring string (match-end 0)))
76           (id (url-unhex-string (match-string 1 string))))
77       (cond
78        ((string-match "^power\\(?: \\([01]\\)\\)?" substring)
79         (let ((state (match-string 1 substring))
80               (player (squeeze-find-player id)))
81           (squeeze-update-power player state))
82         t)
83        ((string-match "^mixer volume \\([+-]?[0-9]*\\)" substring)
84         (let ((value (match-string 1 substring))
85               (player (squeeze-find-player id)))
86           (squeeze-update-mixer-volume player value))
87         t))))))
88
89 (defface squeeze-player-face
90   '((t))
91   "Face for displaying players"
92   :group 'squeeze)
93 (defface squeeze-player-on-face
94   '((t :weight bold :inherit squeeze-player-face))
95   "Face for displaying players which are on"
96   :group 'squeeze)
97 (defface squeeze-player-off-face
98   '((t :weight light :inherit squeeze-player-face))
99   "Face for displaying players which are off"
100   :group 'squeeze)
101
102 (defface squeeze-mixer-face
103   '((t :weight bold))
104   "Face for displaying mixer information"
105   :group 'squeeze)
106 (defface squeeze-mixer-muted-face
107   '((t :weight light :inherit squeeze-mixer-face))
108   "Face for displaying mixer information when muted"
109   :group 'squeeze)
110 (defface squeeze-mixer-quiet-face
111   '((t :foreground "green3" :inherit squeeze-mixer-face))
112   "Face for quiet volume"
113   :group 'squeeze)
114 (defface squeeze-mixer-medium-face
115   '((t :foreground "gold" :inherit squeeze-mixer-face))
116   "Face for medium volume"
117   :group 'squeeze)
118 (defface squeeze-mixer-loud-face
119   '((t :foreground "OrangeRed1" :inherit squeeze-mixer-face))
120   "Face for loud volume"
121   :group 'squeeze)
122 (defface squeeze-mixer-muted-quiet-face
123   '((t :inherit (squeeze-mixer-muted-face squeeze-mixer-quiet-face)))
124   "Face for quiet volume when muted")
125
126 (defun squeeze-mixer-compute-bar (vol width)
127   (let* ((exact (* width (/ vol 100.0)))
128          (nfull (floor exact))
129          (frac (- exact nfull))
130          (nblank (floor (- width exact))))
131     (format "%s%s%s"
132             (make-string nfull ?█)
133             (cond ((= width (+ nfull nblank)) "")
134                   ((< frac 0.0625) " ")
135                   ((< frac 0.1875) "▏")
136                   ((< frac 0.3125) "▎")
137                   ((< frac 0.4375) "▍")
138                   ((< frac 0.5625) "▌")
139                   ((< frac 0.6875) "▋")
140                   ((< frac 0.8125) "▊")
141                   ((< frac 0.9375) "▉")
142                   (t "█"))
143             (make-string nblank ? ))))
144
145 (defun squeeze-mixer-insert-bar (vol width)
146   (let ((bar (squeeze-mixer-compute-bar vol width))
147         (lo (floor (* 0.65 width)))
148         (hi (floor (* 0.9 width))))
149     (insert ?▕
150             (propertize (substring bar 0 lo) 'face 'squeeze-mixer-quiet-face)
151             (propertize (substring bar lo hi) 'face 'squeeze-mixer-medium-face)
152             (propertize (substring bar hi) 'face 'squeeze-mixer-loud-face)
153             ?▏
154             )))
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   (comint-send-string (get-buffer-process "*squeeze*") (format "%s power ?\n" id)))
173
174 (defun squeeze-control-volume-up (&optional id inc)
175   (interactive)
176   (unless inc (setq inc 5))
177   (unless id
178     (setq id (get-text-property (point) 'squeeze-playerid)))
179   (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume %+d\n" id inc)))
180
181 (defun squeeze-control-volume-down (&optional id inc)
182   (interactive)
183   (unless inc (setq inc 5))
184   (unless id
185     (setq id (get-text-property (point) 'squeeze-playerid)))
186   (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume %+d\n" id (- inc))))
187
188 (defun squeeze-control-query-mixer-volume (&optional id)
189   (interactive)
190   (unless id
191     (setq id (get-text-property (point) 'squeeze-playerid)))
192   (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume ?\n" id)))
193
194 (defun squeeze-control-player-face (player)
195   (let ((power (squeeze-player-power player)))
196     (cond ((string= power "1") 'squeeze-player-on-face)
197           ((string= power "0") 'squeeze-player-off-face)
198           (t 'squeeze-player-face))))
199
200 (defun squeeze-control-listen ()
201   (comint-send-string (get-buffer-process "*squeeze*") (format "listen 1\n")))
202
203 (defun squeeze-control-refresh ()
204   (interactive)
205   (let ((squeeze-control-inhibit-display t))
206     (squeeze-control-query-players)
207     (accept-process-output (get-buffer-process "*squeeze*"))
208     (dolist (player squeeze-players)
209       (squeeze-control-query-power (squeeze-player-playerid player))
210       (accept-process-output (get-buffer-process "*squeeze*"))
211       (squeeze-control-query-mixer-volume (squeeze-player-playerid player))
212       (accept-process-output (get-buffer-process "*squeeze*"))))
213   (squeeze-control-display-players))
214
215 (defun squeeze-control-display-players ()
216   (interactive)
217   (let ((players squeeze-players))
218     (with-current-buffer (get-buffer-create "*squeeze-control*")
219       (squeeze-control-mode)
220       (read-only-mode -1)
221       (erase-buffer)
222       (dolist (player players)
223         (insert (propertize (format "%20s" (squeeze-player-name player))
224                             'face (squeeze-control-player-face player)
225                             'squeeze-playerid (squeeze-player-playerid player)))
226         (when (squeeze-player-volume player)
227           (squeeze-mixer-insert-bar (squeeze-player-volume player) 28))
228         (insert "\n"))
229       (read-only-mode 1))))
230
231 (cl-defstruct (squeeze-player (:constructor squeeze-make-player))
232   playerindex playerid uuid ip name model isplayer displaytype canpoweroff connected power volume)
233
234 (defun squeeze-string-plistify (string start end)
235   (save-match-data
236     (let (result)
237       (loop
238        (if (string-match "\\([a-z]+\\)%3A\\([^ \n]+\\)" string start)
239            (let ((mend (match-end 0)))
240              (when (> mend end)
241                (return))
242              (push (intern (format ":%s" (substring string (match-beginning 1) (match-end 1)))) result)
243              (push (url-unhex-string (substring string (match-beginning 2) (match-end 2))) result)
244              (setq start mend))
245          (return)))
246       (nreverse result))))
247
248 (defun squeeze-parse-players-line (string)
249   (let ((countpos (string-match " count%3A\\([0-9]\\) " string))
250         (startpos (match-end 0)))
251     (unless countpos
252       (message "no count found in players line"))
253     (let ((count (parse-integer string (match-beginning 1) (match-end 1)))
254           result endpos)
255       (dotimes (i (1- count))
256         (setq endpos (progn (string-match " connected%3A[0-1] " string startpos)
257                             (match-end 0)))
258         (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos endpos)) result)
259         (setq startpos endpos))
260       (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos (length string))) result)
261       result)))
262
263 (defun squeeze-complete-command-at-point ()
264   (save-excursion
265     (list (progn (backward-word) (point))
266           (progn (forward-word) (point))
267           '(;; General commands and queries
268             "login" "can" "version" "listen" "subscribe" "pref"
269             "logging" "getstring" "setsncredentials" "debug"
270             "exit" "shutdown"
271
272             ;; Player commands and queries
273             "player" "count" "id" "uuid" "name" "ip" "model" "isplayer"
274             "displaytype" "canpoweroff" "?" "signalstrength" "connected"
275             "sleep" "sync" "syncgroups" "power" "mixer" "volume" "muting"
276             "bass" "treble" "pitch" "show" "display" "linesperscreen"
277             "displaynow" "playerpref" "button" "ir" "irenable"
278             "connect" "client" "forget" "disconnect" "players"
279             
280             ;; Database commands and queries
281             "rescan" "rescanprogress" "abortscan" "wipecache" "info"
282             "total" "genres" "artists" "albums" "songs" "years"
283             "musicfolder" "playlists" "tracks" "new" "rename" "delete"
284             "edit" "songinfo" "titles" "search" "pragma"
285
286             ;; Playlist commands and queries
287             "play" "stop" "pause" "mode" "time" "genre" "artist" "album"
288             "title" "duration" "remote" "current_title" "path" "playlist"
289             "add" "insert" "deleteitem" "move" "delete" "preview" "resume"
290             "save" "loadalbum" "addalbum" "loadtracks" "addtracks"
291             "insertalbum" "deletealbum" "clear" "zap" "name" "url"
292             "modified" "playlistsinfo" "index" "shuffle" "repeat"
293             "playlistcontrol"
294
295             ;; Compound queries
296             "serverstatus" "status" "displaystatus" "readdirectory"
297
298             ;; Notifications
299             
300             ;; Alarm commands and queries
301             "alarm" "alarms"
302
303             ;; Plugins commands and queries
304             "favorites"
305             ))))
306
307 (defun squeeze ()
308   (interactive)
309   (let ((buffer (make-comint-in-buffer "squeeze" nil
310                                        (cons squeeze-server-address
311                                              squeeze-server-port))))
312     (switch-to-buffer buffer)
313     (squeeze-mode)))
314
315 (defun squeeze-control ()
316   (interactive)
317   (squeeze)
318   (let ((buffer (get-buffer-create "*squeeze-control*")))
319     (switch-to-buffer buffer)
320     (squeeze-control-listen)
321     (squeeze-control-refresh)
322     (squeeze-control-display-players)))