]> git.draconx.ca Git - mpdhacks.git/blob - mpdreload.pl
Fix quoting of single quotes in MPD protocol.
[mpdhacks.git] / mpdreload.pl
1 #!/usr/bin/env perl
2 #
3 # Copyright © 2019-2020 Nick Bowler
4 #
5 # Replace the current MPD play queue with a saved playlist, by rearranging
6 # existing queue entries when possible.  This avoids losing the current
7 # player state when the loaded playlist is similar to the current queue.
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 use utf8;
15
16 use Encode::Locale qw(decode_argv);
17 decode_argv(Encode::FB_CROAK);
18
19 binmode(STDOUT, ":utf8");
20 use IO::Socket::INET6;
21 use IO::Socket::UNIX;
22
23 use Getopt::Long qw(:config gnu_getopt);
24
25 use FindBin;
26 use lib "$FindBin::Bin";
27 use MPDHacks;
28
29 my $host = $ENV{MPD_HOST} // "localhost";
30 my $port = $ENV{MPD_PORT} // 6600;
31 my $sock;
32
33 # Submit a command to the MPD server; each argument to this function
34 # is quoted and sent as a single argument to MPD.
35 sub mpd_exec {
36         my $cmd = join(' ', map { MPD::escape } @_);
37
38         print $sock "$cmd\n";
39 }
40
41 # Returns a hash reference containing all tracks in the current play queue.
42 # The hash keys are filenames.
43 sub get_tracks_in_play_queue {
44         my %matches;
45         my $entry;
46
47         mpd_exec("playlistinfo");
48         while (<$sock>) {
49                 last if /^OK/;
50                 die($_) if /^ACK/;
51
52                 if (/^(\w+): (.*)$/) {
53                         if ($1 eq "file") {
54                                 if (exists($matches{$2})) {
55                                         $entry = $matches{$2};
56                                 } else {
57                                         $entry = {};
58                                         $matches{$2} = $entry;
59                                 }
60                         }
61
62                         if (exists($entry->{$1})) {
63                                 $entry->{$1}->{$2} = 1;
64                         } else {
65                                 $entry->{$1} = { $2 => 1 }
66                         }
67                 }
68         }
69
70         return \%matches;
71 }
72
73 # Given an MPD playlist name, returns a reference to an array containing
74 # (in order) the files in the playlist.
75 sub get_playlist_files {
76         my ($plname) = @_;
77         my @files;
78
79         mpd_exec("listplaylist", $plname);
80         while (<$sock>) {
81                 last if /^OK/;
82                 die($_) if /^ACK/;
83
84                 if (/^(\w+): (.*)$/) {
85                         if ($1 eq "file") {
86                                 push @files, $2;
87                         }
88                 }
89         }
90
91         return \@files;
92 }
93
94 sub print_version {
95         print <<EOF
96 mpdreload.pl 0.8
97 Copyright © 2019 Nick Bowler
98 License GPLv3+: GNU General Public License version 3 or any later version.
99 This is free software: you are free to change and redistribute it.
100 There is NO WARRANTY, to the extent permitted by law.
101 EOF
102 }
103
104 sub print_usage {
105         my $fh = $_[1] // *STDERR;
106
107         print $fh "Usage: $0 [options] playlist\n";
108         print "Try $0 --help for more information.\n" unless (@_ > 0);
109 }
110
111 sub print_help {
112         print_usage(*STDOUT);
113         print <<EOF
114 This is "mpdreload": a tool to reload a stored playlist into the MPD queue.
115
116 Options:
117   -h, --host=HOST   Connect to the MPD server on HOST, overriding defaults.
118   -p, --port=PORT   Connect to the MPD server on PORT, overriding defaults.
119   -V, --version     Print a version message and then exit.
120   -H, --help        Print this message and then exit.
121
122 Report bugs to <nbowler\@draconx.ca>
123 EOF
124 }
125
126 GetOptions(
127         'host|h=s' => \$host,
128         'port|p=s' => \$port,
129
130         'V|version' => sub { print_version(); exit },
131         'H|help'    => sub { print_help(); exit },
132 ) or do { print_usage(); exit 1};
133
134 if (@ARGV != 1) {
135         print STDERR "Playlist name is required\n" unless @ARGV;
136         print STDERR "Excess command-line arguments\n" if @ARGV;
137
138         print_usage(); exit 1
139 };
140
141 # Connect to MPD.
142 if ($host =~ /^[@\/]/) {
143         $host =~ s/^@/\0/;
144         $sock = new IO::Socket::UNIX(Type => SOCK_STREAM(), Peer => $host);
145 } else {
146         $sock = new IO::Socket::INET6(PeerAddr => $host,
147                                       PeerPort => $port,
148                                       Proto    => 'tcp');
149 }
150 $sock or die "failed to connect to MPD: $!";
151 binmode($sock, ":utf8");
152
153 if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)) {
154         die "MPD failed to announce version: $!";
155 }
156
157 # Retrieve the current play queue and target play queue.
158 my $current = get_tracks_in_play_queue();
159 my $target = get_playlist_files($ARGV[0]);
160
161 mpd_exec("command_list_begin");
162 for (my $i = 0; $i < @$target; $i++) {
163         my $f = $target->[$i];
164         my $ids = $current->{$f}->{Id};
165
166         my $id = (keys %$ids)[0];
167         delete $ids->{$id};
168
169         # Remove tracks with no unused queue IDs
170         delete $current->{$f} unless (keys %$ids > 0);
171
172         if (defined $id) {
173                 mpd_exec("moveid", $id, $i);
174         } else {
175                 mpd_exec("addid", $f, $i);
176         }
177 }
178
179 # Remove any tracks left from the old play queue.
180 foreach (keys %$current) {
181         my $ids = $current->{$_}->{Id};
182         foreach (keys %$ids) {
183                 mpd_exec("deleteid", $_);
184         }
185 }
186
187 mpd_exec("command_list_end");
188 while (<$sock>) {
189         last if /^OK$/;
190         die($_) if /^ACK/;
191 }