From adba02b582138375bfd3adc9f68c0cd398827e7a Mon Sep 17 00:00:00 2001 From: Nick Bowler Date: Fri, 20 Dec 2019 01:01:01 -0500 Subject: [PATCH] mpdreload: New script to refresh MPD play queue from a playlist. 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 | 206 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100755 mpdreload.pl diff --git a/mpdreload.pl b/mpdreload.pl new file mode 100755 index 0000000..4c77e83 --- /dev/null +++ b/mpdreload.pl @@ -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 < 0); +} + +sub print_help { + print_usage(*STDOUT); + print < +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/; +} -- 2.43.2