]> git.draconx.ca Git - mpdhacks.git/blob - mpdthumb.sh
mpdthumb: Fix failure when readpicture/albumart both return data.
[mpdhacks.git] / mpdthumb.sh
1 #!/bin/sh
2 #
3 # Copyright © 2019-2022 Nick Bowler
4 #
5 # Generate thumbnails for cover art retrieved from MPD.
6 #
7 # License GPLv3+: GNU General Public License version 3 or any later version.
8 # This is free software: you are free to change and redistribute it.
9 # There is NO WARRANTY, to the extent permitted by law.
10
11 : "${XDG_CACHE_HOME=$HOME/.cache}"
12 : "${THUMBNAILDIR=$XDG_CACHE_HOME/mpdthumb}"
13
14 # Try to find mpdexec...
15 argv0=$0
16 case $argv0 in
17 /*) self=$argv0 ;;
18 */*) self=$PWD/${argv0#./} ;;
19 *) self=`command -v $argv0` ;;
20 esac
21 owndir=${self%/*}
22
23 if command -v "$owndir/mpdexec.pl" >/dev/null; then
24   : "${MPDEXEC=$owndir/mpdexec.pl}"
25 elif command -v mpdexec.pl >/dev/null; then
26   : "${MPDEXEC=mpdexec.pl}"
27 else
28   : "${MPDEXEC=mpdexec}"
29 fi
30
31 if command -v gm >/dev/null; then
32   : "${CONVERT=gm convert}"
33 else
34   : "${CONVERT=convert}"
35 fi
36
37 print_version () {
38   cat <<'EOF'
39 mpdthumb.sh 0.9
40 Copyright © 2021 Nick Bowler
41 License GPLv3+: GNU General Public License version 3 or any later version.
42 This is free software: you are free to change and redistribute it.
43 There is NO WARRANTY, to the extent permitted by law.
44 EOF
45 }
46
47 print_usage () {
48   test $# -eq 0 || { printf '%s: ' "$argv0"; printf "$@"; }
49   printf 'Usage: %s [options] file [file ...]\n' "$argv0"
50   test $# -eq 0 || printf 'Try %s --help for more information.\n' "$argv0"
51 }
52
53 print_help () {
54   print_usage
55   cat <<EOF
56 This is "mpdthumb": a tool to retrieve cover art thumbnails from MPD.
57
58 One or more filenames may be specified.  The cover art (if any) for each
59 file is retrieved and scaled to the requested size.  The thumbnails are
60 cached locally so that subsequent lookups return results very quickly.
61
62 The filenames of the generated thumbnails are printed one per line in
63 the same order of the associated files specified on the command line.
64 If a given file has no cover art, then a blank line is printed.
65
66 Options:
67   --size=[W][xH]    Specify the dimensions (in pixels) of a bounding
68                     rectangle into which the image will be scaled to fit.
69                     One (but not both) of the dimensions may be omitted
70                     to indicate no limit in that direction.  The default
71                     size is x128.
72   --small           Equivalent to --size=56
73   --binarylimit[=ARG]
74                     If ARG is omitted or 'yes', force the use of the
75                     binarylimit command to reduce the amount of data
76                     transferred in the hot cache case.  If ARG is 'no',
77                     then the old "large offset" method is used which is
78                     incompatible with newer MPD servers.  The default is
79                     'auto', which queries the server to determine the
80                     appropriate method.
81   --no-binarylimit  Equivalent to --binarylimit=no
82   --embedded        Attempt to retrieve embedded covers using readpicture.
83                     This requires (and implies) --binarylimit.
84   --no-embedded     Use only album-level covers.  This is the default.
85   --version         Print a version message and then exit.
86   --help            Print this message and then exit.
87
88 Report bugs to <nbowler@draconx.ca>.
89 EOF
90 }
91
92 opt_binarylimit=auto
93 opt_embedded=no
94
95 size=x128
96 lastarg=
97 dashdash=
98 for arg; do
99   if test ${lastarg:+y}; then
100     arg=$lastarg=$arg
101     lastarg=
102   fi
103
104   case $dashdash$arg in
105   --binarylimit=*) opt_binarylimit=${arg#--*=} ;;
106   --no-binarylimit) opt_binarylimit=no ;;
107   --binarylimit) opt_binarylimit=yes ;;
108   --embedded) opt_embedded=yes ;;
109   --no-embedded) opt_embedded=no ;;
110   --size=*) size=${arg#--size=} ;;
111   --small) size=56 ;;
112   --size) lastarg=$arg ;;
113   --version) print_version; exit ;;
114   --help) print_help; exit ;;
115   --) dashdash=: ;;
116   -*) print_usage 'unrecognized argument: %s\n' "$arg" 1>&2; exit 1 ;;
117   *) set x "$@" "$arg"; shift
118   esac
119
120   shift
121 done
122
123 w=${size%x*} h=${size#*x}
124 if expr "$w$h" : '[0-9][0-9]*$' >/dev/null; then :; else
125   print_usage 'invalid --size setting: %s\n' "$size" 1>&2
126   exit 1
127 fi
128
129 case $# in 0)
130   print_usage 'at least one filename is required\n' 1>&2
131   exit 1
132 esac
133
134 tmp=`mktemp`
135 exec 3>"$tmp" 4<"$tmp"
136 rm -f "$tmp"
137
138 tmp=`mktemp`
139 exec 5>"$tmp" 6<"$tmp"
140 rm -f "$tmp"
141
142 # --embedded implies --binarylimit
143 case $opt_embedded in
144 yes) use_embedded=true ;;
145 *) use_embedded=false ;;
146 esac
147 $use_embedded && opt_binarylimit=yes
148
149 # Test for binarylimit command in MPD server.  We want to minimize data
150 # transfer in order to make cache hits as fast as possible.
151 #
152 # On older servers we can set a ridiculously large offset which causes the
153 # server to send no data, but this is rejected by new servers.  On new servers
154 # we can limit the data transferred explicitly, but not less than 64 bytes
155 # for some reason (probably not a big problem).
156
157 use_binarylimit=true
158 case $opt_binarylimit in
159 auto) $MPDEXEC binarylimit 64 2>/dev/null || use_binarylimit=false ;;
160 no) use_binarylimit=false ;;
161 yes) :;;
162 *)
163   print_usage 'invalid --binarylimit argument; must be yes, no or auto' 1>&2
164   exit 1
165 esac
166
167 if $use_binarylimit; then
168   printf 'binarylimit 64\n' >&3
169   binarylimit_offset=0
170 else
171   binarylimit_offset=2147483647
172 fi
173
174 for file; do
175   case $file in
176   */*) dir=${file%/*}/ ;;
177   *) dir=/ ;;
178   esac
179
180   if $use_embedded; then
181     shift; set x "$@" "$file" "$dir"; shift
182   else
183     file=
184   fi
185
186   printf '%s\n' "$dir" ${file:+"$file"} | sed -n '/[    \\"'\'']/ {
187     s/[\\"]/\\&/g
188     s/.*/"&"/
189   }
190   s/.*/albumart & '"$binarylimit_offset"'/
191   1h
192   2s/^albumart/readpicture/p
193   $ {
194     g
195     p
196   }' >&3;
197 done
198
199 valid=
200 <&4 $MPDEXEC --verbose --ignore-errors >&5 2>&1 || exit
201 while read a b <&6; do
202   case $a in
203   readpicture|albumart)
204     prevn=$# file=$1 mode=$a; shift || exit; continue
205     ;;
206   size:)
207     if test x"$mode/$valid" = x"albumart/readpicture$prevn"; then
208       # readpicture result was OK, skip over albumart result
209       continue
210     fi
211     valid=$mode$#
212     ;;
213   ACK)
214     case $b in
215     *'{binarylimit}'*|*'{readpicture}'*)
216       printf '%s: %s\n' "$argv0" "$a $b" 1>&2; exit 1 ;;
217     esac
218     test x"$valid" = x"readpicture$prevn" || echo
219     continue
220     ;;
221   *) continue ;;
222   esac
223
224   # We combine the filename and the size to compute the cache key and
225   # hope this suffices to detect stale entries.  Unfortunately MPD does
226   # not currently give us the modified date which would be more useful...
227   cache_id=`printf 'MPD:%s:%s' "$file" "$b" | md5sum`
228   cache_id=${cache_id:+${cache_id%% *}_$size.png}
229
230   if test ! -f "$THUMBNAILDIR/$cache_id"; then
231     if test ! -f "$THUMBNAILDIR/CACHEDIR.TAG"; then
232       mkdir -p "$THUMBNAILDIR"
233       { cat >"$THUMBNAILDIR/CACHEDIR.TAG~" || exit; } <<'EOF'
234 Signature: 8a477f597d28d172789f06886806bc55
235 EOF
236       mv -f "$THUMBNAILDIR/CACHEDIR.TAG~" "$THUMBNAILDIR/CACHEDIR.TAG"
237     fi
238
239     # Not cached, retrieve the entire image
240     $MPDEXEC --binary --download "$mode" "$file" >&3 || exit
241     <&4 $CONVERT -scale "$size" - "$THUMBNAILDIR/tmp.$cache_id" ||
242         { rc=$? rm -f "$THUMBNAILDIR/tmp.$cache_id"; exit $rc; }
243     mv -f "$THUMBNAILDIR/tmp.$cache_id" "$THUMBNAILDIR/$cache_id"
244   fi
245
246   printf '%s\n' "$THUMBNAILDIR/$cache_id"
247 done