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 (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))
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)
31 (define-derived-mode squeeze-control-mode special-mode "SqueezeControl"
32 "Major mode for controlling Squeezebox Servers.\\<squeeze-control-mode-map>")
34 (defvar squeeze-control-inhibit-display nil)
36 (defun squeeze-update-state (string)
38 (dolist (line (split-string string "\n"))
39 (when (squeeze-update-state-from-line line)
40 (setq done-something t)))
42 (unless squeeze-control-inhibit-display
43 (squeeze-control-display-players))))
46 (defconst squeeze-player-line-regexp
47 "^\\(\\(?:[0-9a-f]\\{2\\}%3A\\)\\{5\\}[0-9a-f]\\{2\\}\\) ")
49 (defun squeeze-find-player (id)
50 (dolist (player squeeze-players)
51 (when (string= id (squeeze-player-playerid player))
54 (defun squeeze-update-power (player 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"))))))
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))))
70 (defun squeeze-update-state-from-line (string)
72 ((string-match "^players 0" string)
73 (setq squeeze-players (squeeze-parse-players-line string))
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))))
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))
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))
90 (defface squeeze-player-face
92 "Face for displaying players"
94 (defface squeeze-player-on-face
95 '((t :weight bold :inherit squeeze-player-face))
96 "Face for displaying players which are on"
98 (defface squeeze-player-off-face
99 '((t :weight light :inherit squeeze-player-face))
100 "Face for displaying players which are off"
103 (defface squeeze-mixer-face
105 "Face for displaying mixer information"
107 (defface squeeze-mixer-muted-face
108 '((t :weight light :inherit squeeze-mixer-face))
109 "Face for displaying mixer information when muted"
111 (defface squeeze-mixer-quiet-face
112 '((t :foreground "green3" :inherit squeeze-mixer-face))
113 "Face for quiet volume"
115 (defface squeeze-mixer-medium-face
116 '((t :foreground "gold" :inherit squeeze-mixer-face))
117 "Face for medium volume"
119 (defface squeeze-mixer-loud-face
120 '((t :foreground "OrangeRed1" :inherit squeeze-mixer-face))
121 "Face for loud volume"
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")
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))))
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) "▉")
144 (make-string nblank ? ))))
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))))
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))))
156 (defvar squeeze-players ())
158 (defun squeeze-control-query-players ()
160 (comint-send-string (get-buffer-process "*squeeze*") (format "players 0\n")))
162 (defun squeeze-control-toggle-power (&optional id)
165 (setq id (get-text-property (point) 'squeeze-playerid)))
166 (comint-send-string (get-buffer-process "*squeeze*") (format "%s power\n" id)))
168 (defun squeeze-control-query-power (&optional id)
171 (setq id (get-text-property (point) 'squeeze-playerid)))
173 (comint-send-string (get-buffer-process "*squeeze*") (format "%s power ?\n" id))))
175 (defun squeeze-control-volume-up (&optional id inc)
177 (unless inc (setq inc 5))
179 (setq id (get-text-property (point) 'squeeze-playerid)))
181 (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume %+d\n" id inc))))
183 (defun squeeze-control-volume-down (&optional id inc)
185 (unless inc (setq inc 5))
187 (setq id (get-text-property (point) 'squeeze-playerid)))
189 (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume %+d\n" id (- inc)))))
191 (defun squeeze-control-volume-set (id val)
193 (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume %d\n" id val)))
195 (defun squeeze-control-query-mixer-volume (&optional id)
198 (setq id (get-text-property (point) 'squeeze-playerid)))
200 (comint-send-string (get-buffer-process "*squeeze*") (format "%s mixer volume ?\n" id))))
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))))
208 (defun squeeze-control-listen ()
209 (comint-send-string (get-buffer-process "*squeeze*") (format "listen 1\n")))
211 (defun squeeze-control-refresh ()
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))
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)
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))))
234 (defun squeeze-control-mixer-mouse-1 (event)
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)))
241 (defun squeeze-control-mixer-set-volume ()
243 (let* ((val (squeeze-control-compute-volume (point)))
244 (id (get-text-property (point) 'squeeze-playerid)))
245 (squeeze-control-volume-set id val)))
247 (defun squeeze-control-display-players ()
249 (let ((players squeeze-players))
250 (with-current-buffer (get-buffer-create "*squeeze-control*")
251 (squeeze-control-mode)
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
263 'rear-nonsticky '(keymap))))
264 (insert (propertize "\n" 'intangible t)))
265 (read-only-mode 1))))
267 (cl-defstruct (squeeze-player (:constructor squeeze-make-player))
268 playerindex playerid uuid ip name model isplayer displaytype canpoweroff connected power volume)
270 (defun squeeze-string-plistify (string start end)
274 (if (string-match "\\([a-z]+\\)%3A\\([^ \n]+\\)" string start)
275 (let ((mend (match-end 0)))
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)
284 (defun squeeze-parse-players-line (string)
285 (let ((countpos (string-match " count%3A\\([0-9]\\) " string))
286 (startpos (match-end 0)))
288 (message "no count found in players line"))
289 (let ((count (parse-integer string (match-beginning 1) (match-end 1)))
291 (dotimes (i (1- count))
292 (setq endpos (progn (string-match " connected%3A[0-1] " string startpos)
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)
299 (defun squeeze-complete-command-at-point ()
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"
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"
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"
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"
332 "serverstatus" "status" "displaystatus" "readdirectory"
336 ;; Alarm commands and queries
339 ;; Plugins commands and queries
345 (let ((buffer (make-comint-in-buffer "squeeze" nil
346 (cons squeeze-server-address
347 squeeze-server-port))))
348 (switch-to-buffer buffer)
351 (defun squeeze-control ()
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)))