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