#!/usr/bin/env perl
#
-# Copyright © 2019-2020 Nick Bowler
+# Copyright © 2019-2021 Nick Bowler
#
# Replace the current MPD play queue with a saved playlist, by rearranging
# existing queue entries when possible. This avoids losing the current
decode_argv(Encode::FB_CROAK);
binmode(STDOUT, ":utf8");
-use IO::Socket::INET6;
-use IO::Socket::UNIX;
use Getopt::Long qw(:config gnu_getopt);
use lib "$FindBin::Bin";
use MPDHacks;
-my $host = $ENV{MPD_HOST} // "localhost";
-my $port = $ENV{MPD_PORT} // 6600;
my $sock;
-# 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 { MPD::escape } @_);
+my $pl_current_length;
- print $sock "$cmd\n";
-}
-
-# Returns a hash reference containing all tracks in the current play queue.
-# The hash keys are filenames.
+# Returns a hash reference mapping filenames to an array reference listing
+# the queue IDs for that file in the current play queue.
sub get_tracks_in_play_queue {
- my %matches;
- my $entry;
+ my (%matches, %idmap, $entry);
+ my $pos = -1;
- mpd_exec("playlistinfo");
+ 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 }
+ $entry = $matches{$2} //= [];
+ $pos++;
+ } elsif ($1 eq "Id") {
+ push @$entry, $2;
+ $idmap{$2} = $pos;
}
}
}
- return \%matches;
+ $pl_current_length = $pos+1;
+
+ return (\%matches, \%idmap);
}
# Given an MPD playlist name, returns a reference to an array containing
my ($plname) = @_;
my @files;
- mpd_exec("listplaylist", $plname);
+ MPD::exec("listplaylist", $plname);
while (<$sock>) {
last if /^OK/;
die($_) if /^ACK/;
}
sub print_usage {
- my $fh = $_[1] // *STDERR;
+ my ($fh) = (@_, *STDERR);
print $fh "Usage: $0 [options] playlist\n";
- print "Try $0 --help for more information.\n" unless (@_ > 0);
+ print $fh "Try $0 --help for more information.\n" unless (@_ > 0);
}
sub print_help {
}
GetOptions(
- 'host|h=s' => \$host,
- 'port|p=s' => \$port,
+ 'host|h=s' => \$MPD::host,
+ 'port|p=s' => \$MPD::port,
'V|version' => sub { print_version(); exit },
'H|help' => sub { print_help(); exit },
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: $!";
-}
+$sock = MPD::connect();
# Retrieve the current play queue and target play queue.
-my $current = get_tracks_in_play_queue();
+MPD::run("tagtypes", "clear");
+my ($current, $idmap) = get_tracks_in_play_queue();
my $target = get_playlist_files($ARGV[0]);
-mpd_exec("command_list_begin");
+my $add_start;
+
+sub load_tracks($$) {
+ my ($seq, $dst) = @_;
+ my ($newlen, $count);
+
+ my $start = $add_start // $seq;
+ my $add_position = $pl_current_length;
+ my $end = $seq+1;
+
+ $dst //= $start;
+
+ MPD::exec("load", $ARGV[0], "$start:$end");
+ MPD::exec("status");
+ MPD::exec("command_list_end");
+
+ while (<$sock>) {
+ last if (/^OK/);
+ die($_) if (/^ACK/);
+
+ if (/^(\w+): (.*)$/) {
+ if ($1 eq "playlistlength") {
+ $newlen = int($2);
+ }
+ }
+ }
+
+ $count = $newlen - $pl_current_length;
+
+ MPD::exec("command_list_begin");
+ if ($newlen > $pl_current_length) {
+ MPD::exec("move", "$add_position:$newlen", $dst)
+ if ($add_position != $dst);
+ }
+
+ $pl_current_length = $newlen;
+ undef $add_start;
+
+ return $count;
+}
+
+MPD::exec("command_list_begin");
+my ($num_added, $num_failed) = (0, 0);
for (my $i = 0; $i < @$target; $i++) {
my $f = $target->[$i];
- my $ids = $current->{$f}->{Id};
+ my $id = shift @{ $current->{$f} };
- my $id = (keys %$ids)[0];
- delete $ids->{$id};
+ if (defined $id and defined $add_start) {
+ my $n = $i - $add_start;
+ my $m = load_tracks($i-1, $add_start - $num_failed);
- # Remove tracks with no unused queue IDs
- delete $current->{$f} unless (keys %$ids > 0);
+ $num_added += $m;
+ $num_failed += $n - $m;
+ }
if (defined $id) {
- mpd_exec("moveid", $id, $i);
+ # Try not to move tracks already in the right place.
+ MPD::exec("moveid", $id, $i - $num_failed)
+ if ($i - $num_failed != $idmap->{$id} + $num_added);
} else {
- mpd_exec("addid", $f, $i);
+ $add_start //= $i;
}
}
-# Remove any tracks left from the old play queue.
-foreach (keys %$current) {
- my $ids = $current->{$_}->{Id};
- foreach (keys %$ids) {
- mpd_exec("deleteid", $_);
- }
-}
+# Now all unwanted tracks from the original playqueue have been moved to the
+# end and can be deleted all at once.
-mpd_exec("command_list_end");
-while (<$sock>) {
- last if /^OK$/;
- die($_) if /^ACK/;
-}
+my $pos = ($add_start // @$target) - $num_failed;
+MPD::exec("delete", $pos . ":") if map { @$_ } values %$current;
+MPD::exec("load", $ARGV[0], "$add_start:") if defined $add_start;
+MPD::run("command_list_end");