]> git.draconx.ca Git - mpdhacks.git/blob - mpdreload.pl
Factor out MPD connection code.
[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 # Submit a command to the MPD server; each argument to this function
30 # is quoted and sent as a single argument to MPD.
31 sub mpd_exec {
32         my $cmd = join(' ', map { MPD::escape } @_);
33
34         print $sock "$cmd\n";
35 }
36
37 # Returns a hash reference containing all tracks in the current play queue.
38 # The hash keys are filenames.
39 sub get_tracks_in_play_queue {
40         my %matches;
41         my $entry;
42
43         mpd_exec("playlistinfo");
44         while (<$sock>) {
45                 last if /^OK/;
46                 die($_) if /^ACK/;
47
48                 if (/^(\w+): (.*)$/) {
49                         if ($1 eq "file") {
50                                 if (exists($matches{$2})) {
51                                         $entry = $matches{$2};
52                                 } else {
53                                         $entry = {};
54                                         $matches{$2} = $entry;
55                                 }
56                         }
57
58                         if (exists($entry->{$1})) {
59                                 $entry->{$1}->{$2} = 1;
60                         } else {
61                                 $entry->{$1} = { $2 => 1 }
62                         }
63                 }
64         }
65
66         return \%matches;
67 }
68
69 # Given an MPD playlist name, returns a reference to an array containing
70 # (in order) the files in the playlist.
71 sub get_playlist_files {
72         my ($plname) = @_;
73         my @files;
74
75         mpd_exec("listplaylist", $plname);
76         while (<$sock>) {
77                 last if /^OK/;
78                 die($_) if /^ACK/;
79
80                 if (/^(\w+): (.*)$/) {
81                         if ($1 eq "file") {
82                                 push @files, $2;
83                         }
84                 }
85         }
86
87         return \@files;
88 }
89
90 sub print_version {
91         print <<EOF
92 mpdreload.pl 0.8
93 Copyright © 2019 Nick Bowler
94 License GPLv3+: GNU General Public License version 3 or any later version.
95 This is free software: you are free to change and redistribute it.
96 There is NO WARRANTY, to the extent permitted by law.
97 EOF
98 }
99
100 sub print_usage {
101         my $fh = $_[1] // *STDERR;
102
103         print $fh "Usage: $0 [options] playlist\n";
104         print "Try $0 --help for more information.\n" unless (@_ > 0);
105 }
106
107 sub print_help {
108         print_usage(*STDOUT);
109         print <<EOF
110 This is "mpdreload": a tool to reload a stored playlist into the MPD queue.
111
112 Options:
113   -h, --host=HOST   Connect to the MPD server on HOST, overriding defaults.
114   -p, --port=PORT   Connect to the MPD server on PORT, overriding defaults.
115   -V, --version     Print a version message and then exit.
116   -H, --help        Print this message and then exit.
117
118 Report bugs to <nbowler\@draconx.ca>
119 EOF
120 }
121
122 GetOptions(
123         'host|h=s' => \$MPD::host,
124         'port|p=s' => \$MPD::port,
125
126         'V|version' => sub { print_version(); exit },
127         'H|help'    => sub { print_help(); exit },
128 ) or do { print_usage(); exit 1};
129
130 if (@ARGV != 1) {
131         print STDERR "Playlist name is required\n" unless @ARGV;
132         print STDERR "Excess command-line arguments\n" if @ARGV;
133
134         print_usage(); exit 1
135 };
136
137 $sock = MPD::connect();
138
139 # Retrieve the current play queue and target play queue.
140 my $current = get_tracks_in_play_queue();
141 my $target = get_playlist_files($ARGV[0]);
142
143 mpd_exec("command_list_begin");
144 for (my $i = 0; $i < @$target; $i++) {
145         my $f = $target->[$i];
146         my $ids = $current->{$f}->{Id};
147
148         my $id = (keys %$ids)[0];
149         delete $ids->{$id};
150
151         # Remove tracks with no unused queue IDs
152         delete $current->{$f} unless (keys %$ids > 0);
153
154         if (defined $id) {
155                 mpd_exec("moveid", $id, $i);
156         } else {
157                 mpd_exec("addid", $f, $i);
158         }
159 }
160
161 # Remove any tracks left from the old play queue.
162 foreach (keys %$current) {
163         my $ids = $current->{$_}->{Id};
164         foreach (keys %$ids) {
165                 mpd_exec("deleteid", $_);
166         }
167 }
168
169 mpd_exec("command_list_end");
170 while (<$sock>) {
171         last if /^OK$/;
172         die($_) if /^ACK/;
173 }