+#!/usr/bin/env perl
+#
+# Copyright © 2019 Nick Bowler
+#
+# Replace the current MPD play queue with a saved playlist, by rearranging
+# existing queue entries when possible. This avoids losing the current
+# player state when the loaded playlist is similar to the current queue.
+#
+# 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 Encode::Locale qw(decode_argv);
+decode_argv(Encode::FB_CROAK);
+
+binmode(STDOUT, ":utf8");
+use IO::Socket::INET6;
+use IO::Socket::UNIX;
+
+use Getopt::Long qw(:config gnu_getopt);
+
+my $host = $ENV{MPD_HOST} // "localhost";
+my $port = $ENV{MPD_PORT} // 6600;
+my $sock;
+
+# Quotes the argument so that it is presented as a single argument to MPD
+# at the protocol level.
+sub escape {
+ my $s = @_[0] // $_;
+
+ # No way to encode literal newlines in the protocol, so we
+ # convert any newlines in the arguments into a space, which
+ # can help with quoting.
+ $s =~ s/\n/ /g;
+
+ if (/[ \t\\"]/) {
+ $s =~ s/[\\"]/\\$&/g;
+ return "\"$s\"";
+ }
+
+ $s =~ s/^\s*$/"$&"/;
+ return $s;
+}
+
+# Submit a command to the MPD server; each argument to this function
+# is quoted and sent as a single argument to MPD.
+sub mpd_exec {
+ my $cmd = join(' ', map { escape } @_);
+
+ print $sock "$cmd\n";
+}
+
+# Returns a hash reference containing all tracks in the current play queue.
+# The hash keys are filenames.
+sub get_tracks_in_play_queue {
+ my %matches;
+ my $entry;
+
+ mpd_exec("playlistinfo");
+ while (<$sock>) {
+ last if /^OK/;
+ die($_) if /^ACK/;
+
+ if (/^(\w+): (.*)$/) {
+ if ($1 eq "file") {
+ if (exists($matches{$2})) {
+ $entry = $matches{$2};
+ } else {
+ $entry = {};
+ $matches{$2} = $entry;
+ }
+ }
+
+ if (exists($entry->{$1})) {
+ $entry->{$1}->{$2} = 1;
+ } else {
+ $entry->{$1} = { $2 => 1 }
+ }
+ }
+ }
+
+ return \%matches;
+}
+
+# Given an MPD playlist name, returns a reference to an array containing
+# (in order) the files in the playlist.
+sub get_playlist_files {
+ my ($plname) = @_;
+ my @files;
+
+ mpd_exec("listplaylist", $plname);
+ while (<$sock>) {
+ last if /^OK/;
+ die($_) if /^ACK/;
+
+ if (/^(\w+): (.*)$/) {
+ if ($1 eq "file") {
+ push @files, $2;
+ }
+ }
+ }
+
+ return \@files;
+}
+
+sub print_version {
+ print <<EOF
+mpdreload.pl 0.8
+Copyright © 2019 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 = $_[1] // *STDERR;
+
+ print $fh "Usage: $0 [options] playlist\n";
+ print "Try $0 --help for more information.\n" unless (@_ > 0);
+}
+
+sub print_help {
+ print_usage(*STDOUT);
+ print <<EOF
+This is "mpdreload": a tool to reload a stored playlist into the MPD queue.
+
+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.
+ -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' => \$host,
+ 'port|p=s' => \$port,
+
+ 'V|version' => sub { print_version(); exit },
+ 'H|help' => sub { print_help(); exit },
+) or do { print_usage(); exit 1};
+
+if (@ARGV != 1) {
+ print STDERR "Playlist name is required\n" unless @ARGV;
+ print STDERR "Excess command-line arguments\n" if @ARGV;
+
+ print_usage(); exit 1
+};
+
+# Connect to MPD.
+if ($host =~ /^[@\/]/) {
+ $host =~ s/^@/\0/;
+ $sock = new IO::Socket::UNIX(Type => SOCK_STREAM(), Peer => $host);
+} else {
+ $sock = new IO::Socket::INET6(PeerAddr => $host,
+ PeerPort => $port,
+ Proto => 'tcp');
+}
+$sock or die "failed to connect to MPD: $!";
+binmode($sock, ":utf8");
+
+if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)) {
+ die "MPD failed to announce version: $!";
+}
+
+# Retrieve the current play queue and target play queue.
+my $current = get_tracks_in_play_queue();
+my $target = get_playlist_files($ARGV[0]);
+
+mpd_exec("command_list_begin");
+for (my $i = 0; $i < @$target; $i++) {
+ my $f = $target->[$i];
+ my $ids = $current->{$f}->{Id};
+
+ my $id = (keys %$ids)[0];
+ delete $ids->{$id};
+
+ # Remove tracks with no unused queue IDs
+ delete $current->{$f} unless (keys %$ids > 0);
+
+ if (defined $id) {
+ mpd_exec("moveid", $id, $i);
+ } else {
+ mpd_exec("addid", $f, $i);
+ }
+}
+
+# Remove any tracks left from the old play queue.
+foreach (keys %$current) {
+ my $ids = $current->{$_}->{Id};
+ foreach (keys %$ids) {
+ mpd_exec("deleteid", $_);
+ }
+}
+
+mpd_exec("command_list_end");
+while (<$sock>) {
+ last if /^OK$/;
+ die($_) if /^ACK/;
+}