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 not supported at this time, but hopefully one day.
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)
165 (add-hook 'mpd-after-command-hook #'mpd-update-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 (when (itimerp timer)
180 (delete-itimer timer))
181 (when (process-live-p proc)
182 (delete-process (get-process (process-name mpd-process))))
183 (when (frame-live-p mpd-dock-frame)
184 (delete-frame mpd-dock-frame))
185 (when (buffer-live-p mpd-dock-buffer)
186 (kill-buffer mpd-dock-buffer))))
188 (defun mpd-music-directory ()
189 "Returns the value of \"music_directory\" from mpd config."
191 (insert-file-contents-literally mpd-conf-file)
192 (re-search-forward "^music_directory[[:blank:]]+" nil t)
193 (buffer-substring (1+ (point)) (1- (point-at-eol)))))
195 (defvar mpd-music-directory (mpd-music-directory)
196 "The music directory.")
199 (defvar mpd-zero-vars-p t)
200 (defvar mpd-status-update-p nil)
202 (defvar **mpd-var-Album* nil)
203 (defvar **mpd-var-Artist* nil)
204 (defvar **mpd-var-Date* nil)
205 (defvar **mpd-var-Genre* nil)
206 (defvar **mpd-var-Id* nil)
207 (defvar **mpd-var-Pos* nil)
208 (defvar **mpd-var-Time* nil)
209 (defvar **mpd-var-Title* nil)
210 (defvar **mpd-var-Track* nil)
211 (defvar **mpd-var-audio* nil)
212 (defvar **mpd-var-bitrate* nil)
213 (defvar **mpd-var-file* nil)
214 (defvar **mpd-var-length* nil)
215 (defvar **mpd-var-playlist* nil)
216 (defvar **mpd-var-playlistlength* nil)
217 (defvar **mpd-var-random* nil)
218 (defvar **mpd-var-repeat* nil)
219 (defvar **mpd-var-song* nil)
220 (defvar **mpd-var-songid* nil)
221 (defvar **mpd-var-state* nil)
222 (defvar **mpd-var-time* nil)
223 (defvar **mpd-var-volume* nil)
224 (defvar **mpd-var-xfade* nil)
226 (defvar mpd-pre-mute-volume nil
227 "Holds the value of `**mpd-var-volume* prior to muting.
228 The purpose of this is so that when you unmute, it goes back to the
229 volume you had it set to before you muted.")
231 (defvar mpd-this-command nil
232 "The mpd command currently executing.
233 Useful to use in `mpd-after-command-hook' hooks.")
235 (defmacro define-mpd-command (cmd args &rest body)
236 "Define new mpd command."
239 (let ((mpd-this-command ',cmd))
240 (run-hooks 'mpd-after-command-hook))))
242 (defun mpd-send (format &rest args)
243 "Send formated string into connection.
244 FORMAT and ARGS are passed directly to `format' as arguments."
245 (let ((string (concat (apply #'format format args) "\n")))
246 (if (eq (process-status mpd-process) 'open)
247 (process-send-string mpd-process string)
248 (mpd-start-connection)
249 (process-send-string mpd-process string))))
251 (defun mpd-stopped-p ()
252 (string= **mpd-var-state* "stop"))
253 (defun mpd-paused-p ()
254 (string= **mpd-var-state* "pause"))
255 (defun mpd-muted-p ()
256 (zerop (string-to-number **mpd-var-volume*)))
259 (defun mpd-songpos ()
261 (destructuring-bind (a b)
262 (split-string **mpd-var-time* ":")
263 (cons (string-to-int a) (string-to-int b)))
266 (defun mpd-volume-up (step)
267 "Increase the volume by STEP increments.
268 STEP can be given via numeric prefix arg and defaults to 1 if omitted."
270 (let* ((oldvol (string-to-number **mpd-var-volume*))
271 (newvol (+ oldvol step))
272 (mpd-this-command 'mpd-volume-down))
273 (when (>= newvol 100)
275 (mpd-send "setvol %d" newvol)
276 (run-hooks 'mpd-after-command-hook)))
278 (defun mpd-volume-down (step)
279 "Decrease the volume by STEP increments.
280 STEP can be given via numeric prefix arg and defaults to 1 if omitted."
282 (let* ((oldvol (string-to-number **mpd-var-volume*))
283 (newvol (- oldvol step))
284 (mpd-this-command 'mpd-volume-down))
287 (mpd-send "setvol %d" newvol)
288 (run-hooks 'mpd-after-command-hook)))
290 (defun mpd-volume-mute (&optional unmute)
292 With prefix arg, UNMUTE, let the tunes blast again."
295 (mpd-send "setvol %s" mpd-pre-mute-volume)
296 (setq mpd-pre-mute-volume **mpd-var-volume*)
297 (mpd-send "setvol 0"))
298 (let ((mpd-this-command 'mpd-volume-mute))
299 (run-hooks 'mpd-after-command-hook)))
301 (defun mpd-volume-mute/unmute ()
302 "Wrapper around #'mpd-volume-mute to mute and unmute."
305 (mpd-volume-mute 'unmute)
308 (define-mpd-command mpd-volume-max ()
309 "Set volume to maximum."
311 (mpd-send "setvol 100"))
313 (define-mpd-command mpd-volume-min ()
314 "Set volume to minimum.
315 Sets state to \"muted\" by side effect."
317 (setq mpd-pre-mute-volume **mpd-var-volume*)
318 (mpd-send "setvol 0"))
320 (define-mpd-command mpd-seek (time)
321 "Seek current track to TIME."
322 (mpd-send "seekid %s %d" **mpd-var-Id* (+ (car (mpd-songpos)) time)))
324 (defun mpd-seek-forward ()
328 (defun mpd-seek-backward ()
333 (define-mpd-command mpd-next-track ()
334 "Start playing next track."
338 (define-mpd-command mpd-previous-track ()
339 "Start playing previous track."
341 (mpd-send "previous"))
343 (define-mpd-command mpd-stop ()
348 (define-mpd-command mpd-play ()
353 (define-mpd-command mpd-pause ()
358 (define-mpd-command mpd-playpause ()
359 "Resume playing or pause."
365 (defun mpd-process-filter (process output)
366 "MPD proccess filter."
369 (goto-char (point-min))
371 (when (looking-at "\\(.*?\\): \\(.*\\)")
372 (set (intern (format "**mpd-var-%s*" (match-string 1)))
375 (when mpd-status-update-p
376 (setq mpd-status-update-p nil)
377 (setq mpd-zero-vars-p nil)
378 (run-hooks 'mpd-after-variables-update-hook)))
380 (defun mpd-process-sentinel (proc &optional evstr)
381 (let ((timer (get-itimer mpd-itimer)))
382 (message "[MPD]: %s" evstr)
383 (delete-process proc)
384 (when (itimerp timer)
385 (delete-itimer timer))
389 (defvar mpd-cover-glyph nil
390 "The extent holding the album cover art.")
392 (defvar mpd-current-filename nil
393 "Filename of the currently playing mpd track.
395 This differs from `**mpd-var-file*' in that it is only updated once
396 per track change instead of every time `mpd-itimer' fires.")
398 (defun mpd-update-cover ()
399 "Updates the cover art glyph."
400 (unless (equal mpd-current-filename **mpd-var-file*)
401 (with-current-buffer mpd-dock-buffer
402 (let* ((songdir (file-dirname **mpd-var-file*))
403 (cover (expand-file-name
405 (paths-construct-path
406 (list mpd-music-directory songdir))))
407 (nocover (expand-file-name "nocover.jpg" mpd-directory))
409 (if (file-exists-p cover)
412 (shell-command (concat "jpegtopnm " "'" cover "' 2>/dev/null"
413 "|pnmnorm 2>/dev/null"
414 "|pnmscale -height 48 -width 48"
417 (setq scaled (buffer-string)))
418 (set-extent-end-glyph
420 (make-glyph (list (vector 'jpeg :data scaled)))))
421 (set-extent-end-glyph
423 (make-glyph (list (vector 'jpeg :file nocover)))))))
424 (setq mpd-current-filename **mpd-var-file*)))
426 (defun mpd-update-variables ()
427 "Requests status information."
428 (run-hooks 'mpd-before-variables-update-hook)
429 (setq mpd-zero-vars-p t)
430 (mpd-send "currentsong")
431 (setq mpd-status-update-p t)
437 (defvar mpd-dock-frame-plist
443 (menubar-visible-p . nil)
444 (has-modeline-p . nil)
445 (default-gutter-visible-p . nil)
446 (default-toolbar-visible-p . nil)
447 (scrollbar-height . 0)
448 (scrollbar-width . 0)
449 (text-cursor-visible-p . nil))
450 "Frame properties for mpd dock.")
452 (defun mpd-info (&rest args)
453 (let ((title (or **mpd-var-Title* "Unknown"))
454 (artist (or **mpd-var-Artist* "Unknown"))
455 (album (or **mpd-var-Album* "Unknown"))
456 (genre (or **mpd-var-Genre* "Unknown"))
457 (year (or **mpd-var-Date* "Unknown"))
458 (file (file-name-nondirectory **mpd-var-file*)))
464 title artist album year genre file)))
466 (defconst mpd-prev-map
467 (let* ((map (make-sparse-keymap 'mpd-prev-map)))
468 (define-key map [button1] 'mpd-previous-track)
470 "Keymap for \"Prev\" button.")
472 (defconst mpd-pause-map
473 (let* ((map (make-sparse-keymap 'mpd-pause-map)))
474 (define-key map [button1] 'mpd-pause)
476 "Keymap for \"Pause\" button.")
478 (defconst mpd-play-map
479 (let* ((map (make-sparse-keymap 'mpd-play-map)))
480 (define-key map [button1] 'mpd-play)
482 "Keymap for \"Play\" button.")
484 (defconst mpd-next-map
485 (let* ((map (make-sparse-keymap 'mpd-next-map)))
486 (define-key map [button1] 'mpd-next-track)
488 "Keymap for \"Next\" button.")
490 (defun mpd-new-frame ()
491 "Create new mpd frame."
492 (unless (frame-live-p mpd-dock-frame)
493 (setq mpd-dock-frame (new-frame mpd-dock-frame-plist))
494 (select-frame mpd-dock-frame)
495 (unless (buffer-live-p mpd-dock-buffer)
496 (setq mpd-dock-buffer (get-buffer-create "*MpdDock*"))
497 (set-buffer-dedicated-frame mpd-dock-buffer mpd-dock-frame)
499 (let (prev pause play next)
500 (set-buffer mpd-dock-buffer)
501 (set-extent-end-glyph
502 (setq prev (make-extent (point-max) (point-max)))
504 (list (vector 'xpm :file (expand-file-name "Rewind.xpm"
506 (set-extent-properties
508 `(keymap ,mpd-prev-map balloon-help "Previous Track"))
509 (set-extent-end-glyph
510 (setq pause (make-extent (point-max) (point-max)))
512 (list (vector 'xpm :file (expand-file-name "Pause.xpm"
514 (set-extent-properties
516 `(keymap ,mpd-pause-map balloon-help "Pause"))
517 (set-extent-end-glyph
518 (setq play (make-extent (point-max) (point-max)))
520 (list (vector 'xpm :file (expand-file-name "Play.xpm"
522 (set-extent-properties
524 `(keymap ,mpd-play-map balloon-help "Play"))
525 (set-extent-end-glyph
526 (setq next (make-extent (point-max) (point-max)))
528 (list (vector 'xpm :file (expand-file-name "FFwd.xpm"
530 (set-extent-properties
532 `(keymap ,mpd-next-map balloon-help "Next Track"))
534 (set-extent-end-glyph
535 (setq mpd-cover-glyph (make-extent (point-max) (point-max)))
537 (list (vector 'jpeg :file (expand-file-name "nocover.jpg"
539 (set-extent-properties
541 `(keymap ,mpd-pause-map balloon-help ,#'mpd-info)))))
542 (set-specifier horizontal-scrollbar-visible-p nil
543 (cons mpd-dock-frame nil))
544 (set-specifier vertical-scrollbar-visible-p nil
545 (cons mpd-dock-frame nil))
546 (set-window-buffer nil mpd-dock-buffer)))
549 "Start mpd dockapp to interact with MusicPD."
551 (let ((cframe (selected-frame)))
552 ;; Start client connection
553 (mpd-start-connection)
556 (mpd-update-variables)))