]> git.draconx.ca Git - mpdhacks.git/blob - mpdreload.pl
mpdthumb: Fix failure when readpicture/albumart both return data.
[mpdhacks.git] / mpdreload.pl
1 #!/usr/bin/env perl
2 #
3 # Copyright © 2019-2021 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
21 use Getopt::Long qw(:config gnu_getopt);
22
23 use FindBin;
24 use lib "$FindBin::Bin";
25 use MPDHacks;
26
27 my $sock;
28
29 my $pl_current_length;
30
31 # Returns a hash reference mapping filenames to an array reference listing
32 # the queue IDs for that file in the current play queue.
33 sub get_tracks_in_play_queue {
34         my (%matches, %idmap, $entry);
35         my $pos = -1;
36
37         MPD::exec("playlistinfo");
38         while (<$sock>) {
39                 last if /^OK/;
40                 die($_) if /^ACK/;
41
42                 if (/^(\w+): (.*)$/) {
43                         if ($1 eq "file") {
44                                 $entry = $matches{$2} //= [];
45                                 $pos++;
46                         } elsif ($1 eq "Id") {
47                                 push @$entry, $2;
48                                 $idmap{$2} = $pos;
49                         }
50                 }
51         }
52
53         $pl_current_length = $pos+1;
54
55         return (\%matches, \%idmap);
56 }
57
58 # Given an MPD playlist name, returns a reference to an array containing
59 # (in order) the files in the playlist.
60 sub get_playlist_files {
61         my ($plname) = @_;
62         my @files;
63
64         MPD::exec("listplaylist", $plname);
65         while (<$sock>) {
66                 last if /^OK/;
67                 die($_) if /^ACK/;
68
69                 if (/^(\w+): (.*)$/) {
70                         if ($1 eq "file") {
71                                 push @files, $2;
72                         }
73                 }
74         }
75
76         return \@files;
77 }
78
79 sub print_version {
80         print <<EOF
81 mpdreload.pl 0.8
82 Copyright © 2019 Nick Bowler
83 License GPLv3+: GNU General Public License version 3 or any later version.
84 This is free software: you are free to change and redistribute it.
85 There is NO WARRANTY, to the extent permitted by law.
86 EOF
87 }
88
89 sub print_usage {
90         my ($fh) = (@_, *STDERR);
91
92         print $fh "Usage: $0 [options] playlist\n";
93         print $fh "Try $0 --help for more information.\n" unless (@_ > 0);
94 }
95
96 sub print_help {
97         print_usage(*STDOUT);
98         print <<EOF
99 This is "mpdreload": a tool to reload a stored playlist into the MPD queue.
100
101 Options:
102   -h, --host=HOST   Connect to the MPD server on HOST, overriding defaults.
103   -p, --port=PORT   Connect to the MPD server on PORT, overriding defaults.
104   -V, --version     Print a version message and then exit.
105   -H, --help        Print this message and then exit.
106
107 Report bugs to <nbowler\@draconx.ca>
108 EOF
109 }
110
111 GetOptions(
112         'host|h=s' => \$MPD::host,
113         'port|p=s' => \$MPD::port,
114
115         'V|version' => sub { print_version(); exit },
116         'H|help'    => sub { print_help(); exit },
117 ) or do { print_usage(); exit 1};
118
119 if (@ARGV != 1) {
120         print STDERR "Playlist name is required\n" unless @ARGV;
121         print STDERR "Excess command-line arguments\n" if @ARGV;
122
123         print_usage(); exit 1
124 };
125
126 $sock = MPD::connect();
127
128 # Retrieve the current play queue and target play queue.
129 MPD::run("tagtypes", "clear");
130 my ($current, $idmap) = get_tracks_in_play_queue();
131 my $target = get_playlist_files($ARGV[0]);
132
133 my $add_start;
134
135 sub load_tracks($$) {
136         my ($seq, $dst) = @_;
137         my ($newlen, $count);
138
139         my $start = $add_start // $seq;
140         my $add_position = $pl_current_length;
141         my $end = $seq+1;
142
143         $dst //= $start;
144
145         MPD::exec("load", $ARGV[0], "$start:$end");
146         MPD::exec("status");
147         MPD::exec("command_list_end");
148
149         while (<$sock>) {
150                 last if (/^OK/);
151                 die($_) if (/^ACK/);
152
153                 if (/^(\w+): (.*)$/) {
154                         if ($1 eq "playlistlength") {
155                                 $newlen = int($2);
156                         }
157                 }
158         }
159
160         $count = $newlen - $pl_current_length;
161
162         MPD::exec("command_list_begin");
163         if ($newlen > $pl_current_length) {
164                 MPD::exec("move", "$add_position:$newlen", $dst)
165                         if ($add_position != $dst);
166         }
167
168         $pl_current_length = $newlen;
169         undef $add_start;
170
171         return $count;
172 }
173
174 MPD::exec("command_list_begin");
175 my ($num_added, $num_failed) = (0, 0);
176 for (my $i = 0; $i < @$target; $i++) {
177         my $f = $target->[$i];
178         my $id = shift @{ $current->{$f} };
179
180         if (defined $id and defined $add_start) {
181                 my $n = $i - $add_start;
182                 my $m = load_tracks($i-1, $add_start - $num_failed);
183
184                 $num_added += $m;
185                 $num_failed += $n - $m;
186         }
187
188         if (defined $id) {
189                 # Try not to move tracks already in the right place.
190                 MPD::exec("moveid", $id, $i - $num_failed)
191                         if ($i - $num_failed != $idmap->{$id} + $num_added);
192         } else {
193                 $add_start //= $i;
194         }
195 }
196
197 # Now all unwanted tracks from the original playqueue have been moved to the
198 # end and can be deleted all at once.
199
200 my $pos = ($add_start // @$target) - $num_failed;
201 MPD::exec("delete", $pos . ":") if map { @$_ } values %$current;
202 MPD::exec("load", $ARGV[0], "$add_start:") if defined $add_start;
203 MPD::run("command_list_end");