]> git.draconx.ca Git - mpdhacks.git/commitdiff
mpdreload: New script to refresh MPD play queue from a playlist.
authorNick Bowler <nbowler@draconx.ca>
Fri, 20 Dec 2019 06:01:01 +0000 (01:01 -0500)
committerNick Bowler <nbowler@draconx.ca>
Fri, 20 Dec 2019 06:01:01 +0000 (01:01 -0500)
This script implements the effect of clearing the current play queue
and then loading a playlist, except that the play queue is not wiped
during the process which avoids losing playback state unnecessarily.

mpdreload.pl [new file with mode: 0755]

diff --git a/mpdreload.pl b/mpdreload.pl
new file mode 100755 (executable)
index 0000000..4c77e83
--- /dev/null
@@ -0,0 +1,206 @@
+#!/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/;
+}