]> git.draconx.ca Git - mpdhacks.git/blobdiff - mpdexec.pl
mpdthumb: Fix failure when readpicture/albumart both return data.
[mpdhacks.git] / mpdexec.pl
index 962656f2c957aa6920ed12cda42653f82a390927..e83146bcdc065489b6f6e5e4777db430cb19743a 100755 (executable)
 #!/usr/bin/env perl
 #
-# Copyright © 2012,2019 Nick Bowler
+# Copyright © 2012,2019-2021 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 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.
+# 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.
 
 use strict;
 
 use utf8;
 
+use Encode qw(decode encode);
 use Encode::Locale qw(decode_argv);
 decode_argv(Encode::FB_CROAK);
 
 binmode(STDOUT, ":utf8");
 binmode(STDIN, ":utf8");
-use IO::Socket::INET6;
 
-my $host = $ENV{MPD_HOST} // "localhost";
-my $port = $ENV{MPD_PORT} // 6600;
+use Getopt::Long qw(:config gnu_getopt);
 
-my $sock = new IO::Socket::INET6(
-       PeerAddr => $host,
-       PeerPort => $port,
-       Proto    => 'tcp',
-) or die "failed to connect to MPD: $!";
-binmode($sock, ":utf8");
+use FindBin;
+use lib "$FindBin::Bin";
+use MPDHacks;
 
-if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)) {
-       die "MPD failed to announce version: $!";
+my ($quiet, $verbose, $binary, $ignore_errors, $download);
+
+sub print_version {
+       print <<EOF
+mpdexec.pl 0.9
+Copyright © 2021 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) = (@_, *STDERR);
+
+       print $fh "Usage: $0 [options] [command ...]\n";
+       print $fh "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.
+  -v, --verbose     Print commands as they are submitted.  If both --quiet
+                    and --verbose are specified, --verbose has no effect.
+  -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 and readpicture
+                    commands; if this option is specified, such 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
 }
 
-sub mpd_escape {
-       ($_) = @_;
+GetOptions(
+       'host|h=s'         => \$MPD::host,
+       'port|p=s'         => \$MPD::port,
+
+       'quiet|q'          => \$quiet,
+       'no-quiet'         => sub { $quiet = 0; },
+       'verbose|v'        => \$verbose,
+       'no-verbose'       => sub { $verbose = 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; },
 
-       # No way to encode literal newlines in the protocol, so we convert
-       # any newlines in the arguments into a space, which can help with
-       # shell quoting.
-       s/\n/ /g;
+       'V|version'        => sub { print_version(); exit },
+       'H|help'           => sub { print_help(); exit },
+) or do { print_usage; exit 1 };
 
-       if (/[ \t\\"]/) {
-               s/[\\"]/\\$&/g;
-               return "\"$_\"";
+my $binfile = *STDOUT;
+if ($binary) {
+       if ($binary ne "-") {
+               open(my $fh, ">", $binary) or die "failed to open $binary: $!";
+               $binfile = $fh;
        }
-       return $_;
 }
+$quiet = 1 if (defined($binary) && $binary eq "");
+$verbose = 0 if ($quiet);
+
+my $sock = MPD::connect(binmode => ":raw");
+
+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_send {
+       my $cmd = encode('UTF-8', join(' ', @_), Encode::FB_QUIET);
+       print "$cmd\n" if ($verbose);
+       print $sock $cmd, $/;
+}
+
+my %downloadcmds = map { $_ => 1 } ( "albumart", "readpicture" );
 sub mpd_exec {
-       print $sock join(' ', @_), "\n";
+       my $downloadseq;
+
+       # special case for "albumart" and "readpicture"; if no offset is
+       # specified (invalid command) we synthesize a sequence of albumart
+       # commands to retrieve the entire file.
+       if ($download && $downloadcmds{$_[0]} && @_ == 2) {
+               $_[2] = 0;
+               $downloadseq = 2;
+       }
+
+       mpd_send(@_);
        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);
+               }
        }
 }
 
 if (@ARGV) {
-       mpd_exec(map { mpd_escape($_) } @ARGV)
+       mpd_exec(map { MPD::escape } @ARGV)
+} elsif ($ignore_errors) {
+       while (<>) { chomp; mpd_exec($_); }
 } else {
-       while (<>) {
-               chomp;
-               mpd_exec($_);
-       }
+       mpd_send("command_list_begin");
+       while (<>) { chomp; mpd_send($_); }
+       mpd_exec("command_list_end");
 }
 
 print $sock "close\n";