Why do you only discover bugs _after_ you've committed?
[slh] / mpd.el
diff --git a/mpd.el b/mpd.el
index f56820e..c71cda6 100644 (file)
--- a/mpd.el
+++ b/mpd.el
@@ -80,7 +80,7 @@
 ;; `cover.jpg' files in the same directory as the album's audio files.
 ;; Yep, sort your music by album (at least).
 ;;
-;; Embedded art is not supported at this time, but hopefully one day.
+;; Embedded art is supported via FFmpeg.  The embedded art is preferred.
 
 ;;; Player control buttons:
 ;;
@@ -161,8 +161,8 @@ Set `mpd-process' by side effect."
     (set-process-filter mpd-process 'mpd-process-filter)
     (set-process-sentinel mpd-process 'mpd-process-sentinel)
     (process-kill-without-query mpd-process)
-
     (add-hook 'mpd-after-command-hook #'mpd-update-variables)
+    (add-hook 'mpd-before-variables-update-hook #'mpd-clear-variables)
     (setq mpd-itimer
           (start-itimer "mpd-vars-update" #'mpd-update-variables
                         mpd-update-rate mpd-update-rate))))
@@ -176,6 +176,7 @@ frame."
   (let ((proc (get-process (process-name mpd-process)))
        (timer (get-itimer (itimer-name mpd-itimer))))
     (remove-hook 'mpd-after-command-hook #'mpd-update-variables)
+    (remove-hook 'mpd-before-variables-update-hook #'mpd-clear-variables)
     (when (itimerp timer)
       (delete-itimer timer))
     (when (process-live-p proc)
@@ -187,41 +188,17 @@ frame."
 
 (defun mpd-music-directory ()
   "Returns the value of \"music_directory\" from mpd config."
-  (with-current-buffer (find-file-noselect mpd-conf-file)
+  (with-temp-buffer
+    (insert-file-contents-literally mpd-conf-file)
     (re-search-forward "^music_directory[[:blank:]]+" nil t)
     (buffer-substring (1+ (point)) (1- (point-at-eol)))))
 
 (defvar mpd-music-directory (mpd-music-directory)
   "The music directory.")
 
-;; mpd variables
 (defvar mpd-zero-vars-p t)
 (defvar mpd-status-update-p nil)
 
-(defvar **mpd-var-Album* nil)
-(defvar **mpd-var-Artist* nil)
-(defvar **mpd-var-Date* nil)
-(defvar **mpd-var-Genre* nil)
-(defvar **mpd-var-Id* nil)
-(defvar **mpd-var-Pos* nil)
-(defvar **mpd-var-Time* nil)
-(defvar **mpd-var-Title* nil)
-(defvar **mpd-var-Track* nil)
-(defvar **mpd-var-audio* nil)
-(defvar **mpd-var-bitrate* nil)
-(defvar **mpd-var-file* nil)
-(defvar **mpd-var-length* nil)
-(defvar **mpd-var-playlist* nil)
-(defvar **mpd-var-playlistlength* nil)
-(defvar **mpd-var-random* nil)
-(defvar **mpd-var-repeat* nil)
-(defvar **mpd-var-song* nil)
-(defvar **mpd-var-songid* nil)
-(defvar **mpd-var-state* nil)
-(defvar **mpd-var-time* nil)
-(defvar **mpd-var-volume* nil)
-(defvar **mpd-var-xfade* nil)
-
 (defvar mpd-pre-mute-volume nil
   "Holds the value of `**mpd-var-volume* prior to muting.
 The purpose of this is so that when you unmute, it goes back to the
@@ -232,7 +209,10 @@ volume you had it set to before you muted.")
 Useful to use in `mpd-after-command-hook' hooks.")
 
 (defmacro define-mpd-command (cmd args &rest body)
-  "Define new mpd command."
+  "Define new mpd command.
+
+This will run the hooks in `mpd-after-command-hook' while
+`mpd-this-command' is let-bound to the name of the command."
   `(defun ,cmd ,args
      ,@body
      (let ((mpd-this-command ',cmd))
@@ -247,55 +227,60 @@ FORMAT and ARGS are passed directly to `format' as arguments."
       (mpd-start-connection)
       (process-send-string mpd-process string))))
 
+(defun mpd-state ()
+  "Return the current state of mpd as a string.
+
+Possible values are: \"play\", \"pause\", and \"stop\"."
+  (and-boundp '**mpd-var-state*
+    **mpd-var-state*))
+
 (defun mpd-stopped-p ()
-  (string= **mpd-var-state* "stop"))
+  "Return t if the music is stopped."
+    (string= (mpd-state) "stop"))
+
 (defun mpd-paused-p ()
-  (string= **mpd-var-state* "pause"))
-(defun mpd-muted-p ()
-  (zerop (string-to-number **mpd-var-volume*)))
+  "Return t if the music is paused."
+    (string= (mpd-state) "pause"))
 
-;; (mpd-songpos)
-(defun mpd-songpos ()
-  (if **mpd-var-time*
-      (destructuring-bind (a b)
-          (split-string **mpd-var-time* ":")
-        (cons (string-to-int a) (string-to-int b)))
-    (cons 0 1)))                        ; todo?
+;; Volume
+(defun mpd-volume ()
+  "Return the current mpd volume as an integer."
+  (or (and-boundp '**mpd-var-volume*
+       (string-to-int **mpd-var-volume*))
+      0))
+
+(defun mpd-muted-p ()
+  "Return t when the volume is muted."
+  (zerop (mpd-volume)))
 
 (defun mpd-volume-up (step)
   "Increase the volume by STEP increments.
 STEP can be given via numeric prefix arg and defaults to 1 if omitted."
   (interactive "p")
-  (let* ((oldvol (string-to-number **mpd-var-volume*))
-        (newvol (+ oldvol step))
-        (mpd-this-command 'mpd-volume-down))
+  (let* ((oldvol (mpd-volume))
+        (newvol (+ oldvol step)))
     (when (>= newvol 100)
       (setq newvol 100))
-    (mpd-send "setvol %d" newvol)
-    (run-hooks 'mpd-after-command-hook)))
+    (mpd-send "setvol %d" newvol)))
 
 (defun mpd-volume-down (step)
   "Decrease the volume by STEP increments.
 STEP can be given via numeric prefix arg and defaults to 1 if omitted."
   (interactive "p")
-  (let* ((oldvol (string-to-number **mpd-var-volume*))
-        (newvol (- oldvol step))
-        (mpd-this-command 'mpd-volume-down))
+  (let* ((oldvol (mpd-volume))
+        (newvol (- oldvol step)))
     (when (<= newvol 0)
       (setq newvol 0))
-    (mpd-send "setvol %d" newvol)
-    (run-hooks 'mpd-after-command-hook)))
+    (mpd-send "setvol %d" newvol)))
 
-(defun mpd-volume-mute (&optional unmute)
+(define-mpd-command mpd-volume-mute (&optional unmute)
   "Mute the volume.
 With prefix arg, UNMUTE, let the tunes blast again."
   (interactive "P")
   (if unmute
       (mpd-send "setvol %s" mpd-pre-mute-volume)
-    (setq mpd-pre-mute-volume **mpd-var-volume*)
-    (mpd-send "setvol 0"))
-  (let ((mpd-this-command 'mpd-volume-mute))
-    (run-hooks 'mpd-after-command-hook)))
+    (setq mpd-pre-mute-volume (mpd-volume))
+    (mpd-send "setvol 0")))
 
 (defun mpd-volume-mute/unmute ()
   "Wrapper around #'mpd-volume-mute to mute and unmute."
@@ -311,24 +296,36 @@ With prefix arg, UNMUTE, let the tunes blast again."
 
 (define-mpd-command mpd-volume-min ()
   "Set volume to minimum.
-Sets state to \"muted\" by side effect."
+Mutes the mpd audio by side effect."
   (interactive)
-  (setq mpd-pre-mute-volume **mpd-var-volume*)
+  (setq mpd-pre-mute-volume (mpd-volume))
   (mpd-send "setvol 0"))
 
+;; Seek
+(defun mpd-songpos ()
+  "Return position in song as a cons of elapsed and total seconds."
+  (and-boundp '**mpd-var-time*
+    (destructuring-bind (a b)
+       (split-string **mpd-var-time* ":")
+      (cons (string-to-int a) (string-to-int b)))))
+
 (define-mpd-command mpd-seek (time)
   "Seek current track to TIME."
-  (mpd-send "seekid %s %d" **mpd-var-Id* (+ (car (mpd-songpos)) time)))
+  (and-boundp '**mpd-var-Id*
+    (mpd-send "seekid %s %d"
+             **mpd-var-Id* (+ (car (mpd-songpos)) time))))
 
 (defun mpd-seek-forward ()
+  "Seek forward 10 seconds."
   (interactive)
   (mpd-seek 10))
 
 (defun mpd-seek-backward ()
+  "Seek backward 10 seconds."
   (interactive)
   (mpd-seek -10))
 
-;; Plaing operations
+;; Playing operations
 (define-mpd-command mpd-next-track ()
   "Start playing next track."
   (interactive)
@@ -388,31 +385,85 @@ Sets state to \"muted\" by side effect."
 (defvar mpd-cover-glyph nil
   "The extent holding the album cover art.")
 
+(defvar mpd-current-filename nil
+  "Filename of the currently playing mpd track.
+
+This differs from `**mpd-var-file*' in that it is only updated once
+per track change instead of every time `mpd-itimer' fires.")
+
+(defun mpd-file ()
+  "Return the file name of current track from mpd.
+
+Note that this is just what is reported by mpd.  To perform operations
+on the file on disc you will need to prepend `mpd-music-directory' to
+it first."
+  (and-boundp '**mpd-var-file*
+    **mpd-var-file*))
+
+(defun mpd-cover-file ()
+  "Return a possible coverart filename.
+
+The file may not exist on disc so call `file-exists-p' on it, or see
+`mpd-has-cover-p'."
+  (let ((ifile (expand-file-name (mpd-file) mpd-music-directory))
+       (ofile (expand-file-name "cover.jpg" (temp-directory)))
+       (dir (paths-construct-path
+             (list mpd-music-directory (file-dirname (mpd-file)))))
+       (ffmpeg (executable-find "ffmpeg"))
+       (embedded 1))
+    ;; Try for embedded art first
+    (when ffmpeg
+      (setq embedded (call-process "ffmpeg" nil nil nil
+                                  "-hide_banner"
+                                  "-loglevel" "quiet" "-y"
+                                  "-i" ifile ofile)))
+    (if (eq embedded 0)
+       ofile
+      (expand-file-name "cover.jpg" dir))))
+
+(defun mpd-has-cover-p ()
+  "Return t when coverart exists for the current track."
+  (file-exists-p (mpd-cover-file)))
+
+(defun mpd-scale-cover (cover height &optional width)
+  "Scale image, COVER to HEIGHT x WIDTH.
+
+Argument COVER is the image filename.  No checks are made on its
+existence, or even if it is an image file \(only JPEG is supported
+incidently\).  So you should do some rudimentary checks before
+calling this.  The file on disc is left unchanged.
+
+Argument HEIGHT is the height in pixels to scale the image to.
+
+Optional argument WIDTH is the width in pixels to scale the image to.
+If omitted it defaults to HEIGHT.
+
+A string is returned that can be used in the :data key of `make-glyph'."
+  (with-temp-buffer
+    (shell-command
+     (format (concat "jpegtopnm " "'" cover "' 2>/dev/null"
+                    "|pnmnorm 2>/dev/null"
+                    "|pnmscale -height %d -width %d"
+                    "|pnmtojpeg") height (or width height))
+                  'insert)
+    (buffer-string)))
+
 (defun mpd-update-cover ()
   "Updates the cover art glyph."
-  (with-current-buffer mpd-dock-buffer
-    (let* ((songdir (file-dirname **mpd-var-file*))
-          (cover (expand-file-name
-                  "cover.jpg"
-                  (paths-construct-path
-                   (list mpd-music-directory songdir))))
-          (nocover (expand-file-name "nocover.jpg" mpd-directory))
-          (scaled))
-      (if (file-exists-p cover)
-         (progn
-           (with-temp-buffer
-             (shell-command (concat "jpegtopnm " "'" cover "' 2>/dev/null"
-                                    "|pnmnorm 2>/dev/null"
-                                    "|pnmscale -height 48 -width 48"
-                                    "|pnmtojpeg")
-                            'insert)
-             (setq scaled (buffer-string)))
-           (set-extent-end-glyph
-            mpd-cover-glyph
-            (make-glyph (list (vector 'jpeg :data scaled)))))
-       (set-extent-end-glyph
-        mpd-cover-glyph
-        (make-glyph (list (vector 'jpeg :file nocover))))))))
+  (unless (equal mpd-current-filename (mpd-file))
+    (with-current-buffer mpd-dock-buffer
+      (let ((cover (mpd-cover-file))
+           (nocover (expand-file-name "nocover.jpg" mpd-directory)))
+       (if (mpd-has-cover-p)
+           (progn
+             (setq cover (mpd-scale-cover cover 48))
+             (set-extent-end-glyph
+              mpd-cover-glyph
+              (make-glyph `([jpeg :data ,cover]))))
+         (set-extent-end-glyph
+          mpd-cover-glyph
+          (make-glyph `([jpeg :file ,nocover]))))))
+    (setq mpd-current-filename (mpd-file))))
 
 (defun mpd-update-variables ()
   "Requests status information."
@@ -423,6 +474,16 @@ Sets state to \"muted\" by side effect."
   (mpd-send "status")
   (mpd-update-cover))
 
+(defun mpd-clear-variables ()
+  "Clears the most relevant mpd variables."
+  (with-boundp '(**mpd-var-Title* **mpd-var-Artist* **mpd-var-Album*
+                                 **mpd-var-Genre* **mpd-var-Date*)
+    (setq **mpd-var-Title* nil
+         **mpd-var-Artist* nil
+         **mpd-var-Album* nil
+         **mpd-var-Genre* nil
+         **mpd-var-Date* nil)))
+
 \f
 ;;;; Dockapp section
 (defvar mpd-dock-frame-plist
@@ -441,18 +502,43 @@ Sets state to \"muted\" by side effect."
   "Frame properties for mpd dock.")
 
 (defun mpd-info (&rest args)
-  (let ((title (or **mpd-var-Title* "Unknown"))
-       (artist (or **mpd-var-Artist* "Unknown"))
-       (album (or **mpd-var-Album* "Unknown"))
-       (genre (or **mpd-var-Genre* "Unknown"))
-       (year (or **mpd-var-Date* "Unknown"))
-       (file (file-name-nondirectory **mpd-var-file*)))
-    (format "--[ %s ]\n
+  "Returns a string for use in the mpd balloon-help frame."
+  (with-boundp '(**mpd-var-Title* **mpd-var-Artist* **mpd-var-Album*
+                                 **mpd-var-Genre* **mpd-var-Date*)
+    (let ((title (or **mpd-var-Title* "Unknown"))
+         (artist (or **mpd-var-Artist* "Unknown"))
+         (album (or **mpd-var-Album* "Unknown"))
+         (genre (or **mpd-var-Genre* "Unknown"))
+         (year (or **mpd-var-Date* "Unknown"))
+         (file (file-name-nondirectory (mpd-file))))
+      (format (concat
+              (when (mpd-has-cover-p)
+                "\n\n")
+              "--[ %s ]\n
 Artist: %s
 Album: %s
 Year: %s  Genre: %s\n
 --[ %s ]"
-           title artist album year genre file)))
+              (when (mpd-has-cover-p)
+                "\n\n\n\n\n\n\n\n"))
+             title artist album year genre file))))
+
+(defun mpd-balloon-cover ()
+  "Inserts coverart into mpd balloon."
+  (let ((cover (mpd-cover-file)))
+    (when (mpd-has-cover-p)
+      (setq cover (mpd-scale-cover cover 128))
+      (set-extent-begin-glyph
+       (make-extent (point-min) (point-min))
+       (make-glyph `([jpeg :data ,cover]))))))
+
+(defadvice balloon-help-display-help (after mpd-balloon-cover (&rest args) activate)
+  "Display cover art image in the balloon."
+  (when (process-live-p (get-process "mpd"))
+    (set-buffer balloon-help-buffer)
+    (goto-char (point-max))
+    (and (re-search-backward (file-name-nondirectory (mpd-file)) nil t)
+        (mpd-balloon-cover))))
 
 (defconst mpd-prev-map
   (let* ((map (make-sparse-keymap 'mpd-prev-map)))
@@ -492,32 +578,28 @@ Year: %s  Genre: %s\n
          (set-extent-end-glyph
           (setq prev (make-extent (point-max) (point-max)))
           (make-glyph
-           (list (vector 'xpm :file (expand-file-name "Rewind.xpm"
-                                                      mpd-directory)))))
+           `([xpm :file ,(expand-file-name "Rewind.xpm" mpd-directory)])))
          (set-extent-properties 
           prev
           `(keymap ,mpd-prev-map balloon-help "Previous Track"))
          (set-extent-end-glyph
           (setq pause (make-extent (point-max) (point-max)))
           (make-glyph
-           (list (vector 'xpm :file (expand-file-name "Pause.xpm"
-                                                      mpd-directory)))))
+           `([xpm :file ,(expand-file-name "Pause.xpm" mpd-directory)])))
          (set-extent-properties
           pause
           `(keymap ,mpd-pause-map balloon-help "Pause"))
          (set-extent-end-glyph
           (setq play (make-extent (point-max) (point-max)))
           (make-glyph
-           (list (vector 'xpm :file (expand-file-name "Play.xpm"
-                                                      mpd-directory)))))
+           `([xpm :file ,(expand-file-name "Play.xpm" mpd-directory)])))
          (set-extent-properties
           play
           `(keymap ,mpd-play-map balloon-help "Play"))
          (set-extent-end-glyph
           (setq next (make-extent (point-max) (point-max)))
           (make-glyph
-           (list (vector 'xpm :file (expand-file-name "FFwd.xpm"
-                                                      mpd-directory)))))
+           `([xpm :file ,(expand-file-name "FFwd.xpm" mpd-directory)])))
          (set-extent-properties
           next
           `(keymap ,mpd-next-map balloon-help "Next Track"))
@@ -525,8 +607,8 @@ Year: %s  Genre: %s\n
          (set-extent-end-glyph
           (setq mpd-cover-glyph (make-extent (point-max) (point-max)))
           (make-glyph
-           (list (vector 'jpeg :file (expand-file-name "nocover.jpg"
-                                                       mpd-directory)))))
+           `([jpeg :file ,(expand-file-name "nocover.jpg"
+                                           mpd-directory)])))
          (set-extent-properties
           mpd-cover-glyph
           `(keymap ,mpd-pause-map balloon-help ,#'mpd-info)))))
@@ -546,6 +628,24 @@ Year: %s  Genre: %s\n
     (focus-frame cframe)
     (mpd-update-variables)))
 
+(defun mpd-now-playing ()
+  "Return a formatted string of Title and Artist.
+
+This is for use in things like Gnus for a X-Now-Playing header etc."
+  (with-fboundp 'taglib:show-tag
+    (let ((title (and mpd-current-filename
+                     (taglib:show-tag
+                      (expand-file-name
+                       (mpd-file) mpd-music-directory)
+                      'title)))
+         (artist (and mpd-current-filename
+                      (taglib:show-tag
+                       (expand-file-name
+                        (mpd-file) mpd-music-directory)
+                       'artist))))
+      (format "%s --- [%s]"
+             (or title "The Sounds of Silence")
+             (or artist "Marcel Marceau")))))
 
 (provide 'mpd)