]> git.draconx.ca Git - mpdhacks.git/blob - mpdexec.pl
Fix quoting of single quotes in MPD protocol.
[mpdhacks.git] / mpdexec.pl
1 #!/usr/bin/env perl
2 #
3 # Copyright © 2012,2019-2020 Nick Bowler
4 #
5 # Send commands to MPD.  Each command-line argument is quoted as necessary
6 # so it appears as a single argument at the protocol level.  The result is
7 # printed to standard output.
8 #
9 # License GPLv3+: GNU General Public License version 3 or any later version.
10 # This is free software: you are free to change and redistribute it.
11 # There is NO WARRANTY, to the extent permitted by law.
12
13 use strict;
14
15 use utf8;
16
17 use Encode qw(decode encode);
18 use Encode::Locale qw(decode_argv);
19 decode_argv(Encode::FB_CROAK);
20
21 binmode(STDOUT, ":utf8");
22 binmode(STDIN, ":utf8");
23 use IO::Socket::INET6;
24
25 use Getopt::Long qw(:config gnu_getopt);
26
27 use FindBin;
28 use lib "$FindBin::Bin";
29 use MPDHacks;
30
31 my $host = $ENV{MPD_HOST} // "localhost";
32 my $port = $ENV{MPD_PORT} // 6600;
33 my ($quiet, $binary, $ignore_errors, $download);
34
35 sub print_version {
36         print <<EOF
37 mpdexec.pl 0.8
38 Copyright © 2019 Nick Bowler
39 License GPLv3+: GNU General Public License version 3 or any later version.
40 This is free software: you are free to change and redistribute it.
41 There is NO WARRANTY, to the extent permitted by law.
42 EOF
43 }
44
45 sub print_usage {
46         my $fh = $_[1] // *STDERR;
47
48         print $fh "Usage: $0 [options] [command ...]\n";
49         print "Try $0 --help for more information.\n" unless (@_ > 0);
50 }
51
52 sub print_help {
53         print_usage(*STDOUT);
54         print <<EOF
55 This is "mpdexec": a tool to send simple commands to MPD.
56
57 Options:
58   -h, --host=HOST   Connect to the MPD server on HOST, overriding defaults.
59   -p, --port=PORT   Connect to the MPD server on PORT, overriding defaults.
60   -q, --quiet       Do not output any response messages.  Only errors (on
61                     standard error) or binary data (if enabled) are output.
62   -b, --binary[=FILE]
63                     Output raw binary response data, which is normally not
64                     written.  If FILE is specified, the data is written there.
65                     Otherwise, --quiet is automatically enabled and the data
66                     goes to standard output.
67   --download        Enable automatic sequencing of albumart commands; if this
68                     option is specified, albumart commands without offsets will
69                     be expanded into multiple commands in order to download the
70                     entire file.
71   --ignore-errors   In batch mode, continue submitting commands after errors.
72   -V, --version     Print a version message and then exit.
73   -H, --help        Print this message and then exit.
74
75 Report bugs to <nbowler\@draconx.ca>.
76 EOF
77 }
78
79 GetOptions(
80         'host|h=s'         => \$host,
81         'port|p=s'         => \$port,
82
83         'quiet|q'          => \$quiet,
84         'no-quiet'         => sub { $quiet = 0; },
85         'binary|b:s'       => \$binary,
86         'no-binary'        => sub { $binary = undef; },
87
88         'download'         => \$download,
89         'no-download'      => sub { $download = 0; },
90
91         'ignore-errors'    => \$ignore_errors,
92         'no-ignore-errors' => sub { $ignore_errors = 0; },
93
94         'V|version'        => sub { print_version(); exit },
95         'H|help'           => sub { print_help(); exit },
96 ) or do { print_usage; exit 1 };
97
98 my $binfile = *STDOUT;
99 if ($binary) {
100         if ($binary ne "-") {
101                 open(my $fh, ">", $binary) or die "failed to open $binary: $!";
102                 $binfile = $fh;
103         }
104 }
105 $quiet = 1 if (defined($binary) && $binary eq "");
106
107 my $sock = new IO::Socket::INET6(
108         PeerAddr => $host,
109         PeerPort => $port,
110         Proto    => 'tcp',
111 ) or die "failed to connect to MPD: $!";
112 #binmode($sock, ":utf8");
113 binmode($sock);
114
115 if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)) {
116         die "MPD failed to announce version: $!";
117 }
118
119 sub read_binary {
120         my ($count) = @_;
121         my $buf;
122
123         binmode($binfile);
124
125         return 0 unless ($count);
126         my $rc = $sock->read($buf, $count) or die "$!";
127         if (defined($binary)) {
128                 $binfile->write($buf) or die "$!";
129         }
130
131         return $rc;
132 }
133
134 sub mpd_exec {
135         my $downloadseq;
136
137         # special case for "albumart"; if no offset is specified
138         # (invalid command) we synthesize a sequence of albumart
139         # commands to retrieve the entire file.
140         if ($download && $_[0] eq "albumart" && @_ == 2) {
141                 $_[2] = 0;
142                 $downloadseq = 2;
143         }
144
145         print $sock encode('UTF-8', join(' ', @_), Encode::FB_QUIET), $/;
146         while (<$sock>) {
147                 $_ = decode('UTF_8', $_, Encode::FB_QUIET);
148
149                 if (/^OK/) {
150                         last unless ($downloadseq);
151                         print $sock encode('UTF-8',
152                                            join(' ', @_),
153                                            Encode::FB_QUIET), $/;
154                         next;
155                 }
156
157                 if (/^binary: ([0-9]+)$/) {
158                         print unless ($quiet);
159                         read_binary($1);
160
161                         if ($downloadseq) {
162                                 $downloadseq = 0 unless ($1);
163                                 $_[$downloadseq] += $1;
164                         }
165                 } elsif (/^ACK/) {
166                         *STDOUT->flush;
167                         print STDERR;
168                         last if ($ignore_errors);
169                         exit 1;
170                 } else {
171                         print unless ($quiet);
172                 }
173         }
174 }
175
176 if (@ARGV) {
177         mpd_exec(map { MPD::escape($_) } @ARGV)
178 } else {
179         while (<>) {
180                 chomp;
181                 mpd_exec($_);
182         }
183 }
184
185 print $sock "close\n";
186 close $sock;