1 ;;; mpd.el --- A complete ripoff of xwem-mpd.
3 ;; Copyright (C) 2008 - 2013 Steve Youngs
6 ;; Copyright (C) 2005 Richard Klinda
7 ;; Author: Richard Klinda <ignotus@freemail.hu>
8 ;; Zajcev Evgeny <zevlg@yandex.ru>
11 ;; Keywords: music, entertainment
13 ;; This file is NOT part of anything.
15 ;; The original xwem-mpd.el was released under the terms of the GPLv2.
16 ;; mpd.el uses the BSD licence.
18 ;; Redistribution and use in source and binary forms, with or without
19 ;; modification, are permitted provided that the following conditions
22 ;; 1. Redistributions of source code must retain the above copyright
23 ;; notice, this list of conditions and the following disclaimer.
25 ;; 2. Redistributions in binary form must reproduce the above copyright
26 ;; notice, this list of conditions and the following disclaimer in the
27 ;; documentation and/or other materials provided with the distribution.
29 ;; 3. Neither the name of the author nor the names of any contributors
30 ;; may be used to endorse or promote products derived from this
31 ;; software without specific prior written permission.
33 ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
34 ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
35 ;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
36 ;; DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
37 ;; FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
38 ;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
39 ;; SUBSTITUTE GOODS OR SERVICES# LOSS OF USE, DATA, OR PROFITS# OR
40 ;; BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
41 ;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
42 ;; OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
43 ;; IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
47 ;; You need MusicPD - Music Playing Daemon
48 ;; (http://www.musicpd.org/) to be set up and running.
50 ;; The xwem-mpd-dock from xwem-mpd.el was designed to sit in the xwem
51 ;; minibuffer (panel), and theoretically you would be able to have the
52 ;; mpd-dock-frame from here sit in a panel (KDE, GNOME, LXDE, etc)
53 ;; too. I say "theoretically" because I've never tried so I'm leaving
54 ;; it as an exercise for the reader. :-)
56 ;; No, instead of docking the mpd-dock-frame onto a panel I prefer to
57 ;; tweak its frame properties so it will be completely ignored by the
58 ;; WM. That gives me a frame that has no decorations, is visible on all
59 ;; workspaces, always "on top", and doesn't show up in "task switchers".
60 ;; To get tha magic you set the `override-redirect' property to `t', and
61 ;; for positioning set `left' and/or `top' properties.
63 ;;; Keybinding Suggestions:
65 ;; (global-set-key [XF86AudioPlay] #'mpd-playpause)
66 ;; (global-set-key [XF86AudioStop] #'mpd-stop)
67 ;; (global-set-key [XF86AudioNext] #'mpd-next-track)
68 ;; (global-set-key [XF86AudioPrev] #'mpd-previous-track)
69 ;; (global-set-key [XF86AudioRaiseVolume] #'mpd-volume-up)
70 ;; (global-set-key [XF86AudioLowerVolume] #'mpd-volume-down)
71 ;; (global-set-key [XF86AudioMute] #'mpd-volume-mute/unmute)
73 ;; Yeah, it helps if you have a keyboard with those fancy keys. :-)
77 ;; First up you will need a 48x48 `nocover.jpg' and put it in
78 ;; `mpd-directory' (see the images directory in this repo) for albums
79 ;; that you don't have any art for. Secondly, album art is taken from
80 ;; `cover.jpg' files in the same directory as the album's audio files.
81 ;; Yep, sort your music by album (at least).
83 ;; Embedded art is supported via FFmpeg. The embedded art is preferred.
85 ;;; Player control buttons:
87 ;; Put the XPM's in `mpd-directory'
92 "Group to customize mpd."
96 (defcustom mpd-update-rate 5
97 "MPD variables updating rate in seconds."
101 (defcustom mpd-directory
102 (file-name-as-directory
103 (expand-file-name ".mpd" (user-home-directory)))
104 "Base mpd directory."
108 (defcustom mpd-conf-file
110 (list (expand-file-name
112 (paths-construct-path (list (getenv "XDG_CONFIG_HOME") "mpd")))
113 (expand-file-name ".mpdconf" (user-home-directory))
116 (paths-construct-path (list (user-home-directory) ".mpd")))
117 (expand-file-name "mpd.conf" "/etc"))))
119 (mapcar #'(lambda (file)
120 (and (file-exists-p file)
123 "The mpd config file.
125 The default should be fine here because the file is searched for in
126 the same places and in the same order as mpd itself does."
127 :type '(file :must-match t)
130 (defcustom mpd-after-command-hook nil
131 "Hooks to run after MPD command is executed.
132 Executed command name stored in `mpd-this-command'."
136 (defcustom mpd-before-variables-update-hook nil
137 "Hooks to run before updating mpd variables."
141 (defcustom mpd-after-variables-update-hook nil
142 "Hooks to run after mpd variables has been updated."
147 (defvar mpd-process nil)
148 (defvar mpd-itimer nil)
149 (defvar mpd-dock-frame nil)
150 (defvar mpd-dock-buffer nil)
152 (defun mpd-start-connection ()
153 "Open connection to MusicPD daemon.
154 Set `mpd-process' by side effect."
155 (when (or (not mpd-process)
156 (not (eq mpd-process 'open)))
157 (setq mpd-process (open-network-stream "mpd" " *mpd connection*"
159 (when (fboundp 'set-process-coding-system)
160 (set-process-coding-system mpd-process 'utf-8 'utf-8))
161 (set-process-filter mpd-process 'mpd-process-filter)
162 (set-process-sentinel mpd-process 'mpd-process-sentinel)
163 (process-kill-without-query mpd-process)
164 (add-hook 'mpd-after-command-hook #'mpd-update-variables)
165 (add-hook 'mpd-before-variables-update-hook #'mpd-clear-variables)
167 (start-itimer "mpd-vars-update" #'mpd-update-variables
168 mpd-update-rate mpd-update-rate))))
170 (defun mpd-disconnect ()
171 "Disconnect from the mpd daemon.
173 Also removes the update hook, kills the itimer, and removes the dock
176 (let ((proc (get-process (process-name mpd-process)))
177 (timer (get-itimer (itimer-name mpd-itimer))))
178 (remove-hook 'mpd-after-command-hook #'mpd-update-variables)
179 (remove-hook 'mpd-before-variables-update-hook #'mpd-clear-variables)
180 (when (itimerp timer)
181 (delete-itimer timer))
182 (when (process-live-p proc)
183 (delete-process (get-process (process-name mpd-process))))
184 (when (frame-live-p mpd-dock-frame)
185 (delete-frame mpd-dock-frame))
186 (when (buffer-live-p mpd-dock-buffer)
187 (kill-buffer mpd-dock-buffer))))
189 (defun mpd-music-directory ()
190 "Returns the value of \"music_directory\" from mpd config."
192 (insert-file-contents-literally mpd-conf-file)
193 (re-search-forward "^music_directory[[:blank:]]+" nil t)
194 (buffer-substring (1+ (point)) (1- (point-at-eol)))))
196 (defvar mpd-music-directory (mpd-music-directory)
197 "The music directory.")
199 (defvar mpd-zero-vars-p t)
200 (defvar mpd-status-update-p nil)
202 (defvar mpd-pre-mute-volume nil
203 "Holds the value of `**mpd-var-volume* prior to muting.
204 The purpose of this is so that when you unmute, it goes back to the
205 volume you had it set to before you muted.")
207 (defvar mpd-this-command nil
208 "The mpd command currently executing.
209 Useful to use in `mpd-after-command-hook' hooks.")
211 (defmacro define-mpd-command (cmd args &rest body)
212 "Define new mpd command.
214 This will run the hooks in `mpd-after-command-hook' while
215 `mpd-this-command' is let-bound to the name of the command."
218 (let ((mpd-this-command ',cmd))
219 (run-hooks 'mpd-after-command-hook))))
221 (defun mpd-send (format &rest args)
222 "Send formated string into connection.
223 FORMAT and ARGS are passed directly to `format' as arguments."
224 (let ((string (concat (apply #'format format args) "\n")))
225 (if (eq (process-status mpd-process) 'open)
226 (process-send-string mpd-process string)
227 (mpd-start-connection)
228 (process-send-string mpd-process string))))
231 "Return the current state of mpd as a string.
233 Possible values are: \"play\", \"pause\", and \"stop\"."
234 (and-boundp '**mpd-var-state*
237 (defun mpd-stopped-p ()
238 "Return t if the music is stopped."
239 (string= (mpd-state) "stop"))
241 (defun mpd-paused-p ()
242 "Return t if the music is paused."
243 (string= (mpd-state) "pause"))
247 "Return the current mpd volume as an integer."
248 (or (and-boundp '**mpd-var-volume*
249 (string-to-int **mpd-var-volume*))
252 (defun mpd-muted-p ()
253 "Return t when the volume is muted."
254 (zerop (mpd-volume)))
256 (defun mpd-volume-up (step)
257 "Increase the volume by STEP increments.
258 STEP can be given via numeric prefix arg and defaults to 1 if omitted."
260 (let* ((oldvol (mpd-volume))
261 (newvol (+ oldvol step)))
262 (when (>= newvol 100)
264 (mpd-send "setvol %d" newvol)))
266 (defun mpd-volume-down (step)
267 "Decrease the volume by STEP increments.
268 STEP can be given via numeric prefix arg and defaults to 1 if omitted."
270 (let* ((oldvol (mpd-volume))
271 (newvol (- oldvol step)))
274 (mpd-send "setvol %d" newvol)))
276 (define-mpd-command mpd-volume-mute (&optional unmute)
278 With prefix arg, UNMUTE, let the tunes blast again."
281 (mpd-send "setvol %s" mpd-pre-mute-volume)
282 (setq mpd-pre-mute-volume (mpd-volume))
283 (mpd-send "setvol 0")))
285 (defun mpd-volume-mute/unmute ()
286 "Wrapper around #'mpd-volume-mute to mute and unmute."
289 (mpd-volume-mute 'unmute)
292 (define-mpd-command mpd-volume-max ()
293 "Set volume to maximum."
295 (mpd-send "setvol 100"))
297 (define-mpd-command mpd-volume-min ()
298 "Set volume to minimum.
299 Mutes the mpd audio by side effect."
301 (setq mpd-pre-mute-volume (mpd-volume))
302 (mpd-send "setvol 0"))
305 (defun mpd-songpos ()
306 "Return position in song as a cons of elapsed and total seconds."
307 (and-boundp '**mpd-var-time*
308 (destructuring-bind (a b)
309 (split-string **mpd-var-time* ":")
310 (cons (string-to-int a) (string-to-int b)))))
312 (define-mpd-command mpd-seek (time)
313 "Seek current track to TIME."
314 (and-boundp '**mpd-var-Id*
315 (mpd-send "seekid %s %d"
316 **mpd-var-Id* (+ (car (mpd-songpos)) time))))
318 (defun mpd-seek-forward ()
319 "Seek forward 10 seconds."
323 (defun mpd-seek-backward ()
324 "Seek backward 10 seconds."
328 ;; Playing operations
329 (define-mpd-command mpd-next-track ()
330 "Start playing next track."
334 (define-mpd-command mpd-previous-track ()
335 "Start playing previous track."
337 (mpd-send "previous"))
339 (define-mpd-command mpd-stop ()
344 (define-mpd-command mpd-play ()
349 (define-mpd-command mpd-pause ()
354 (define-mpd-command mpd-playpause ()
355 "Resume playing or pause."
361 (defun mpd-process-filter (process output)
362 "MPD proccess filter."
365 (goto-char (point-min))
367 (when (looking-at "\\(.*?\\): \\(.*\\)")
368 (set (intern (format "**mpd-var-%s*" (match-string 1)))
371 (when mpd-status-update-p
372 (setq mpd-status-update-p nil)
373 (setq mpd-zero-vars-p nil)
374 (run-hooks 'mpd-after-variables-update-hook)))
376 (defun mpd-process-sentinel (proc &optional evstr)
377 (let ((timer (get-itimer mpd-itimer)))
378 (message "[MPD]: %s" evstr)
379 (delete-process proc)
380 (when (itimerp timer)
381 (delete-itimer timer))
385 (defvar mpd-cover-glyph nil
386 "The extent holding the album cover art.")
388 (defvar mpd-current-filename nil
389 "Filename of the currently playing mpd track.
391 This differs from `**mpd-var-file*' in that it is only updated once
392 per track change instead of every time `mpd-itimer' fires.")
395 "Return the file name of current track from mpd.
397 Note that this is just what is reported by mpd. To perform operations
398 on the file on disc you will need to prepend `mpd-music-directory' to
400 (and-boundp '**mpd-var-file*
403 (defun mpd-cover-file ()
404 "Return a possible coverart filename.
406 The file may not exist on disc so call `file-exists-p' on it, or see
408 (let ((ifile (expand-file-name (mpd-file) mpd-music-directory))
409 (ofile (expand-file-name "cover.jpg" (temp-directory)))
410 (dir (paths-construct-path
411 (list mpd-music-directory (file-dirname (mpd-file)))))
412 (ffmpeg (executable-find "ffmpeg"))
414 ;; Try for embedded art first
416 (setq embedded (call-process "ffmpeg" nil nil nil
418 "-loglevel" "quiet" "-y"
422 (expand-file-name "cover.jpg" dir))))
424 (defun mpd-has-cover-p ()
425 "Return t when coverart exists for the current track."
426 (file-exists-p (mpd-cover-file)))
428 (defun mpd-scale-cover (cover height &optional width)
429 "Scale image, COVER to HEIGHT x WIDTH.
431 Argument COVER is the image filename. No checks are made on its
432 existence, or even if it is an image file \(only JPEG is supported
433 incidently\). So you should do some rudimentary checks before
434 calling this. The file on disc is left unchanged.
436 Argument HEIGHT is the height in pixels to scale the image to.
438 Optional argument WIDTH is the width in pixels to scale the image to.
439 If omitted it defaults to HEIGHT.
441 A string is returned that can be used in the :data key of `make-glyph'."
444 (format (concat "jpegtopnm " "'" cover "' 2>/dev/null"
445 "|pnmnorm 2>/dev/null"
446 "|pnmscale -height %d -width %d"
447 "|pnmtojpeg") height (or width height))
451 (defun mpd-update-cover ()
452 "Updates the cover art glyph."
453 (unless (equal mpd-current-filename (mpd-file))
454 (with-current-buffer mpd-dock-buffer
455 (let ((cover (mpd-cover-file))
456 (nocover (expand-file-name "nocover.jpg" mpd-directory)))
457 (if (mpd-has-cover-p)
459 (setq cover (mpd-scale-cover cover 48))
460 (set-extent-end-glyph
462 (make-glyph `([jpeg :data ,cover]))))
463 (set-extent-end-glyph
465 (make-glyph `([jpeg :file ,nocover]))))))
466 (setq mpd-current-filename (mpd-file))))
468 (defun mpd-update-variables ()
469 "Requests status information."
470 (run-hooks 'mpd-before-variables-update-hook)
471 (setq mpd-zero-vars-p t)
472 (mpd-send "currentsong")
473 (setq mpd-status-update-p t)
477 (defun mpd-clear-variables ()
478 "Clears the most relevant mpd variables."
479 (with-boundp '(**mpd-var-Title* **mpd-var-Artist* **mpd-var-Album*
480 **mpd-var-Genre* **mpd-var-Date*)
481 (setq **mpd-var-Title* nil
482 **mpd-var-Artist* nil
485 **mpd-var-Date* nil)))
489 (defvar mpd-dock-frame-plist
495 (menubar-visible-p . nil)
496 (has-modeline-p . nil)
497 (default-gutter-visible-p . nil)
498 (default-toolbar-visible-p . nil)
499 (scrollbar-height . 0)
500 (scrollbar-width . 0)
501 (text-cursor-visible-p . nil))
502 "Frame properties for mpd dock.")
504 (defun mpd-info (&rest args)
505 "Returns a string for use in the mpd balloon-help frame."
506 (with-boundp '(**mpd-var-Title* **mpd-var-Artist* **mpd-var-Album*
507 **mpd-var-Genre* **mpd-var-Date*)
508 (let ((title (or **mpd-var-Title* "Unknown"))
509 (artist (or **mpd-var-Artist* "Unknown"))
510 (album (or **mpd-var-Album* "Unknown"))
511 (genre (or **mpd-var-Genre* "Unknown"))
512 (year (or **mpd-var-Date* "Unknown"))
513 (file (file-name-nondirectory (mpd-file))))
515 (when (mpd-has-cover-p)
522 (when (mpd-has-cover-p)
524 title artist album year genre file))))
526 (defun mpd-balloon-cover ()
527 "Inserts coverart into mpd balloon."
528 (let ((cover (mpd-cover-file)))
529 (when (mpd-has-cover-p)
530 (setq cover (mpd-scale-cover cover 128))
531 (set-extent-begin-glyph
532 (make-extent (point-min) (point-min))
533 (make-glyph `([jpeg :data ,cover]))))))
535 (defadvice balloon-help-display-help (after mpd-balloon-cover (&rest args) activate)
536 "Display cover art image in the balloon."
537 (when (process-live-p (get-process "mpd"))
538 (set-buffer balloon-help-buffer)
539 (goto-char (point-max))
540 (and (re-search-backward (file-name-nondirectory (mpd-file)) nil t)
541 (mpd-balloon-cover))))
543 (defconst mpd-prev-map
544 (let* ((map (make-sparse-keymap 'mpd-prev-map)))
545 (define-key map [button1] 'mpd-previous-track)
547 "Keymap for \"Prev\" button.")
549 (defconst mpd-pause-map
550 (let* ((map (make-sparse-keymap 'mpd-pause-map)))
551 (define-key map [button1] 'mpd-pause)
553 "Keymap for \"Pause\" button.")
555 (defconst mpd-play-map
556 (let* ((map (make-sparse-keymap 'mpd-play-map)))
557 (define-key map [button1] 'mpd-play)
559 "Keymap for \"Play\" button.")
561 (defconst mpd-next-map
562 (let* ((map (make-sparse-keymap 'mpd-next-map)))
563 (define-key map [button1] 'mpd-next-track)
565 "Keymap for \"Next\" button.")
567 (defun mpd-new-frame ()
568 "Create new mpd frame."
569 (unless (frame-live-p mpd-dock-frame)
570 (setq mpd-dock-frame (new-frame mpd-dock-frame-plist))
571 (select-frame mpd-dock-frame)
572 (unless (buffer-live-p mpd-dock-buffer)
573 (setq mpd-dock-buffer (get-buffer-create "*MpdDock*"))
574 (set-buffer-dedicated-frame mpd-dock-buffer mpd-dock-frame)
576 (let (prev pause play next)
577 (set-buffer mpd-dock-buffer)
578 (set-extent-end-glyph
579 (setq prev (make-extent (point-max) (point-max)))
581 `([xpm :file ,(expand-file-name "Rewind.xpm" mpd-directory)])))
582 (set-extent-properties
584 `(keymap ,mpd-prev-map balloon-help "Previous Track"))
585 (set-extent-end-glyph
586 (setq pause (make-extent (point-max) (point-max)))
588 `([xpm :file ,(expand-file-name "Pause.xpm" mpd-directory)])))
589 (set-extent-properties
591 `(keymap ,mpd-pause-map balloon-help "Pause"))
592 (set-extent-end-glyph
593 (setq play (make-extent (point-max) (point-max)))
595 `([xpm :file ,(expand-file-name "Play.xpm" mpd-directory)])))
596 (set-extent-properties
598 `(keymap ,mpd-play-map balloon-help "Play"))
599 (set-extent-end-glyph
600 (setq next (make-extent (point-max) (point-max)))
602 `([xpm :file ,(expand-file-name "FFwd.xpm" mpd-directory)])))
603 (set-extent-properties
605 `(keymap ,mpd-next-map balloon-help "Next Track"))
607 (set-extent-end-glyph
608 (setq mpd-cover-glyph (make-extent (point-max) (point-max)))
610 `([jpeg :file ,(expand-file-name "nocover.jpg"
612 (set-extent-properties
614 `(keymap ,mpd-pause-map balloon-help ,#'mpd-info)))))
615 (set-specifier horizontal-scrollbar-visible-p nil
616 (cons mpd-dock-frame nil))
617 (set-specifier vertical-scrollbar-visible-p nil
618 (cons mpd-dock-frame nil))
619 (set-window-buffer nil mpd-dock-buffer)))
622 "Start mpd dockapp to interact with MusicPD."
624 (let ((cframe (selected-frame)))
625 ;; Start client connection
626 (mpd-start-connection)
629 (mpd-update-variables)))
631 (defun mpd-now-playing ()
632 "Return a formatted string of Title and Artist.
634 This is for use in things like Gnus for a X-Now-Playing header etc."
635 (with-fboundp 'taglib:show-tag
636 (let ((title (and mpd-current-filename
639 (mpd-file) mpd-music-directory)
641 (artist (and mpd-current-filename
644 (mpd-file) mpd-music-directory)
646 (format "%s --- [%s]"
647 (or title "The Sounds of Silence")
648 (or artist "Marcel Marceau")))))