diff options
Diffstat (limited to 'lisp/mpc.el')
| -rw-r--r-- | lisp/mpc.el | 228 |
1 files changed, 166 insertions, 62 deletions
diff --git a/lisp/mpc.el b/lisp/mpc.el index 825eb3c05d4..af1aac93f14 100644 --- a/lisp/mpc.el +++ b/lisp/mpc.el @@ -1,6 +1,6 @@ -;;; mpc.el --- A client for the Music Player Daemon -*- coding: utf-8; lexical-binding: t -*- +;;; mpc.el --- A client for the Music Player Daemon -*- lexical-binding: t -*- -;; Copyright (C) 2006-2013 Free Software Foundation, Inc. +;; Copyright (C) 2006-2015 Free Software Foundation, Inc. ;; Author: Stefan Monnier <monnier@iro.umontreal.ca> ;; Keywords: multimedia @@ -44,7 +44,6 @@ ;; - visual feedback for drag'n'drop ;; - display/set `repeat' and `random' state (and maybe also `crossfade'). ;; - allow multiple *mpc* sessions in the same Emacs to control different mpds. -;; - look for .folder.png (freedesktop) or folder.jpg (XP) as well. ;; - fetch album covers and lyrics from the web? ;; - improve MPC-Status: better volume control, add a way to show/hide the ;; rest, plus add the buttons currently in the toolbar. @@ -92,7 +91,9 @@ ;; UI-commands : mpc- ;; internal : mpc-- -(eval-when-compile (require 'cl-lib)) +(eval-when-compile + (require 'cl-lib) + (require 'subr-x)) (defgroup mpc () "Client for the Music Player Daemon (mpd)." @@ -217,7 +218,7 @@ defaults to 6600 and HOST defaults to localhost." (goto-char (point-max)) (insert-before-markers ;So it scrolls. (replace-regexp-in-string "\n" "\n " - (apply 'format format args)) + (apply #'format-message format args)) "\n")))) (defun mpc--proc-filter (proc string) @@ -253,6 +254,7 @@ defaults to 6600 and HOST defaults to localhost." (defun mpc--proc-connect (host) (let ((port 6600) + local pass) (when (string-match "\\`\\(?:\\(.*\\)@\\)?\\(.*?\\)\\(?::\\(.*\\)\\)?\\'" @@ -267,6 +269,11 @@ defaults to 6600 and HOST defaults to localhost." (if (string-match "[^[:digit:]]" v) (string-to-number v) v))))) + (when (file-name-absolute-p host) + ;; Expand file name because `file-name-absolute-p' + ;; considers paths beginning with "~" as absolute + (setq host (expand-file-name host)) + (setq local t)) (mpc--debug "Connecting to %s:%s..." host port) (with-current-buffer (get-buffer-create (format " *mpc-%s:%s*" host port)) @@ -279,7 +286,10 @@ defaults to 6600 and HOST defaults to localhost." (let* ((coding-system-for-read 'utf-8-unix) (coding-system-for-write 'utf-8-unix) (proc (condition-case err - (open-network-stream "MPC" (current-buffer) host port) + (make-network-process :name "MPC" :buffer (current-buffer) + :host (unless local host) + :service (if local host port) + :family (if local 'local)) (error (user-error (error-message-string err)))))) (when (processp mpc-proc) ;; Inherit the properties of the previous connection. @@ -491,10 +501,13 @@ to call FUN for any change whatsoever.") (cancel-timer mpc--status-timer) (setq mpc--status-timer nil))) (defun mpc--status-timer-run () - (condition-case err - (when (process-get (mpc-proc) 'ready) - (with-local-quit (mpc-status-refresh))) - (error (message "MPC: %s" err)))) + (with-demoted-errors "MPC: %S" + (when (process-get (mpc-proc) 'ready) + (let* ((buf (mpc-proc-buffer (mpc-proc) 'status)) + (win (get-buffer-window buf t))) + (if (not win) + (mpc--status-timer-stop) + (with-local-quit (mpc-status-refresh))))))) (defvar mpc--status-idle-timer nil) (defun mpc--status-idle-timer-start () @@ -519,11 +532,8 @@ to call FUN for any change whatsoever.") ;; client starts playback, we may get a chance to notice it. (run-with-idle-timer 10 t 'mpc--status-idle-timer-run)))) (defun mpc--status-idle-timer-run () - (when (process-get (mpc-proc) 'ready) - (condition-case err - (with-local-quit (mpc-status-refresh)) - (error (message "MPC: %s" err)))) - (mpc--status-timer-start)) + (mpc--status-timer-start) + (mpc--status-timer-run)) (defun mpc--status-timers-refresh () "Start/stop the timers according to whether a song is playing." @@ -786,6 +796,22 @@ The songs are returned as alists." ;; (setq mpc-queue-back nil mpc-queue nil) ) +(defun mpc-cmd-consume (&optional arg) + "Set consume mode state." + (mpc-proc-cmd (list "consume" arg) #'mpc-status-refresh)) + +(defun mpc-cmd-random (&optional arg) + "Set random (shuffle) mode state." + (mpc-proc-cmd (list "random" arg) #'mpc-status-refresh)) + +(defun mpc-cmd-repeat (&optional arg) + "Set repeat mode state." + (mpc-proc-cmd (list "repeat" arg) #'mpc-status-refresh)) + +(defun mpc-cmd-single (&optional arg) + "Set single mode state." + (mpc-proc-cmd (list "single" arg) #'mpc-status-refresh)) + (defun mpc-cmd-pause (&optional arg callback) "Pause or resume playback of the queue of songs." (let ((cb callback)) @@ -891,9 +917,7 @@ If PLAYLIST is t or nil or missing, use the main playlist." :type '(choice (const nil) directory)) (defcustom mpc-data-directory - (if (and (not (file-directory-p "~/.mpc")) - (file-directory-p "~/.emacs.d")) - "~/.emacs.d/mpc" "~/.mpc") + (locate-user-emacs-file "mpc" ".mpc") "Directory where MPC.el stores auxiliary data." :type 'directory) @@ -905,8 +929,13 @@ If PLAYLIST is t or nil or missing, use the main playlist." (defun mpc-file-local-copy (file) ;; Try to set mpc-mpd-music-directory. (when (and (null mpc-mpd-music-directory) - (string-match "\\`localhost" mpc-host)) - (let ((files '("~/.mpdconf" "/etc/mpd.conf")) + (or (string-match "\\`localhost" mpc-host) + (file-name-absolute-p mpc-host))) + (let ((files `(,(let ((xdg (getenv "XDG_CONFIG_HOME"))) + (concat (if (and xdg (file-name-absolute-p xdg)) + xdg "~/.config") + "/mpd/mpd.conf")) + "~/.mpdconf" "~/.mpd/mpd.conf" "/etc/mpd.conf")) file) (while (and files (not file)) (if (file-exists-p (car files)) (setq file (car files))) @@ -997,35 +1026,42 @@ If PLAYLIST is t or nil or missing, use the main playlist." (substring time (match-end 0)) time))))) (`Cover - (let* ((dir (file-name-directory (cdr (assq 'file info)))) - (cover (concat dir "cover.jpg")) - (file (condition-case err - (mpc-file-local-copy cover) - (error (message "MPC: %s" err)))) - image) + (let ((dir (file-name-directory + (mpc-file-local-copy (cdr (assq 'file info)))))) ;; (debug) (push `(equal ',dir (file-name-directory (cdr (assq 'file info)))) pred) - (if (null file) - ;; Make sure we return something on which we can - ;; place the `mpc-pred' property, as - ;; a negative-cache. We could also use - ;; a default cover. - (progn (setq size nil) " ") - (if (null size) (setq image (create-image file)) - (let ((tempfile (make-temp-file "mpc" nil ".jpg"))) - (call-process "convert" nil nil nil - "-scale" size file tempfile) - (setq image (create-image tempfile)) - (mpc-tempfiles-add image tempfile))) - (setq size nil) - (propertize dir 'display image)))) + (if-let ((covers '(".folder.png" "cover.jpg" "folder.jpg")) + (cover (cl-loop for file in (directory-files dir) + if (member (downcase file) covers) + return (concat dir file))) + (file (with-demoted-errors "MPC: %s" + (mpc-file-local-copy cover)))) + (let (image) + (if (null size) (setq image (create-image file)) + (let ((tempfile (make-temp-file "mpc" nil ".jpg"))) + (call-process "convert" nil nil nil + "-scale" size file tempfile) + (setq image (create-image tempfile)) + (mpc-tempfiles-add image tempfile))) + (setq size nil) + (propertize dir 'display image)) + ;; Make sure we return something on which we can + ;; place the `mpc-pred' property, as + ;; a negative-cache. We could also use + ;; a default cover. + (progn (setq size nil) " ")))) (_ (let ((val (cdr (assq tag info)))) ;; For Streaming URLs, there's no other info ;; than the URL in `file'. Pretend it's in `Title'. (when (and (null val) (eq tag 'Title)) (setq val (cdr (assq 'file info)))) (push `(equal ',val (cdr (assq ',tag info))) pred) - val))))) + (cond + ((not (and (eq tag 'Date) (stringp val))) val) + ;; For "date", only keep the year! + ((string-match "[0-9]\\{4\\}" val) + (match-string 0 val)) + (t val))))))) (space (when size (setq size (string-to-number size)) (propertize " " 'display @@ -1070,8 +1106,7 @@ If PLAYLIST is t or nil or missing, use the main playlist." ;;; The actual UI code ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defvar mpc-mode-map - (let ((map (make-keymap))) - (suppress-keymap map) + (let ((map (make-sparse-keymap))) ;; (define-key map "\e" 'mpc-stop) (define-key map "q" 'mpc-quit) (define-key map "\r" 'mpc-select) @@ -1090,11 +1125,28 @@ If PLAYLIST is t or nil or missing, use the main playlist." ;; is applied elsewhere :-( ;; (define-key map [(double mouse-2)] 'mpc-play-at-point) (define-key map "p" 'mpc-pause) + (define-key map "s" 'mpc-toggle-play) + (define-key map ">" 'mpc-next) + (define-key map "<" 'mpc-prev) + (define-key map "g" nil) map)) (easy-menu-define mpc-mode-menu mpc-mode-map "Menu for MPC.el." '("MPC.el" + ["Play/Pause" mpc-toggle-play] ;FIXME: Add one of ⏯/▶/⏸ in there? + ["Next Track" mpc-next] ;FIXME: Add ⇥ there? + ["Previous Track" mpc-prev] ;FIXME: Add ⇤ there? + "--" + ["Repeat Playlist" mpc-toggle-repeat :style toggle + :selected (member '(repeat . "1") mpc-status)] + ["Shuffle Playlist" mpc-toggle-shuffle :style toggle + :selected (member '(random . "1") mpc-status)] + ["Repeat Single Track" mpc-toggle-single :style toggle + :selected (member '(single . "1") mpc-status)] + ["Consume Mode" mpc-toggle-consume :style toggle + :selected (member '(consume . "1") mpc-status)] + "--" ["Add new browser" mpc-tagbrowser] ["Update DB" mpc-update] ["Quit" mpc-quit])) @@ -1138,11 +1190,11 @@ If PLAYLIST is t or nil or missing, use the main playlist." :help "Append to the playlist") map)) -(define-derived-mode mpc-mode fundamental-mode "MPC" +(define-derived-mode mpc-mode special-mode "MPC" "Major mode for the features common to all buffers of MPC." (buffer-disable-undo) - (setq buffer-read-only t) - (setq-local tool-bar-map mpc-tool-bar-map) + (if (boundp 'tool-bar-map) ; not if --without-x + (setq-local tool-bar-map mpc-tool-bar-map)) (setq-local truncate-lines t)) ;;; The mpc-status-mode buffer ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1243,7 +1295,7 @@ If PLAYLIST is t or nil or missing, use the main playlist." (let ((ol (make-overlay (line-beginning-position) (line-beginning-position 2)))) (overlay-put ol 'mpc-select t) - (overlay-put ol 'face 'region) + (overlay-put ol 'face 'highlight) (overlay-put ol 'evaporate t) (push ol mpc-select))) @@ -1542,7 +1594,7 @@ when constructing the set of constraints." (move-overlay mpc-tagbrowser-all-ol (point) (line-beginning-position 2)) (let ((ol (make-overlay (point) (line-beginning-position 2)))) - (overlay-put ol 'face 'region) + (overlay-put ol 'face 'highlight) (overlay-put ol 'evaporate t) (setq-local mpc-tagbrowser-all-ol ol)))))) @@ -1619,7 +1671,7 @@ Return non-nil if a selection was deactivated." (setq active (if (listp active) (mpc-intersection active vals) vals)))) - (when (and (listp active)) + (when (listp active) ;; Remove the selections if they are all in conflict with ;; other constraints. (let ((deactivate t)) @@ -1633,7 +1685,13 @@ Return non-nil if a selection was deactivated." (setq selection nil) (mapc 'delete-overlay mpc-select) (setq mpc-select nil) - (mpc-tagbrowser-all-select))))) + (mpc-tagbrowser-all-select)))) + + ;; Don't bother splitting the "active" elements to the first part if + ;; they're the same as the selection. + (when (equal (sort (copy-sequence active) #'string-lessp) + (sort (copy-sequence selection) #'string-lessp)) + (setq active 'all))) ;; FIXME: This `mpc-sort' takes a lot of time. Maybe we should ;; be more clever and presume the buffer is mostly sorted already. @@ -1742,7 +1800,7 @@ A value of t means the main playlist.") (completing-read "Rename playlist: " (mpc-cmd-list 'Playlist) nil 'require-match))) - (newname (read-string (format "Rename '%s' to: " oldname)))) + (newname (read-string (format-message "Rename `%s' to: " oldname)))) (if (zerop (length newname)) (error "Aborted") (list oldname newname)))) @@ -1796,7 +1854,10 @@ A value of t means the main playlist.") ;; Maintain the volume. (setq mpc-volume (mpc-volume-widget - (string-to-number (cdr (assq 'volume mpc-status)))))) + (string-to-number (cdr (assq 'volume mpc-status))))) + (let ((status-buf (mpc-proc-buffer (mpc-proc) 'status))) + (when (buffer-live-p status-buf) + (with-current-buffer status-buf (force-mode-line-update))))) (defvar mpc-volume-step 5) @@ -1811,9 +1872,14 @@ A value of t means the main playlist.") (char-after (posn-point posn)))) '(?◁ ?<)) (- mpc-volume-step) mpc-volume-step)) - (newvol (+ (string-to-number (cdr (assq 'volume mpc-status))) diff))) - (mpc-proc-cmd (list "setvol" newvol) 'mpc-status-refresh) - (message "Set MPD volume to %s%%" newvol))) + (curvol (string-to-number (cdr (assq 'volume mpc-status)))) + (newvol (max 0 (min 100 (+ curvol diff))))) + (if (= newvol curvol) + (progn + (message "MPD volume already at %s%%" newvol) + (ding)) + (mpc-proc-cmd (list "setvol" newvol) 'mpc-status-refresh) + (message "Set MPD volume to %s%%" newvol)))) (defun mpc-volume-widget (vol &optional size) (unless size (setq size 12.5)) @@ -1849,7 +1915,6 @@ A value of t means the main playlist.") (defvar mpc-songs-mode-map (let ((map (make-sparse-keymap))) - (set-keymap-parent map mpc-mode-map) (define-key map [remap mpc-select] 'mpc-songs-jump-to) map)) @@ -1861,7 +1926,7 @@ This is used so that they can be compared with `eq', which is needed for `text-property-any'.") (defun mpc-songs-hashcons (name) (or (gethash name mpc-songs-hashcons) (puthash name name mpc-songs-hashcons))) -(defcustom mpc-songs-format "%2{Disc--}%3{Track} %-5{Time} %25{Title} %20{Album} %20{Artist} %10{Date}" +(defcustom mpc-songs-format "%2{Disc--}%3{Track} %-5{Time} %25{Title} %20{Album} %20{Artist} %5{Date}" "Format used to display each song in the list of songs." :type 'string) @@ -1927,7 +1992,7 @@ This is used so that they can be compared with `eq', which is needed for ;; I punt on it and just use file-name sorting, which does the ;; right thing if your library is properly arranged. (dolist (song (if dontsort active - (sort active + (sort (copy-sequence active) (lambda (song1 song2) (let ((cmp (mpc-compare-strings (cdr (assq 'file song1)) @@ -2011,14 +2076,16 @@ This is used so that they can be compared with `eq', which is needed for posn)))) (let* ((plbuf (mpc-proc-cmd "playlist")) (re (if song-file - (concat "^\\([0-9]+\\):" (regexp-quote song-file) "$"))) + ;; Newer MPCs apparently include "file: " in the buffer. + (concat "^\\([0-9]+\\):\\(?:file: \\)?" + (regexp-quote song-file) "$"))) (sn (with-current-buffer plbuf (goto-char (point-min)) (when (and re (re-search-forward re nil t)) (match-string 1))))) (cond ((null re) (posn-set-point posn)) - ((null sn) (error "This song is not in the playlist")) + ((null sn) (user-error "This song is not in the playlist")) ((null (with-current-buffer plbuf (re-search-forward re nil t))) ;; song-file only appears once in the playlist: no ambiguity, ;; we're good to go! @@ -2295,6 +2362,30 @@ This is used so that they can be compared with `eq', which is needed for (mpc-status-stop) (if proc (delete-process proc)))) +(defun mpc-toggle-consume () + "Toggle consume mode: removing played songs from the playlist." + (interactive) + (mpc-cmd-consume + (if (string= "0" (cdr (assq 'consume (mpc-cmd-status)))) "1" "0"))) + +(defun mpc-toggle-repeat () + "Toggle repeat mode." + (interactive) + (mpc-cmd-repeat + (if (string= "0" (cdr (assq 'repeat (mpc-cmd-status)))) "1" "0"))) + +(defun mpc-toggle-single () + "Toggle single mode." + (interactive) + (mpc-cmd-single + (if (string= "0" (cdr (assq 'single (mpc-cmd-status)))) "1" "0"))) + +(defun mpc-toggle-shuffle () + "Toggle shuffling of the playlist (random mode)." + (interactive) + (mpc-cmd-random + (if (string= "0" (cdr (assq 'random (mpc-cmd-status)))) "1" "0"))) + (defun mpc-stop () "Stop playing the current queue of songs." (interactive) @@ -2312,6 +2403,16 @@ This is used so that they can be compared with `eq', which is needed for (interactive) (mpc-cmd-pause "0")) +(defun mpc-toggle-play () + "Toggle between play and pause. +If stopped, start playback." + (interactive) + (if (member (cdr (assq 'state (mpc-cmd-status))) '("stop")) + (mpc-cmd-play) + (if (member (cdr (assq 'state (mpc-cmd-status))) '("pause")) + (mpc-resume) + (mpc-pause)))) + (defun mpc-play () "Start playing whatever is selected." (interactive) @@ -2328,7 +2429,7 @@ This is used so that they can be compared with `eq', which is needed for (if (mpc-playlist-add) (if (member (cdr (assq 'state (mpc-cmd-status))) '("stop")) (mpc-cmd-play)) - (error "Don't know what to play")))) + (user-error "Don't know what to play")))) (defun mpc-next () "Jump to the next song in the queue." @@ -2592,7 +2693,8 @@ This is used so that they can be compared with `eq', which is needed for (mpc-cmd-move (let ((poss '())) (dotimes (i (length songs)) (push (+ i (length pl)) poss)) - (nreverse poss)) dest-pos mpc-songs-playlist) + (nreverse poss)) + dest-pos mpc-songs-playlist) (message "Added %d songs" (length songs))))) (mpc-songs-refresh)) (t @@ -2611,6 +2713,8 @@ This is used so that they can be compared with `eq', which is needed for (interactive (progn (if current-prefix-arg + ;; FIXME: We should provide some completion here, especially for the + ;; case where the user specifies a local socket/file name. (setq mpc-host (read-string "MPD host and port: " nil nil mpc-host))) nil)) (let* ((song-buf (mpc-songs-buf)) |
