From: Nick Bowler Date: Fri, 28 Jun 2019 03:09:00 +0000 (-0400) Subject: mpdmenu: Retrieve cover art from MPD. X-Git-Url: https://git.draconx.ca/gitweb/mpdhacks.git/commitdiff_plain/614746d84dbada7c6f55c1d824e485d876f1018f mpdmenu: Retrieve cover art from MPD. 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. --- diff --git a/mpdexec.pl b/mpdexec.pl index 02c2950..0d477da 100755 --- a/mpdexec.pl +++ b/mpdexec.pl @@ -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 < 0); +} + +sub print_help { + print_usage(*STDOUT); + print <. +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); + } } } diff --git a/mpdmenu.pl b/mpdmenu.pl index 9a7c77e..2311a53 100755 --- a/mpdmenu.pl +++ b/mpdmenu.pl @@ -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 = ; 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 index 0000000..c48e398 --- /dev/null +++ b/mpdthumb.sh @@ -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 index a275749..0000000 --- a/thumbnail.zsh +++ /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 ] [--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