(defcustom squeeze-server-port 9090
"Port number for the Squeezebox server"
:group 'squeeze)
-
+(defcustom squeeze-server-http-port 9000
+ "Port number for the Squeezebox HTTP server"
+ :group 'squeeze)
(defvar squeeze-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "TAB") 'completion-at-point)
map))
+(defun squeeze-unhex-string (string)
+ (with-temp-buffer
+ (let ((case-fold-search t)
+ (start 0))
+ (while (string-match "%[0-9a-f][0-9a-f]" string start)
+ (let* ((s (match-beginning 0))
+ (ch1 (url-unhex (elt string (+ s 1))))
+ (code (+ (* 16 ch1)
+ (url-unhex (elt string (+ s 2))))))
+ (insert (substring string start s)
+ (byte-to-string code))
+ (setq start (match-end 0))))
+ (insert (substring string start)))
+ (buffer-string)))
+
(defun squeeze-unhex-and-decode-utf8-string (string)
- (decode-coding-string (url-unhex-string string) 'utf-8))
+ (decode-coding-string (squeeze-unhex-string string) 'utf-8))
(define-derived-mode squeeze-mode comint-mode "Squeeze"
"Major mode for interacting with the Squeezebox Server CLI.\\<squeeze-mode-map>"
(define-key map (kbd "+") 'squeeze-control-volume-up)
(define-key map (kbd "-") 'squeeze-control-volume-down)
(define-key map (kbd "t") 'squeeze-control-toggle-syncgroup-display)
+ (define-key map (kbd ">") 'squeeze-control-next-track)
+ (define-key map (kbd "<") 'squeeze-control-previous-track)
+ (define-key map (kbd "s") 'squeeze-control-select-player)
+ (define-key map (kbd "a") 'squeeze-list-albums)
map))
+(defvar squeeze-control-current-player nil)
+
+(defun squeeze-control-select-player (id)
+ (interactive
+ (list (or (get-text-property (point) 'squeeze-playerid)
+ (let ((name (completing-read "Select player: " (mapcar 'squeeze-player-name squeeze-players))))
+ (squeeze-player-playerid (squeeze-find-player-from-name name))))))
+ (setq squeeze-control-current-player id))
+
+(defun squeeze-control-next-track ()
+ (interactive)
+ (squeeze-send-string "%s playlist index +1" squeeze-control-current-player))
+
+(defun squeeze-control-previous-track ()
+ (interactive)
+ (squeeze-send-string "%s playlist index -1" squeeze-control-current-player))
+
(define-derived-mode squeeze-control-mode special-mode "SqueezeControl"
"Major mode for controlling Squeezebox Servers.\\<squeeze-control-mode-map>")
(defvar squeeze-control-inhibit-display nil)
-(defun squeeze-update-state (string)
- (let (done-something)
- (dolist (line (split-string string "\n"))
- (when (squeeze-update-state-from-line line)
- (setq done-something t)))
- (when done-something
- (unless squeeze-control-inhibit-display
- (squeeze-control-display-players))))
- string)
+(lexical-let ((buffer (get-buffer-create " *squeeze-update-state*")))
+ (defun squeeze-update-state (string)
+ ;; FIXME: we could make this a lot more elegant by using the
+ ;; buffer abstraction more
+ (if (cl-position ?\n string)
+ (let (done-something)
+ (with-current-buffer buffer
+ (insert string)
+ (setq string (buffer-string))
+ (erase-buffer))
+ (dolist (line (split-string string "\n"))
+ (when (squeeze-update-state-from-line line)
+ (setq done-something t)))
+ (when done-something
+ (unless squeeze-control-inhibit-display
+ (squeeze-control-display-players))))
+ (with-current-buffer buffer
+ (insert string)))
+ string))
(defconst squeeze-player-line-regexp
"^\\(\\(?:[0-9a-f]\\{2\\}%3A\\)\\{5\\}[0-9a-f]\\{2\\}\\) ")
(when (string= id (squeeze-player-playerid player))
(return player))))
+(defun squeeze-find-player-from-name (name)
+ (dolist (player squeeze-players)
+ (when (string= name (squeeze-player-name player))
+ (return player))))
+
(defun squeeze-update-power (player state)
(if state
(setf (squeeze-player-power player) state)
(and current (max 0 (min 100 (+ current number)))))
(setf (squeeze-player-volume player) number))))
+(require 'notifications)
+
(defun squeeze-update-state-from-line (string)
(cond
((string-match "^players 0" string)
((string-match "^syncgroups" string)
(setq squeeze-syncgroups (squeeze-parse-syncgroups-line string))
t)
+ ((string-match "^albums" string)
+ (squeeze-parse-albums-line string)
+ t)
((string-match squeeze-player-line-regexp string)
(let ((substring (substring string (match-end 0)))
(id (url-unhex-string (match-string 1 string))))
(cond
+ ((string-match "^playlist newsong \\(.*\\) \\([0-9]+\\)$" substring)
+ (let ((value (save-match-data (url-unhex-string (match-string 1 substring))))
+ (index (url-unhex-string (match-string 2 substring))))
+ (notifications-notify :title "Now playing" :body (encode-coding-string (format "%s: %s" index value) 'utf-8)))
+ t)
((string-match "^power\\(?: \\([01]\\)\\)?" substring)
(let ((state (match-string 1 substring))
(player (squeeze-find-player id)))
(make-string nfull ?█)
(if (= width (+ nfull nblank))
""
- (aref " ▏▎▍▌▋▊▉█" (floor (+ frac 0.0625) 0.125)))
+ (string (aref " ▏▎▍▌▋▊▉█" (floor (+ frac 0.0625) 0.125))))
(make-string nblank ? ))))
(defun squeeze-mixer-make-bar (vol width)
(let ((syncgroups squeeze-syncgroups)
(seen))
(while syncgroups
- (let ((names (getf syncgroups :names))
- (members (split-string (getf syncgroups :members) ",")))
+ (let ((names (getf syncgroups :sync_member_names))
+ ;; new server version has sync_members and sync_member_names
+ (members (split-string (getf syncgroups :sync_members) ",")))
(insert (propertize names 'face 'squeeze-syncgroup-face) "\n")
(dolist (member members)
(let ((player (squeeze-find-player member)))
playerindex playerid uuid ip name model isplayer displaytype canpoweroff connected power volume)
(defun squeeze-string-plistify (string start end)
+ (unless end
+ (setq end (length string)))
(save-match-data
(let (result)
(loop
- (if (string-match "\\([a-z]+\\)%3A\\([^ \n]+\\)" string start)
+ (if (string-match "\\([a-z_]+\\)%3A\\([^ \n]+\\)" string start)
(let ((mend (match-end 0)))
(when (> mend end)
(return))
(push (intern (format ":%s" (substring string (match-beginning 1) (match-end 1)))) result)
- (push (url-unhex-string (substring string (match-beginning 2) (match-end 2))) result)
+ (push (decode-coding-string
+ (url-unhex-string (substring string (match-beginning 2) (match-end 2)))
+ 'utf-8)
+ result)
(setq start mend))
(return)))
(nreverse result))))
(when startpos
(squeeze-string-plistify string startpos (length string)))))
+(defun squeeze-parse-count (string)
+ (save-match-data
+ (let ((countpos (string-match "count%3A\\([0-9]*\\)\\>" string)))
+ (if countpos
+ (string-to-number
+ (substring string (match-beginning 1) (match-end 1)))
+ (let ((kind
+ (progn (string-match "^\\([a-z]*\\) " string)
+ (substring string (match-beginning 1) (match-end 1)))))
+ (message "no count found in %s line" kind)
+ nil)))))
+
(defun squeeze-parse-players-line (string)
- (let ((countpos (string-match " count%3A\\([0-9]\\) " string))
- (startpos (match-end 0)))
- (unless countpos
- (message "no count found in players line"))
- (let ((count (string-to-number (substring string (match-beginning 1) (match-end 1))))
- result endpos)
+ (let ((count (squeeze-parse-count string))
+ (startpos (string-match "playerindex" string))
+ result endpos)
+ (when (> count 0)
(dotimes (i (1- count))
(setq endpos (progn (string-match " connected%3A[0-1] " string startpos)
(match-end 0)))
(push (apply 'squeeze-make-player (squeeze-string-plistify string startpos endpos)) result)
(setq startpos endpos))
- (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos (length string))) result)
- result)))
+ (push (apply 'squeeze-make-player (squeeze-string-plistify string startpos (length string))) result))
+ result))
+
+(defcustom squeeze-artwork-directory "~/.emacs.d/squeeze/artwork/"
+ "Base directory for album and track artwork")
+
+(defvar squeeze-albums nil)
+
+(cl-defstruct (squeeze-album (:constructor squeeze-make-album))
+ id album artwork_track_id artist index)
+
+(defun squeeze-parse-albums-line (string)
+ (let ((count (squeeze-parse-count string))
+ (countpos (string-match "\\_<count%3a" string))
+ (start-index-pos (progn (string-match "^albums \\([0-9]+\\)\\>" string)
+ (match-beginning 1)))
+ index start end)
+ (unless squeeze-albums
+ (setq squeeze-albums (make-vector count nil)))
+ (when start-index-pos
+ (setq index (string-to-number (substring string start-index-pos)))
+ (setq start (string-match "\\_<id%3a" string start-index-pos))
+ (while start
+ (setq end (string-match "\\_<\\(id%3a\\|count%3a\\)" string (1+ start)))
+ (aset squeeze-albums index
+ (apply 'squeeze-make-album
+ :index index
+ (squeeze-string-plistify string start end)))
+ (incf index)
+ (setq start (if (= end countpos) nil end))))))
+
+(defun squeeze-get-albums ()
+ (squeeze-send-string "albums 0 1000 tags:lja")
+ (squeeze-accept-process-output))
+
+(defvar squeeze-albums-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "RET") 'squeeze-albums-load-album)
+ map))
+
+(define-derived-mode squeeze-albums-mode tabulated-list-mode "SqueezeAlbums"
+ "Major mode for displaying Albums from Squeezebox Servers.\\<squeeze-albums-mode-map>"
+ (setq tabulated-list-format [("Cover" 10 nil) ("Title" 30 t)])
+ (add-hook 'tabulated-list-revert-hook 'squeeze-get-albums nil t)
+ (setq tabulated-list-entries 'squeeze-albums-tabulated-list-entries)
+ (tabbar-mode -1)
+ (setq tabulated-list-use-header-line nil)
+ (tabulated-list-init-header))
+
+(defun squeeze-albums-artwork-callback (status artwork_track_id filename)
+ (unless status
+ (goto-char 1)
+ (let ((end (search-forward "\n\n")))
+ (delete-region 1 end)
+ (write-file (format "%s%s/%s" squeeze-artwork-directory artwork_track_id filename))
+ (kill-buffer))))
+
+(defun squeeze-albums-tabulated-list-entry (x)
+ (let* ((ati (squeeze-album-artwork_track_id x))
+ (file (format "%s%s/cover_50x50_o.jpg" squeeze-artwork-directory ati)))
+ (unless (file-exists-p file)
+ (make-directory (format "%s%s" squeeze-artwork-directory ati) t)
+ (let ((url (format "http://%s:%s/music/%s/cover_50x50_o.jpg"
+ squeeze-server-address squeeze-server-http-port ati)))
+ (url-queue-retrieve url 'squeeze-albums-artwork-callback
+ (list ati "cover_50x50_o.jpg") t t)))
+ (list x (vector (propertize (format "%s" ati)
+ 'display `(when (file-exists-p ,file)
+ image :type jpeg :file ,file))
+ (squeeze-album-album x)))))
+
+(defun squeeze-albums-tabulated-list-entries ()
+ (mapcar 'squeeze-albums-tabulated-list-entry (append squeeze-albums nil)))
(defun squeeze-complete-command-at-point ()
(save-excursion
(list (progn (backward-word) (point))
(progn (forward-word) (point))
- '(;; General commands and queries
- "login" "can" "version" "listen" "subscribe" "pref"
- "logging" "getstring" "setsncredentials" "debug"
- "exit" "shutdown"
-
- ;; Player commands and queries
- "player" "count" "id" "uuid" "name" "ip" "model" "isplayer"
- "displaytype" "canpoweroff" "?" "signalstrength" "connected"
- "sleep" "sync" "syncgroups" "power" "mixer" "volume" "muting"
- "bass" "treble" "pitch" "show" "display" "linesperscreen"
- "displaynow" "playerpref" "button" "ir" "irenable"
- "connect" "client" "forget" "disconnect" "players"
-
- ;; Database commands and queries
- "rescan" "rescanprogress" "abortscan" "wipecache" "info"
- "total" "genres" "artists" "albums" "songs" "years"
- "musicfolder" "playlists" "tracks" "new" "rename" "delete"
- "edit" "songinfo" "titles" "search" "pragma"
-
- ;; Playlist commands and queries
- "play" "stop" "pause" "mode" "time" "genre" "artist" "album"
- "title" "duration" "remote" "current_title" "path" "playlist"
- "add" "insert" "deleteitem" "move" "delete" "preview" "resume"
- "save" "loadalbum" "addalbum" "loadtracks" "addtracks"
- "insertalbum" "deletealbum" "clear" "zap" "name" "url"
- "modified" "playlistsinfo" "index" "shuffle" "repeat"
- "playlistcontrol"
-
- ;; Compound queries
- "serverstatus" "status" "displaystatus" "readdirectory"
-
- ;; Notifications
-
- ;; Alarm commands and queries
- "alarm" "alarms"
-
- ;; Plugins commands and queries
- "favorites"
- ))))
-
-(defun squeeze ()
+ (append
+ (mapcar 'squeeze-player-playerid squeeze-players)
+ '(;; General commands and queries
+ "login" "can" "version" "listen" "subscribe" "pref"
+ "logging" "getstring" "setsncredentials" "debug"
+ "exit" "shutdown"
+
+ ;; Player commands and queries
+ "player" "count" "id" "uuid" "name" "ip" "model" "isplayer"
+ "displaytype" "canpoweroff" "?" "signalstrength" "connected"
+ "sleep" "sync" "syncgroups" "power" "mixer" "volume" "muting"
+ "bass" "treble" "pitch" "show" "display" "linesperscreen"
+ "displaynow" "playerpref" "button" "ir" "irenable"
+ "connect" "client" "forget" "disconnect" "players"
+
+ ;; Database commands and queries
+ "rescan" "rescanprogress" "abortscan" "wipecache" "info"
+ "total" "genres" "artists" "albums" "songs" "years"
+ "musicfolder" "playlists" "tracks" "new" "rename" "delete"
+ "edit" "songinfo" "titles" "search" "pragma"
+
+ ;; Playlist commands and queries
+ "play" "stop" "pause" "mode" "time" "genre" "artist" "album"
+ "title" "duration" "remote" "current_title" "path" "playlist"
+ "add" "insert" "deleteitem" "move" "delete" "preview" "resume"
+ "save" "loadalbum" "addalbum" "loadtracks" "addtracks"
+ "insertalbum" "deletealbum" "clear" "zap" "name" "url"
+ "modified" "playlistsinfo" "index" "shuffle" "repeat"
+ "playlistcontrol"
+
+ ;; Compound queries
+ "serverstatus" "status" "displaystatus" "readdirectory"
+
+ ;; Notifications
+
+ ;; Alarm commands and queries
+ "alarm" "alarms"
+
+ ;; Plugins commands and queries
+ "favorites"
+ )))))
+
+(defun squeeze-read-server-parameters (address port)
+ (let ((host (read-string "Host: " nil nil address))
+ (port (read-number "Port: " port)))
+ (cons host port)))
+
+(defun squeeze (&optional address port)
(interactive)
- (let ((buffer (make-comint-in-buffer "squeeze" nil
- (cons squeeze-server-address
- squeeze-server-port))))
+ (unless address (setq address squeeze-server-address))
+ (unless port (setq port squeeze-server-port))
+ (when current-prefix-arg
+ (let ((parameters (squeeze-read-server-parameters address port)))
+ (setq address (car parameters)
+ port (cdr parameters))))
+ (let ((buffer (make-comint-in-buffer "squeeze" nil (cons address port))))
(switch-to-buffer buffer)
(squeeze-mode)))
-(defun squeeze-control ()
+(defun squeeze-control (&optional address port)
(interactive)
- (squeeze)
+ (unless address (setq address squeeze-server-address))
+ (unless port (setq port squeeze-server-port))
+ (when current-prefix-arg
+ (let ((parameters (squeeze-read-server-parameters address port)))
+ (setq address (car parameters)
+ port (cdr parameters))))
+ (let ((current-prefix-arg nil))
+ (squeeze address port))
(let ((buffer (get-buffer-create "*squeeze-control*")))
(switch-to-buffer buffer)
(squeeze-control-listen)
(squeeze-control-refresh)
(squeeze-control-display-players)))
+(defun squeeze-list-albums ()
+ (interactive)
+ (squeeze-get-albums)
+ (let ((buffer (get-buffer-create "*squeeze-albums*")))
+ (switch-to-buffer buffer)
+ (squeeze-albums-mode)
+ (tabulated-list-print)))
+
+(defun squeeze-albums-load-album ()
+ (interactive)
+ (squeeze-send-string "%s playlistcontrol cmd:load album_id:%s"
+ squeeze-control-current-player
+ (squeeze-album-id (tabulated-list-get-id))))
+
(provide 'squeeze)