#
# 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.
use utf8;
+use Encode qw(decode encode);
use Encode::Locale qw(decode_argv);
decode_argv(Encode::FB_CROAK);
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: $!";
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);
+ }
}
}
use constant {
MPD_MJR_MIN => 0,
- MPD_MNR_MIN => 13,
+ MPD_MNR_MIN => 21,
MPD_REV_MIN => 0,
};
# 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
#
$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;
}
--- /dev/null
+#!/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
+++ /dev/null
-#!/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