3 # Copyright © 2019 Nick Bowler
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.
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.
16 use Encode::Locale qw(decode_argv);
17 decode_argv(Encode::FB_CROAK);
19 binmode(STDOUT, ":utf8");
20 use IO::Socket::INET6;
23 use Getopt::Long qw(:config gnu_getopt);
25 my $host = $ENV{MPD_HOST} // "localhost";
26 my $port = $ENV{MPD_PORT} // 6600;
29 # Quotes the argument so that it is presented as a single argument to MPD
30 # at the protocol level.
34 # No way to encode literal newlines in the protocol, so we
35 # convert any newlines in the arguments into a space, which
36 # can help with quoting.
48 # Submit a command to the MPD server; each argument to this function
49 # is quoted and sent as a single argument to MPD.
51 my $cmd = join(' ', map { escape } @_);
56 # Returns a hash reference containing all tracks in the current play queue.
57 # The hash keys are filenames.
58 sub get_tracks_in_play_queue {
62 mpd_exec("playlistinfo");
67 if (/^(\w+): (.*)$/) {
69 if (exists($matches{$2})) {
70 $entry = $matches{$2};
73 $matches{$2} = $entry;
77 if (exists($entry->{$1})) {
78 $entry->{$1}->{$2} = 1;
80 $entry->{$1} = { $2 => 1 }
88 # Given an MPD playlist name, returns a reference to an array containing
89 # (in order) the files in the playlist.
90 sub get_playlist_files {
94 mpd_exec("listplaylist", $plname);
99 if (/^(\w+): (.*)$/) {
112 Copyright © 2019 Nick Bowler
113 License GPLv3+: GNU General Public License version 3 or any later version.
114 This is free software: you are free to change and redistribute it.
115 There is NO WARRANTY, to the extent permitted by law.
120 my $fh = $_[1] // *STDERR;
122 print $fh "Usage: $0 [options] playlist\n";
123 print "Try $0 --help for more information.\n" unless (@_ > 0);
127 print_usage(*STDOUT);
129 This is "mpdreload": a tool to reload a stored playlist into the MPD queue.
132 -h, --host=HOST Connect to the MPD server on HOST, overriding defaults.
133 -p, --port=PORT Connect to the MPD server on PORT, overriding defaults.
134 -V, --version Print a version message and then exit.
135 -H, --help Print this message and then exit.
137 Report bugs to <nbowler\@draconx.ca>
142 'host|h=s' => \$host,
143 'port|p=s' => \$port,
145 'V|version' => sub { print_version(); exit },
146 'H|help' => sub { print_help(); exit },
147 ) or do { print_usage(); exit 1};
150 print STDERR "Playlist name is required\n" unless @ARGV;
151 print STDERR "Excess command-line arguments\n" if @ARGV;
153 print_usage(); exit 1
157 if ($host =~ /^[@\/]/) {
159 $sock = new IO::Socket::UNIX(Type => SOCK_STREAM(), Peer => $host);
161 $sock = new IO::Socket::INET6(PeerAddr => $host,
165 $sock or die "failed to connect to MPD: $!";
166 binmode($sock, ":utf8");
168 if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)) {
169 die "MPD failed to announce version: $!";
172 # Retrieve the current play queue and target play queue.
173 my $current = get_tracks_in_play_queue();
174 my $target = get_playlist_files($ARGV[0]);
176 mpd_exec("command_list_begin");
177 for (my $i = 0; $i < @$target; $i++) {
178 my $f = $target->[$i];
179 my $ids = $current->{$f}->{Id};
181 my $id = (keys %$ids)[0];
184 # Remove tracks with no unused queue IDs
185 delete $current->{$f} unless (keys %$ids > 0);
188 mpd_exec("moveid", $id, $i);
190 mpd_exec("addid", $f, $i);
194 # Remove any tracks left from the old play queue.
195 foreach (keys %$current) {
196 my $ids = $current->{$_}->{Id};
197 foreach (keys %$ids) {
198 mpd_exec("deleteid", $_);
202 mpd_exec("command_list_end");