]> git.draconx.ca Git - mpdhacks.git/commitdiff
mpdmenu: Retrieve cover art from MPD.
authorNick Bowler <nbowler@draconx.ca>
Fri, 28 Jun 2019 03:09:00 +0000 (23:09 -0400)
committerNick Bowler <nbowler@draconx.ca>
Sat, 29 Jun 2019 16:05:59 +0000 (12:05 -0400)
Recent versions of MPD support retrieving cover art, which means we
(mostly) don't need to have the music directory mounted to poke around
in it.  Augment mpdexec with support for binary transfers in order to
retrieve this cover art, and implement a new thumbnailer script which
makes use of this functionality.

Mostly, because the function to launch geeqie in the scans directory
still requires poking in the music dir.  But all the menu images should
be displayed just from the connection to MPD now.

As a bonus, the new script is quite a bit faster than the old one, too.

mpdexec.pl
mpdmenu.pl
mpdthumb.sh [new file with mode: 0755]
thumbnail.zsh [deleted file]

index 02c29507689e4af3e788b3ecb31d6f968821c755..0d477dae465d0923b297fea31d2af3919508d54a 100755 (executable)
@@ -2,9 +2,9 @@
 #
 # Copyright © 2012,2019 Nick Bowler
 #
-# Simple program to send a command to MPD.  Each command-line argument is
-# quoted as necessary so it appears as a single argument at the protocol
-# level.  The result is printed to standard output.
+# Send commands to MPD.  Each command-line argument is quoted as necessary
+# so it appears as a single argument at the protocol level.  The result is
+# printed to standard output.
 #
 # License GPLv3+: GNU General Public License version 3 or any later version.
 # This is free software: you are free to change and redistribute it.
@@ -14,6 +14,7 @@ use strict;
 
 use utf8;
 
+use Encode qw(decode encode);
 use Encode::Locale qw(decode_argv);
 decode_argv(Encode::FB_CROAK);
 
@@ -21,15 +22,91 @@ binmode(STDOUT, ":utf8");
 binmode(STDIN, ":utf8");
 use IO::Socket::INET6;
 
+use Getopt::Long qw(:config gnu_getopt);
+
 my $host = $ENV{MPD_HOST} // "localhost";
 my $port = $ENV{MPD_PORT} // 6600;
+my ($quiet, $binary, $ignore_errors, $download);
+
+sub print_version {
+       print <<EOF
+mpdexec.pl 0.8
+Copyright © 2019 Nick Bowler
+License GPLv3+: GNU General Public License version 3 or any later version.
+This is free software: you are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law.
+EOF
+}
+
+sub print_usage {
+       my $fh = $_[1] // *STDERR;
+
+       print $fh "Usage: $0 [options] [command ...]\n";
+       print "Try $0 --help for more information.\n" unless (@_ > 0);
+}
+
+sub print_help {
+       print_usage(*STDOUT);
+       print <<EOF
+This is "mpdexec": a tool to send simple commands to MPD.
+
+Options:
+  -h, --host=HOST   Connect to the MPD server on HOST, overriding defaults.
+  -p, --port=PORT   Connect to the MPD server on PORT, overriding defaults.
+  -q, --quiet       Do not output any response messages.  Only errors (on
+                    standard error) or binary data (if enabled) are output.
+  -b, --binary[=FILE]
+                    Output raw binary response data, which is normally not
+                    written.  If FILE is specified, the data is written there.
+                    Otherwise, --quiet is automatically enabled and the data
+                    goes to standard output.
+  --download        Enable automatic sequencing of albumart commands; if this
+                    option is specified, albumart commands without offsets will
+                    be expanded into multiple commands in order to download the
+                    entire file.
+  --ignore-errors   In batch mode, continue submitting commands after errors.
+  -V, --version     Print a version message and then exit.
+  -H, --help        Print this message and then exit.
+
+Report bugs to <nbowler\@draconx.ca>.
+EOF
+}
+
+GetOptions(
+       'host|h=s'         => \$host,
+       'port|p=s'         => \$port,
+
+       'quiet|q'          => \$quiet,
+       'no-quiet'         => sub { $quiet = 0; },
+       'binary|b:s'       => \$binary,
+       'no-binary'        => sub { $binary = undef; },
+
+       'download'         => \$download,
+       'no-download'      => sub { $download = 0; },
+
+       'ignore-errors'    => \$ignore_errors,
+       'no-ignore-errors' => sub { $ignore_errors = 0; },
+
+       'V|version'        => sub { print_version(); exit },
+       'H|help'           => sub { print_help(); exit },
+) or do { print_usage; exit 1 };
+
+my $binfile = *STDOUT;
+if ($binary) {
+       if ($binary ne "-") {
+               open(my $fh, ">", $binary) or die "failed to open $binary: $!";
+               $binfile = $fh;
+       }
+}
+$quiet = 1 if (defined($binary) && $binary eq "");
 
 my $sock = new IO::Socket::INET6(
        PeerAddr => $host,
        PeerPort => $port,
        Proto    => 'tcp',
 ) or die "failed to connect to MPD: $!";
-binmode($sock, ":utf8");
+#binmode($sock, ":utf8");
+binmode($sock);
 
 if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)) {
        die "MPD failed to announce version: $!";
@@ -50,12 +127,60 @@ sub mpd_escape {
        return $_;
 }
 
+sub read_binary {
+       my ($count) = @_;
+       my $buf;
+
+       binmode($binfile);
+
+       return 0 unless ($count);
+       my $rc = $sock->read($buf, $count) or die "$!";
+       if (defined($binary)) {
+               $binfile->write($buf) or die "$!";
+       }
+
+       return $rc;
+}
+
 sub mpd_exec {
-       print $sock join(' ', @_), "\n";
+       my $downloadseq;
+
+       # special case for "albumart"; if no offset is specified
+       # (invalid command) we synthesize a sequence of albumart
+       # commands to retrieve the entire file.
+       if ($download && $_[0] eq "albumart" && @_ == 2) {
+               $_[2] = 0;
+               $downloadseq = 2;
+       }
+
+       print $sock encode('UTF-8', join(' ', @_), Encode::FB_QUIET), $/;
        while (<$sock>) {
-               last if (/^OK/);
-               print;
-               exit 1 if (/^ACK/);
+               $_ = decode('UTF_8', $_, Encode::FB_QUIET);
+
+               if (/^OK/) {
+                       last unless ($downloadseq);
+                       print $sock encode('UTF-8',
+                                          join(' ', @_),
+                                          Encode::FB_QUIET), $/;
+                       next;
+               }
+
+               if (/^binary: ([0-9]+)$/) {
+                       print unless ($quiet);
+                       read_binary($1);
+
+                       if ($downloadseq) {
+                               $downloadseq = 0 unless ($1);
+                               $_[$downloadseq] += $1;
+                       }
+               } elsif (/^ACK/) {
+                       *STDOUT->flush;
+                       print STDERR;
+                       last if ($ignore_errors);
+                       exit 1;
+               } else {
+                       print unless ($quiet);
+               }
        }
 }
 
index 9a7c77efe9f4f185dede74aab9cb4e202b3cfb7d..2311a539cf546f8e301277618142551cf1fe7810 100755 (executable)
@@ -18,7 +18,7 @@ use FindBin;
 
 use constant {
        MPD_MJR_MIN => 0,
-       MPD_MNR_MIN => 13,
+       MPD_MNR_MIN => 21,
        MPD_REV_MIN => 0,
 };
 
@@ -33,11 +33,10 @@ my $MUSIC = $ENV{MUSIC} // "/srv/music";
 # get_item_thumbnails({ options }, file, ...)
 # get_item_thumbnails(file, ...)
 #
-# For each music file listed, obtain a thumbnail (if any) for the
-# cover art.
+# For each music file listed, obtain a thumbnail (if any) for the cover art.
 #
-# The first argument is a hash reference to control the mode of
-# operation; it may be omitted for default options.
+# The first argument is a hash reference to control the mode of operation;
+# it may be omitted for default options.
 #
 #   get_item_thumbnails({ small => 1 }, ...) - smaller thumbnails
 #
@@ -60,17 +59,16 @@ sub get_item_thumbnails {
                $c = "%";
        }
 
+       open THUMB, "-|", "$FindBin::Bin/mpdthumb.sh", @opts, "--", @_;
        foreach (@_) {
-               open THUMB, "-|", "$FindBin::Bin/thumbnail.zsh", "--music",
-                                 @opts, $_;
                my $thumb = <THUMB>;
                chomp $thumb;
 
                $thumb = "$c$thumb$c" if (-f $thumb);
                push @results, $thumb;
-               close THUMB;
-               die("thumbnail.zsh failed") if ($?);
        }
+       close THUMB;
+       die("mpdthumb failed") if ($?);
 
        return @results;
 }
diff --git a/mpdthumb.sh b/mpdthumb.sh
new file mode 100755 (executable)
index 0000000..c48e398
--- /dev/null
@@ -0,0 +1,120 @@
+#!/bin/sh
+#
+# Copyright © 2019 Nick Bowler
+#
+# Generate thumbnails for cover art retrieved from MPD.
+#
+# License GPLv3+: GNU General Public License version 3 or any later version.
+# This is free software: you are free to change and redistribute it.
+# There is NO WARRANTY, to the extent permitted by law.
+
+: "${XDG_CACHE_HOME=$HOME/.cache}"
+: "${THUMBNAILDIR=$XDG_CACHE_HOME/mpdthumb}"
+
+# Try to find mpdexec...
+case $0 in
+/*) self=$0 ;;
+*/*) self=$PWD/${0#./} ;;
+*) self=`command -v $0` ;;
+esac
+owndir=${self%/*}
+
+if command -v "$owndir/mpdexec.pl" >/dev/null; then
+  : "${MPDEXEC=$owndir/mpdexec.pl}"
+elif command -v mpdexec.pl >/dev/null; then
+  : "${MPDEXEC=mpdexec.pl}"
+else
+  : "${MPDEXEC=mpdexec}"
+fi
+
+if command -v gm >/dev/null; then
+  : "${CONVERT=gm convert}"
+else
+  : "${CONVERT=convert}"
+fi
+
+size=x128
+
+lastarg=
+dashdash=
+for arg; do
+  if test ${lastarg:+y}; then
+    arg=$lastarg=$arg
+    lastarg=
+  fi
+
+  case $dashdash$arg in
+  --size=*) size=${arg#--size=} ;;
+  --small) size=56 ;;
+  --size) lastarg=$arg ;;
+  --) dashdash=: ;;
+  -*) printf '%s: unrecognized argument: %s\n' "$0" "$arg" 1>&2; exit 1 ;;
+  *) set x "$@" "$arg"; shift
+  esac
+
+  shift
+done
+
+w=${size%x*} h=${size#*x}
+if expr "$w$h" : '[0-9][0-9]*$' >/dev/null; then :; else
+  printf '%s: invalid --size setting: %s\n' "$0" "$size" 1>&2
+  exit 1
+fi
+
+case $# in 0)
+  printf 'usage: %s [options] file [file ...]\n' "$0" 1>&2
+  exit 1
+esac
+
+tmp=`mktemp`
+exec 3>"$tmp" 4<"$tmp"
+rm -f "$tmp"
+
+tmp=`mktemp`
+exec 5>"$tmp" 6<"$tmp"
+rm -f "$tmp"
+
+for arg; do
+  arg=${arg%/*}/
+  shift; set x "$@" "$arg"; shift
+
+  printf '%s\n' "$arg" | sed '/[       \\"]/ {
+    s/[\\"]/\\&/g
+    s/.*/"&"/
+  }
+  s/.*/albumart & 2147483647/' >&3;
+done
+
+<&4 $MPDEXEC --ignore-errors >&5 2>&1 || exit
+while read a b <&6; do
+  case $a in
+  size:) :;;
+  ACK) echo; shift || exit; continue ;;
+  *) continue ;;
+  esac
+
+  # We combine the filename and the size to compute the cache key and
+  # hope this suffices to detect stale entries.  Unfortunately MPD does
+  # not currently give us the modified date which would be more useful...
+  file=$1; shift || exit
+  cache_id=`printf 'MPD:%s:%s' "$file" "$b" | md5sum`
+  cache_id=${cache_id:+${cache_id%% *}_$size.png}
+
+  if test ! -f "$THUMBNAILDIR/$cache_id"; then
+    if test ! -f "$THUMBNAILDIR/CACHEDIR.TAG"; then
+      mkdir -p "$THUMBNAILDIR"
+      { cat >"$THUMBNAILDIR/CACHEDIR.TAG~" || exit; } <<'EOF'
+Signature: 8a477f597d28d172789f06886806bc55
+EOF
+      mv -f "$THUMBNAILDIR/CACHEDIR.TAG~" "$THUMBNAILDIR/CACHEDIR.TAG"
+    fi
+
+    # Not cached, retrieve the entire image
+    $MPDEXEC --binary --download albumart "$file" >&3 || exit
+    <&4 $CONVERT -scale "$size" - "$THUMBNAILDIR/tmp.$cache_id" ||
+        { rc=$? rm -f "$THUMBNAILDIR/tmp.$cache_id"; exit $rc; }
+    mv -f "$THUMBNAILDIR/tmp.$cache_id" "$THUMBNAILDIR/$cache_id"
+  fi
+
+  printf '%s\n' "$THUMBNAILDIR/$cache_id"
+done
diff --git a/thumbnail.zsh b/thumbnail.zsh
deleted file mode 100755 (executable)
index a275749..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/env zsh
-#
-# Copyright © 2008, 2017 Nick Bowler
-#
-# Simple thumbnail generator for use with FVWM.  Thumbnails can be generated at
-# any desired size, and are cached for future use.  Prints the cached filename
-# to standard output.
-#
-# License WTFPL2: Do What The Fuck You Want To Public License, version 2.
-# This is free software: you are free to do what the fuck you want to.
-# There is NO WARRANTY, to the extent permitted by law.
-
-# resolve_file [file]
-#
-# If the argument is a symbolic link, print the target of that link.
-# Otherwise, prints the basename of file.
-resolve_file () {
-  test $# -eq 1 || return
-
-  # Ensure filename won't be confused for any kind of find argument...
-  case $1 in
-  /*) :;;
-  *) set x "./$1"; shift
-  esac
-
-  find "$1" -prune \( -type l -printf '%l' -o -printf '%f' \)
-}
-
-if [[ -z "$MUSIC" ]]; then
-       MUSIC=/home/music
-fi
-
-thumbs="$HOME/.fvwm/.thumbs"
-
-if ! [[ -d "$thumbs" ]]; then
-       mkdir "$thumbs" || exit 1
-fi
-
-size="x128"
-printimg=""
-ismusic=""
-
-while [[ "${1#--}" != "$1" && "$1" != "-" ]]; do
-       if   [[ "$1" == "--small" ]]; then
-               size="56"
-       elif [[ "$1" == "--size"  ]]; then
-               size="$2"
-               shift
-       elif [[ "$1" = "--music"  ]]; then
-               ismusic="yes"
-       elif [[ "$1" = "--image"  ]]; then
-               printimg="yes"
-       else
-               echo "unrecognised option: $1" 1>&2
-               exit 1
-       fi
-       shift
-done
-[ "$1" = "-" ] && shift
-
-if ! [[ "$size" =~ '^([0-9]*(x[0-9]+)?)$' ]]; then
-       echo "invalid size specification: $size" 1>&2
-       exit 1
-fi
-
-if [[ -z "$1" ]]; then
-       echo "usage: thumbnail.zsh [--small|--size <spec>] [--image] [--music] path" 1>&2
-       exit 1
-fi
-
-if [[ -n "$ismusic" ]]; then
-       imgpath="$MUSIC/$(dirname "${1#$MUSIC}")/cover.jpg"
-       [[ ! -f "$imgpath" ]] && imgpath="${imgpath%jpg}png"
-else
-       imgpath="$1"
-fi
-
-[[ ! -f "$imgpath" ]] && exit 0
-image=`resolve_file "$imgpath"`
-case $image in
-/*) :;;
-*) image=`dirname $imgpath`/$image
-esac
-[[ ! -f "$image" ]] && exit 0
-
-thumb="$thumbs/$(echo -n $image | md5sum - | cut -d ' ' -f 1)_$size.png"
-if [[ -f "$thumb" ]]; then
-       mtime_s="$(stat -c %Y -- "$image")"
-       mtime_t="$(stat -c %Y -- "$thumb")"
-       if [ "$mtime_s" -gt "$mtime_t" ]; then
-               convert -scale "$size" "$image" "$thumb"
-       fi
-else
-       convert -scale "$size" "$image" "$thumb"
-fi
-
-echo "$thumb"
-[ -n "$printimg" ] && echo "$image"
-exit 0