1 ;;; mpd.el --- A complete ripoff of xwem-mpd.
3 ;; Copyright (C) 2008 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://musicpd.sourceforge.net/) to be set up and running.
53 (autoload 'google-query "google-query" nil t))
56 "Group to customize mpd."
60 (defcustom mpd-update-rate 5
61 "MPD variables updating rate in seconds."
65 (defcustom mpd-directory
66 (file-name-as-directory
67 (expand-file-name ".mpd" (user-home-directory)))
72 (defcustom mpd-lyrics-dir
73 (file-name-as-directory
74 (expand-file-name "lyrics" mpd-directory))
75 "Directory containing songs lyrics."
79 (defcustom mpd-after-command-hook nil
80 "Hooks to run after MPD command is executed.
81 Executed command name stored in `mpd-this-command'."
85 (defcustom mpd-before-variables-update-hook nil
86 "Hooks to run before updating mpd variables."
90 (defcustom mpd-after-variables-update-hook nil
91 "Hooks to run after mpd variables has been updated."
96 (defvar mpd-process nil)
97 (defvar mpd-itimer nil)
98 (defvar mpd-dock-frame nil)
99 (defvar mpd-dock-buffer nil)
101 (defun mpd-start-connection ()
102 "Open connection to MusicPD daemon.
103 Set `mpd-process' by side effect."
104 (when (or (not mpd-process)
105 (not (eq mpd-process 'open)))
106 (setq mpd-process (open-network-stream "mpd" " *mpd connection*"
108 (when (fboundp 'set-process-coding-system)
109 (set-process-coding-system mpd-process 'utf-8 'utf-8))
110 (set-process-filter mpd-process 'mpd-process-filter)
111 (set-process-sentinel mpd-process 'mpd-process-sentinel)
112 (process-kill-without-query mpd-process)
114 (add-hook 'mpd-after-command-hook #'mpd-update-variables)
116 (start-itimer "mpd-vars-update" #'mpd-update-variables
117 mpd-update-rate mpd-update-rate))))
119 (defun mpd-disconnect ()
120 "Disconnect from the mpd daemon.
122 Also removes the update hook, kills the itimer, and removes the dock
125 (let ((proc (get-process (process-name mpd-process)))
126 (timer (get-itimer (itimer-name mpd-itimer))))
127 (remove-hook 'mpd-after-command-hook #'mpd-update-variables)
128 (when (itimerp timer)
129 (delete-itimer timer))
130 (when (process-live-p proc)
131 (delete-process (get-process (process-name mpd-process))))
132 (when (frame-live-p mpd-dock-frame)
133 (delete-frame mpd-dock-frame))
134 (when (buffer-live-p mpd-dock-buffer)
135 (kill-buffer mpd-dock-buffer))))
138 (defvar mpd-zero-vars-p t)
139 (defvar mpd-status-update-p nil)
141 (defvar **mpd-var-Album* nil)
142 (defvar **mpd-var-Artist* nil)
143 (defvar **mpd-var-Date* nil)
144 (defvar **mpd-var-Genre* nil)
145 (defvar **mpd-var-Id* nil)
146 (defvar **mpd-var-Pos* nil)
147 (defvar **mpd-var-Time* nil)
148 (defvar **mpd-var-Title* nil)
149 (defvar **mpd-var-Track* nil)
150 (defvar **mpd-var-audio* nil)
151 (defvar **mpd-var-bitrate* nil)
152 (defvar **mpd-var-file* nil)
153 (defvar **mpd-var-length* nil)
154 (defvar **mpd-var-playlist* nil)
155 (defvar **mpd-var-playlistlength* nil)
156 (defvar **mpd-var-random* nil)
157 (defvar **mpd-var-repeat* nil)
158 (defvar **mpd-var-song* nil)
159 (defvar **mpd-var-songid* nil)
160 (defvar **mpd-var-state* nil)
161 (defvar **mpd-var-time* nil)
162 (defvar **mpd-var-volume* nil)
163 (defvar **mpd-var-xfade* nil)
165 (defvar mpd-pre-mute-volume nil
166 "Holds the value of `**mpd-var-volume* prior to muting.
167 The purpose of this is so that when you unmute, it goes back to the
168 volume you had it set to before you muted.")
170 (defvar mpd-this-command nil
171 "The mpd command currently executing.
172 Useful to use in `mpd-after-command-hook' hooks.")
174 (defmacro define-mpd-command (cmd args &rest body)
175 "Define new mpd command."
178 (let ((mpd-this-command ',cmd))
179 (run-hooks 'mpd-after-command-hook))))
181 (defun mpd-send (format &rest args)
182 "Send formated string into connection.
183 FORMAT and ARGS are passed directly to `format' as arguments."
184 (let ((string (concat (apply #'format format args) "\n")))
185 (if (eq (process-status mpd-process) 'open)
186 (process-send-string mpd-process string)
187 (mpd-start-connection)
188 (process-send-string mpd-process string))))
190 (defun mpd-stopped-p ()
191 (string= **mpd-var-state* "stop"))
192 (defun mpd-paused-p ()
193 (string= **mpd-var-state* "pause"))
194 (defun mpd-muted-p ()
195 (zerop (string-to-number **mpd-var-volume*)))
198 (defun mpd-songpos ()
200 (destructuring-bind (a b)
201 (split-string **mpd-var-time* ":")
202 (cons (string-to-int a) (string-to-int b)))
205 (defun mpd-volume-up (step)
206 "Increase the volume by STEP increments.
207 STEP can be given via numeric prefix arg and defaults to 1 if omitted."
209 (let* ((oldvol (string-to-number **mpd-var-volume*))
210 (newvol (+ oldvol step))
211 (mpd-this-command 'mpd-volume-down))
212 (when (>= newvol 100)
214 (mpd-send "setvol %d" newvol)
215 (run-hooks 'mpd-after-command-hook)))
217 (defun mpd-volume-down (step)
218 "Decrease the volume by STEP increments.
219 STEP can be given via numeric prefix arg and defaults to 1 if omitted."
221 (let* ((oldvol (string-to-number **mpd-var-volume*))
222 (newvol (- oldvol step))
223 (mpd-this-command 'mpd-volume-down))
226 (mpd-send "setvol %d" newvol)
227 (run-hooks 'mpd-after-command-hook)))
229 (defun mpd-volume-mute (&optional unmute)
231 With prefix arg, UNMUTE, let the tunes blast again."
234 (mpd-send "setvol %s" mpd-pre-mute-volume)
235 (setq mpd-pre-mute-volume **mpd-var-volume*)
236 (mpd-send "setvol 0"))
237 (let ((mpd-this-command 'mpd-volume-mute))
238 (run-hooks 'mpd-after-command-hook)))
240 (defun mpd-volume-mute/unmute ()
241 "Wrapper around #'mpd-volume-mute to mute and unmute."
244 (mpd-volume-mute 'unmute)
247 (define-mpd-command mpd-volume-max ()
248 "Set volume to maximum."
250 (mpd-send "setvol 100"))
252 (define-mpd-command mpd-volume-min ()
253 "Set volume to minimum.
254 Sets state to \"muted\" by side effect."
256 (setq mpd-pre-mute-volume **mpd-var-volume*)
257 (mpd-send "setvol 0"))
259 (define-mpd-command mpd-seek (time)
260 "Seek current track to TIME."
261 (mpd-send "seekid %s %d" **mpd-var-Id* (+ (car (mpd-songpos)) time)))
263 (defun mpd-seek-forward ()
267 (defun mpd-seek-backward ()
272 (define-mpd-command mpd-next-track ()
273 "Start playing next track."
277 (define-mpd-command mpd-previous-track ()
278 "Start playing previous track."
280 (mpd-send "previous"))
282 (define-mpd-command mpd-stop ()
287 (define-mpd-command mpd-play ()
292 (define-mpd-command mpd-pause ()
297 (define-mpd-command mpd-playpause ()
298 "Resume playing or pause."
304 (defun mpd-process-filter (process output)
305 "MPD proccess filter."
308 (goto-char (point-min))
310 (when (looking-at "\\(.*?\\): \\(.*\\)")
311 (set (intern (format "**mpd-var-%s*" (match-string 1)))
314 (when mpd-status-update-p
315 (setq mpd-status-update-p nil)
316 (setq mpd-zero-vars-p nil)
317 (run-hooks 'mpd-after-variables-update-hook)))
319 (defun mpd-process-sentinel (proc &optional evstr)
320 (let ((timer (get-itimer mpd-itimer)))
321 (message "[MPD]: %s" evstr)
322 (delete-process proc)
323 (when (itimerp timer)
324 (delete-itimer timer))
328 (defun mpd-update-variables ()
329 "Requests status information."
330 (run-hooks 'mpd-before-variables-update-hook)
331 (setq mpd-zero-vars-p t)
332 (mpd-send "currentsong")
333 (setq mpd-status-update-p t)
337 (defun mpd-lyric-filename ()
338 "Return lyric filename for now playing song."
339 (when **mpd-var-file*
341 (concat (replace-in-string **mpd-var-file* "\/" "--") ".txt")
344 (defun mpd-lyric-check ()
345 "Return non-nil if current track has local lyrics."
346 (let ((fn (mpd-lyric-filename)))
347 (and fn (file-exists-p fn))))
349 (defun mpd-lyric-save ()
350 "Save selected lyric to lyric file."
352 (if (mpd-lyric-check)
353 (message "There is already a lyric for this song")
354 (let ((text (get-selection-no-error)))
356 (message "You should have selected the lyric first!")
358 (with-current-buffer (find-file-noselect (mpd-lyric-filename))
362 ;; Haven't decided what to do with this one yet. --SY.
363 ;;(define-sawfish-command mpd-lyric-show ()
364 ;; "Show lyrics for now playing song."
365 ;; (sawfish-interactive)
366 ;; (if (mpd-lyric-check)
367 ;; (let ((temp-buffer-show-function 'sawfish-special-popup-frame)
368 ;; (header (format "\"%s\" (by: %s)"
370 ;; **mpd-var-Artist*))
371 ;; (title (format "Lyrics: %s" **mpd-var-Title*)))
372 ;; (with-output-to-temp-buffer title
373 ;; (set-buffer standard-output)
374 ;; (insert header "\n"
375 ;; (make-string (length header) ?=)
377 ;; (insert-file-contents (mpd-lyric-filename))
378 ;; (toggle-read-only 1)
379 ;; (view-mode nil #'(lambda (&rest not-used-buffer)
380 ;; (delete-frame (selected-frame))))))
381 ;; (when (and **mpd-var-Artist* **mpd-var-Title*)
382 ;; (let ((lyric-frame (new-frame)))
383 ;; (select-frame lyric-frame)
384 ;; (google-query (format "\"%s\" \"%s\" lyrics"
385 ;; **mpd-var-Artist* **mpd-var-Title*))
386 ;; (focus-frame lyric-frame)))))
390 (defvar mpd-dock-frame-plist
396 (menubar-visible-p . nil)
397 (has-modeline-p . nil)
398 (default-gutter-visible-p . nil)
399 (default-toolbar-visible-p . nil)
400 (scrollbar-height . 0)
401 (scrollbar-width . 0)
402 (text-cursor-visible-p . nil))
403 "Frame properties for mpd dock.")
405 (defun mpd-info (&rest args)
406 (let ((title (or **mpd-var-Title* "Unknown"))
407 (artist (or **mpd-var-Artist* "Unknown"))
408 (album (or **mpd-var-Album* "Unknown"))
409 (genre (or **mpd-var-Genre* "Unknown"))
410 (year (or **mpd-var-Date* "Unknown"))
411 (file (file-name-nondirectory **mpd-var-file*)))
417 title artist album year genre file)))
419 (defconst mpd-prev-map
420 (let* ((map (make-sparse-keymap 'mpd-prev-map)))
421 (define-key map [button1] 'mpd-previous-track)
423 "Keymap for \"Prev\" button.")
425 (defconst mpd-pause-map
426 (let* ((map (make-sparse-keymap 'mpd-pause-map)))
427 (define-key map [button1] 'mpd-pause)
429 "Keymap for \"Pause\" button.")
431 (defconst mpd-play-map
432 (let* ((map (make-sparse-keymap 'mpd-play-map)))
433 (define-key map [button1] 'mpd-play)
435 "Keymap for \"Play\" button.")
437 (defconst mpd-next-map
438 (let* ((map (make-sparse-keymap 'mpd-next-map)))
439 (define-key map [button1] 'mpd-next-track)
441 "Keymap for \"Next\" button.")
443 (make-face 'mpd-dock-face
444 "Face used in the mpd dock buffer.")
446 (defun mpd-new-frame ()
447 "Create new mpd frame."
448 (unless (frame-live-p mpd-dock-frame)
449 (setq mpd-dock-frame (new-frame mpd-dock-frame-plist))
450 (select-frame mpd-dock-frame)
451 (unless (buffer-live-p mpd-dock-buffer)
452 (setq mpd-dock-buffer (get-buffer-create "*MpdDock*"))
453 (set-buffer-dedicated-frame mpd-dock-buffer mpd-dock-frame)
455 (let (prev pause play next)
456 (set-buffer mpd-dock-buffer)
457 (set-extent-properties
458 (insert-face "[Song Info]" 'mpd-dock-face)
459 `(mouse-face highlight read-only t
460 balloon-help ,#'mpd-info))
462 (set-extent-end-glyph
463 (setq prev (make-extent (point-max) (point-max)))
465 (list (vector 'xpm :file (expand-file-name "Rewind.xpm"
467 (set-extent-properties
469 `(keymap ,mpd-prev-map balloon-help "Previous Track"))
470 (set-extent-end-glyph
471 (setq pause (make-extent (point-max) (point-max)))
473 (list (vector 'xpm :file (expand-file-name "Pause.xpm"
475 (set-extent-properties
477 `(keymap ,mpd-pause-map balloon-help "Pause"))
478 (set-extent-end-glyph
479 (setq play (make-extent (point-max) (point-max)))
481 (list (vector 'xpm :file (expand-file-name "Play.xpm"
483 (set-extent-properties
485 `(keymap ,mpd-play-map balloon-help "Play"))
486 (set-extent-end-glyph
487 (setq next (make-extent (point-max) (point-max)))
489 (list (vector 'xpm :file (expand-file-name "FFwd.xpm"
491 (set-extent-properties
493 `(keymap ,mpd-next-map balloon-help "Next Track")))))
494 (set-specifier horizontal-scrollbar-visible-p nil
495 (cons mpd-dock-frame nil))
496 (set-specifier vertical-scrollbar-visible-p nil
497 (cons mpd-dock-frame nil))
498 (set-window-buffer nil mpd-dock-buffer)))
501 "Start mpd dockapp to interact with MusicPD."
503 (let ((cframe (selected-frame)))
504 ;; Start client connection
505 (mpd-start-connection)
508 (mpd-update-variables)))