#!/usr/bin/env perl
#
-# Copyright © 2012 Nick Bowler
+# Copyright © 2012,2019-2021 Nick Bowler
#
-# Simple program to send a command to MPD. 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 encoding 'utf8';
-use IO::Socket::INET6;
-my $host = $ENV{MPD_HOST} // "localhost";
-my $port = $ENV{MPD_PORT} // 6600;
+use Encode qw(decode encode);
+use Encode::Locale qw(decode_argv);
+decode_argv(Encode::FB_CROAK);
-my $sock = new IO::Socket::INET6(
- PeerAddr => $host,
- PeerPort => $port,
- Proto => 'tcp',
-) or die "failed to connect to MPD: $!";
-binmode($sock, ":utf8");
+binmode(STDOUT, ":utf8");
+binmode(STDIN, ":utf8");
-if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)) {
- die "MPD failed to announce version: $!";
+use Getopt::Long qw(:config gnu_getopt);
+
+use FindBin;
+use lib "$FindBin::Bin";
+use MPDHacks;
+
+my ($quiet, $binary, $ignore_errors, $download);
+
+sub print_version {
+ print <<EOF
+mpdexec.pl 0.8
+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.
+ -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
+}
+
+GetOptions(
+ 'host|h=s' => \$MPD::host,
+ 'port|p=s' => \$MPD::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 = 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;
+}
+
+my %downloadcmds = map { $_ => 1 } ( "albumart", "readpicture" );
+sub mpd_exec {
+ 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;
+ }
+
+ print $sock encode('UTF-8', join(' ', @_), Encode::FB_QUIET), $/;
+ while (<$sock>) {
+ $_ = 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);
+ }
+ }
}
-print $sock join(' ', @ARGV), "\n";
-while (<$sock>) {
- last if (/^OK/);
- print;
- exit 1 if (/^ACK/);
+if (@ARGV) {
+ mpd_exec(map { MPD::escape($_) } @ARGV)
+} else {
+ while (<>) {
+ chomp;
+ mpd_exec($_);
+ }
}
print $sock "close\n";