2 "Interact with Squeezebox media servers"
6 (defcustom squeeze-server-address "localhost"
7 "Address for the Squeezebox server"
9 (defcustom squeeze-server-port 9090
10 "Port number for the Squeezebox server"
13 (defvar squeeze-mode-map
14 (let ((map (make-sparse-keymap)))
15 (define-key map (kbd "TAB") 'completion-at-point)
18 (defun squeeze-unhex-string (string)
20 (let ((case-fold-search t)
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))))
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)))
33 (defun squeeze-unhex-and-decode-utf8-string (string)
34 (decode-coding-string (squeeze-unhex-string string) 'utf-8))
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))
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)
52 (define-derived-mode squeeze-control-mode special-mode "SqueezeControl"
53 "Major mode for controlling Squeezebox Servers.\\<squeeze-control-mode-map>")
55 (defvar squeeze-control-inhibit-display nil)
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)
63 (with-current-buffer buffer
65 (setq string (buffer-string))
67 (dolist (line (split-string string "\n"))
68 (when (squeeze-update-state-from-line line)
69 (setq done-something t)))
71 (unless squeeze-control-inhibit-display
72 (squeeze-control-display-players))))
73 (with-current-buffer buffer
77 (defconst squeeze-player-line-regexp
78 "^\\(\\(?:[0-9a-f]\\{2\\}%3A\\)\\{5\\}[0-9a-f]\\{2\\}\\) ")
80 (defun squeeze-find-player (id)
81 (dolist (player squeeze-players)
82 (when (string= id (squeeze-player-playerid player))
85 (defun squeeze-update-power (player 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"))))))
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))))
101 (require 'notifications)
103 (defun squeeze-update-state-from-line (string)
105 ((string-match "^players 0" string)
106 (setq squeeze-players (squeeze-parse-players-line string))
108 ((string-match "^syncgroups" string)
109 (setq squeeze-syncgroups (squeeze-parse-syncgroups-line string))
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))))
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)))
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))
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))
131 (defface squeeze-player-face
133 "Face for displaying players"
135 (defface squeeze-player-on-face
136 '((t :weight bold :inherit squeeze-player-face))
137 "Face for displaying players which are on"
139 (defface squeeze-player-off-face
140 '((t :weight light :inherit squeeze-player-face))
141 "Face for displaying players which are off"
144 (defface squeeze-mixer-face
146 "Face for displaying mixer information"
148 (defface squeeze-mixer-muted-face
149 '((t :weight light :inherit squeeze-mixer-face))
150 "Face for displaying mixer information when muted"
152 (defface squeeze-mixer-quiet-face
153 '((t :foreground "green3" :inherit squeeze-mixer-face))
154 "Face for quiet volume"
156 (defface squeeze-mixer-medium-face
157 '((t :foreground "gold" :inherit squeeze-mixer-face))
158 "Face for medium volume"
160 (defface squeeze-mixer-loud-face
161 '((t :foreground "OrangeRed1" :inherit squeeze-mixer-face))
162 "Face for loud volume"
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
169 "Face for syncgroups"
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))))
178 (make-string nfull ?█)
179 (if (= width (+ nfull nblank))
181 (string (aref " ▏▎▍▌▋▊▉█" (floor (+ frac 0.0625) 0.125))))
182 (make-string nblank ? ))))
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))))
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))))
194 (defvar squeeze-players ())
195 (defvar squeeze-syncgroups ())
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)))
204 (comint-send-string process string)
205 (error "can't find squeeze process"))))
207 (defun squeeze-control-query-syncgroups ()
209 (squeeze-send-string "syncgroups ?"))
211 (defun squeeze-control-query-players ()
213 (squeeze-send-string "players 0"))
215 (defun squeeze-control-toggle-power (&optional id)
218 (setq id (get-text-property (point) 'squeeze-playerid)))
219 (squeeze-send-string "%s power" id))
221 (defun squeeze-control-play-favorite (&optional favorite id)
222 (interactive "nFavourite: ")
224 (setq id (get-text-property (point) 'squeeze-playerid)))
225 (squeeze-send-string "%s favorites playlist play item_id:%d" id favorite))
227 (defun squeeze-control-query-power (&optional id)
230 (setq id (get-text-property (point) 'squeeze-playerid)))
232 (squeeze-send-string "%s power ?" id)))
234 (defun squeeze-control-volume-up (&optional id inc)
236 (unless inc (setq inc 5))
238 (setq id (get-text-property (point) 'squeeze-playerid)))
240 (squeeze-send-string "%s mixer volume %+d" id inc)))
242 (defun squeeze-control-volume-down (&optional id inc)
244 (unless inc (setq inc 5))
246 (setq id (get-text-property (point) 'squeeze-playerid)))
248 (squeeze-send-string "%s mixer volume %+d" id (- inc))))
250 (defun squeeze-control-volume-set (id val)
252 (squeeze-send-string "%s mixer volume %d" id val))
254 (defun squeeze-control-query-mixer-volume (&optional id)
257 (setq id (get-text-property (point) 'squeeze-playerid)))
259 (squeeze-send-string "%s mixer volume ?" id)))
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))))
267 (defun squeeze-control-listen ()
268 (squeeze-send-string "listen 1"))
270 (defun squeeze-accept-process-output ()
271 (while (accept-process-output (get-buffer-process "*squeeze*") 0.1 nil t)))
273 (defun squeeze-control-refresh ()
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))
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)
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))))
296 (defun squeeze-control-mixer-mouse-1 (event)
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)))
303 (defun squeeze-control-mixer-set-volume ()
305 (let* ((val (squeeze-control-compute-volume (point)))
306 (id (get-text-property (point) 'squeeze-playerid)))
307 (squeeze-control-volume-set id val)))
309 (defvar squeeze-control-display-syncgroups nil)
311 (defun squeeze-control-toggle-syncgroup-display ()
313 (setf squeeze-control-display-syncgroups
314 (not squeeze-control-display-syncgroups))
315 (squeeze-control-display-players))
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)
323 (squeeze-mixer-make-bar (squeeze-player-volume player) 28)
324 'squeeze-playerid (squeeze-player-playerid player)
325 'keymap squeeze-control-mixer-map
327 'rear-nonsticky '(keymap))))
328 (insert (propertize "\n" 'intangible t)))
330 (defun squeeze-control-display-players ()
332 (with-current-buffer (get-buffer-create "*squeeze-control*")
333 (let ((saved (point)))
334 (squeeze-control-mode)
338 (squeeze-control-display-syncgroups
339 (let ((syncgroups squeeze-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)))))
356 (dolist (player squeeze-players)
357 (squeeze-control-insert-player player))
361 (cl-defstruct (squeeze-player (:constructor squeeze-make-player))
362 playerindex playerid uuid ip name model isplayer displaytype canpoweroff connected power volume)
364 (defun squeeze-string-plistify (string start end)
366 (setq end (length string)))
370 (if (string-match "\\([a-z_]+\\)%3A\\([^ \n]+\\)" string start)
371 (let ((mend (match-end 0)))
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)))
383 (defun squeeze-parse-syncgroups-line (string)
384 (let ((syncgroupspos (string-match "^syncgroups " string))
385 (startpos (match-end 0)))
387 (squeeze-string-plistify string startpos (length string)))))
389 (defun squeeze-parse-count (string)
391 (let ((countpos (string-match "count%3A\\([0-9]*\\)\\>" string)))
394 (substring string (match-beginning 1) (match-end 1)))
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)
401 (defun squeeze-parse-players-line (string)
402 (let ((count (squeeze-parse-count string))
403 (startpos (string-match "playerindex" string))
406 (dotimes (i (1- count))
407 (setq endpos (progn (string-match " connected%3A[0-1] " string startpos)
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))
415 (defun squeeze-complete-command-at-point ()
417 (list (progn (backward-word) (point))
418 (progn (forward-word) (point))
420 (mapcar 'squeeze-player-playerid squeeze-players)
421 '(;; General commands and queries
422 "login" "can" "version" "listen" "subscribe" "pref"
423 "logging" "getstring" "setsncredentials" "debug"
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"
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"
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"
450 "serverstatus" "status" "displaystatus" "readdirectory"
454 ;; Alarm commands and queries
457 ;; Plugins commands and queries
461 (defun squeeze-read-server-parameters (address port)
462 (let ((host (read-string "Host: " nil nil address))
463 (port (read-number "Port: " port)))
466 (defun squeeze (&optional address port)
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)
478 (defun squeeze-control (&optional address port)
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)))