]> git.draconx.ca Git - mpdhacks.git/blob - mpdreload.pl
mpdreload: New script to refresh MPD play queue from a playlist.
[mpdhacks.git] / mpdreload.pl
1 #!/usr/bin/env perl
2 #
3 # Copyright © 2019 Nick Bowler
4 #
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.
8 #
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.
12
13 use strict;
14 use utf8;
15
16 use Encode::Locale qw(decode_argv);
17 decode_argv(Encode::FB_CROAK);
18
19 binmode(STDOUT, ":utf8");
20 use IO::Socket::INET6;
21 use IO::Socket::UNIX;
22
23 use Getopt::Long qw(:config gnu_getopt);
24
25 my $host = $ENV{MPD_HOST} // "localhost";
26 my $port = $ENV{MPD_PORT} // 6600;
27 my $sock;
28
29 # Quotes the argument so that it is presented as a single argument to MPD
30 # at the protocol level.
31 sub escape {
32         my $s = @_[0] // $_;
33
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.
37         $s =~ s/\n/ /g;
38
39         if (/[ \t\\"]/) {
40                 $s =~ s/[\\"]/\\$&/g;
41                 return "\"$s\"";
42         }
43
44         $s =~ s/^\s*$/"$&"/;
45         return $s;
46 }
47
48 # Submit a command to the MPD server; each argument to this function
49 # is quoted and sent as a single argument to MPD.
50 sub mpd_exec {
51         my $cmd = join(' ', map { escape } @_);
52
53         print $sock "$cmd\n";
54 }
55
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 {
59         my %matches;
60         my $entry;
61
62         mpd_exec("playlistinfo");
63         while (<$sock>) {
64                 last if /^OK/;
65                 die($_) if /^ACK/;
66         
67                 if (/^(\w+): (.*)$/) {
68                         if ($1 eq "file") {
69                                 if (exists($matches{$2})) {
70                                         $entry = $matches{$2};
71                                 } else {
72                                         $entry = {};
73                                         $matches{$2} = $entry;
74                                 }
75                         }
76
77                         if (exists($entry->{$1})) {
78                                 $entry->{$1}->{$2} = 1;
79                         } else {
80                                 $entry->{$1} = { $2 => 1 }
81                         }
82                 }
83         }
84
85         return \%matches;
86 }
87
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 {
91         my ($plname) = @_;
92         my @files;
93
94         mpd_exec("listplaylist", $plname);
95         while (<$sock>) {
96                 last if /^OK/;
97                 die($_) if /^ACK/;
98
99                 if (/^(\w+): (.*)$/) {
100                         if ($1 eq "file") {
101                                 push @files, $2;
102                         }
103                 }
104         }
105
106         return \@files;
107 }
108
109 sub print_version {
110         print <<EOF
111 mpdreload.pl 0.8
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.
116 EOF
117 }
118
119 sub print_usage {
120         my $fh = $_[1] // *STDERR;
121
122         print $fh "Usage: $0 [options] playlist\n";
123         print "Try $0 --help for more information.\n" unless (@_ > 0);
124 }
125
126 sub print_help {
127         print_usage(*STDOUT);
128         print <<EOF
129 This is "mpdreload": a tool to reload a stored playlist into the MPD queue.
130
131 Options:
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.
136
137 Report bugs to <nbowler\@draconx.ca>
138 EOF
139 }
140
141 GetOptions(
142         'host|h=s' => \$host,
143         'port|p=s' => \$port,
144
145         'V|version' => sub { print_version(); exit },
146         'H|help'    => sub { print_help(); exit },
147 ) or do { print_usage(); exit 1};
148
149 if (@ARGV != 1) {
150         print STDERR "Playlist name is required\n" unless @ARGV;
151         print STDERR "Excess command-line arguments\n" if @ARGV;
152
153         print_usage(); exit 1
154 };
155
156 # Connect to MPD.
157 if ($host =~ /^[@\/]/) {
158         $host =~ s/^@/\0/;
159         $sock = new IO::Socket::UNIX(Type => SOCK_STREAM(), Peer => $host);
160 } else {
161         $sock = new IO::Socket::INET6(PeerAddr => $host,
162                                       PeerPort => $port,
163                                       Proto    => 'tcp');
164 }
165 $sock or die "failed to connect to MPD: $!";
166 binmode($sock, ":utf8");
167
168 if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/)) {
169         die "MPD failed to announce version: $!";
170 }
171
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]);
175
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};
180
181         my $id = (keys %$ids)[0];
182         delete $ids->{$id};
183
184         # Remove tracks with no unused queue IDs
185         delete $current->{$f} unless (keys %$ids > 0);
186
187         if (defined $id) {
188                 mpd_exec("moveid", $id, $i);
189         } else {
190                 mpd_exec("addid", $f, $i);
191         }
192 }
193
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", $_);
199         }
200 }
201
202 mpd_exec("command_list_end");
203 while (<$sock>) {
204         last if /^OK$/;
205         die($_) if /^ACK/;
206 }