4 class App::MPD::Feeder;
6 use App::MPD::Feeder::DB;
7 use App::MPD::Feeder::Options;
8 use App::MPD::Feeder::WorkQueue;
12 use IO::Async::Signal;
13 use IO::Async::Timer::Countdown;
14 use IO::Async::Timer::Periodic;
15 use Log::Any qw($log);
18 use Syntax::Keyword::Try;
19 use Time::Duration qw(duration_exact);
21 has $cfg_file :reader;
24 has $db_needs_update :writer = 1;
26 has $mpd_connected = 0;
27 has $playlist_needs_filling = 1;
28 has $quit_requested = 0;
29 has $reload_requested = 0;
32 has $reconnect_delay = 5;
34 use constant DEFAULT_CONFIG_FILE => '/etc/mpd-feeder/mpd-feeder.conf';
37 Getopt::Long::Configure('pass_through');
38 Getopt::Long::GetOptions( 'cfg|config=s' => \$cfg_file );
39 Getopt::Long::Configure('no_pass_through');
41 $cfg_file //= DEFAULT_CONFIG_FILE if -e DEFAULT_CONFIG_FILE;
45 $db_needs_update = 0 if $opt->skip_db_update;
49 my $new_opt = App::MPD::Feeder::Options->new;
51 $new_opt->parse_config_file($cfg_file) if $cfg_file;
53 $new_opt->parse_command_line;
55 Log::Any::Adapter->set( Stderr => log_level => $new_opt->log_level );
59 $reconnect_delay = $opt->initial_reconnect_delay;
61 $db = App::MPD::Feeder::DB->new( opt => $opt );
67 my %conn = ( auto_connect => 0 );
68 $conn{host} = $opt->mpd_host if $opt->mpd_host;
69 $conn{port} = $opt->mpd_port if $opt->mpd_port;
71 $mpd = Net::Async::MPD->new(%conn);
75 $log->warn("Connection to MPD lost");
76 $mpd->loop->stop('disconnected');
78 $idler->cancel if $idler;
85 $playlist_needs_filling = 1;
94 my $loop = $mpd->loop;
96 my $int_signal_handler = sub {
97 state $signal_count = 0;
100 if ( $signal_count > 1 ) {
101 $log->warn("Another signal received (#$signal_count)");
102 $log->warn("Exiting abruptly");
106 $log->debug("Signal received. Stopping loop");
114 IO::Async::Signal->new(
116 on_receipt => $int_signal_handler,
122 IO::Async::Signal->new(
125 $log->debug("SIGHUP received. Scheduling reload");
126 $reload_requested = 1;
127 $loop->stop('reload');
134 IO::Async::Signal->new(
138 "SIGUSR1 received. Dumping configuration to STDERR");
139 my $old = select \*STDERR;
155 method update_db( $force = undef ) {
156 if ( !$db_needs_update and !$force ) {
157 $log->debug("Skipping DB update");
161 $log->info('Updating song database');
163 my $rows = $mpd->send('listallinfo')->get;
165 $log->trace('got all songs from MPD');
171 foreach my $entry (@$rows) {
172 next unless exists $entry->{file};
174 $self->db->store_song( $entry->{file},
175 $entry->{AlbumArtist} // $entry->{Artist},
181 my ($total_songs, $total_artists, $total_albums,
182 $new_songs, $new_artists, $new_albums
183 ) = $self->db->finish_update;
186 "Updated data about $song_count songs (including $new_songs new), "
187 . "$total_artists artists (including $new_artists new) "
189 . "and $total_albums albums (including $new_albums new)"
192 $db_needs_update = 0;
196 $self->db->cancel_update;
201 method queue_songs( $num = undef ) {
202 if ( !defined $num ) {
203 return unless $playlist_needs_filling;
205 $log->trace("Requesting playlist");
206 my $present = $mpd->send('playlist')->get // [];
207 $present = scalar(@$present);
209 $log->notice( "Playlist contains $present songs. Wanted: "
210 . $opt->target_queue_length );
211 if ( $present < $opt->target_queue_length ) {
212 $self->queue_songs( $opt->target_queue_length - $present );
215 $playlist_needs_filling = 0;
221 my @list = $self->db->find_suitable_songs($num);
223 die "Found no suitable songs" unless @list;
225 if ( @list < $num ) {
228 'Found only %d suitable songs instead of %d',
234 $log->info("About to add $num songs to the playlist");
237 for my $song (@list) {
238 my $path = $song->{song};
243 $log->debug( "Adding " . join( ', ', map {"«$_»"} @paths ) );
245 # MPD needs raw bytes
246 utf8::encode($_) for @paths;
249 push @commands, [ add => "\"$_\"" ];
251 my $f = $mpd->send( \@commands );
252 $f->on_fail( sub { die @_ } );
255 $self->db->note_song_qeued($_) for @list;
256 $playlist_needs_filling = 0;
263 $log->notice("disconnecting and re-starting");
267 my @exec = ( $0, '--config', $self->cfg_file, '--skip-db-update' );
268 if ( $log->is_trace ) {
269 $log->trace( 'exec ' . join( ' ', map { /\s/ ? "'$_'" : $_ } @exec ) );
275 if ( $idler && !$idler->is_ready ) {
276 if ($mpd_connected) {
277 $log->trace("hand-sending 'noidle'");
279 $mpd->{mpd_handle}->write("noidle\n");
282 $log->trace("not connected to MPD: skipping 'noidle'");
286 $log->trace("no idler found");
290 method sleep_before_reconnection {
291 $log->debug( "Waiting for "
292 . duration_exact($reconnect_delay)
293 . " before re-connecting" );
296 IO::Async::Timer::Countdown->new(
297 delay => $reconnect_delay,
298 on_expire => sub { $mpd->loop->stop },
302 $reconnect_delay = $reconnect_delay * 1.5;
303 $reconnect_delay = 120 if $reconnect_delay > 120;
308 unless ($mpd_connected) {
309 $log->trace("Connecting to MPD...");
310 my $f = $mpd->connect->await;
314 $playlist_needs_filling = 1;
315 $reconnect_delay = $opt->initial_reconnect_delay;
316 $mpd->loop->later( sub { $self->pulse } );
318 elsif ( $f->is_failed ) {
319 $mpd->loop->stop('disconnected');
320 $log->warn($f->failure);
321 $self->sleep_before_reconnection;
324 die "connect Future neither done nor failed!?";
330 if ($db_needs_update) {
332 $mpd->loop->later( sub { $self->pulse } );
336 if ($playlist_needs_filling) {
338 $mpd->loop->later( sub { $self->pulse } );
342 $log->debug("Waiting idle. PID=$$");
343 $last_mpd_comm = time;
344 $idler = $mpd->send("idle database playlist");
347 $log->trace('got out of idle');
349 if ( $idler->is_done ) {
350 my $result = $idler->get;
352 if ( ref $result and $result->{changed} ) {
353 my $changed = $result->{changed};
354 $changed = [$changed] unless ref $changed;
356 $mpd->emit($_) for @$changed;
359 elsif ( $idler->is_cancelled ) {
360 $log->trace("idle was cancelled");
363 elsif ( $idler->is_failed ) {
364 $log->warn("idle failed: ".$idler->failure);
376 my $loop = $mpd->loop;
379 IO::Async::Timer::Periodic->new(
382 if (!$mpd_connected) {
383 $log->trace("Not connected to MPD. Skipping alive check.");
384 $loop->stop('disconnected');
388 if ( time - $last_mpd_comm > 300 ) {
391 "no active MPD communication for more that 5 minutes");
392 $log->debug("forcing alive check");
397 "contacted MPD less than 5 minutes ago. skipping alive check"
405 if ( $quit_requested ) {
406 $log->trace("about to quit");
411 elsif ( $reload_requested ) {
416 $log->trace('About to run the loop');
418 $mpd->loop->later( sub { $self->pulse } );