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