X-Git-Url: https://git.ktnx.net/?a=blobdiff_plain;f=bin%2Fmpd-feeder;h=be0fc2c073b7e2c0ba466442b535d05afa87dbbe;hb=744b004c547dab563ead08fa0da87dabf6e537e9;hp=95227e9c6dbfd2934a9372d838fafe638233ce55;hpb=099f323a72ce907c8f0183f5f924218762d35346;p=mpd-feeder.git diff --git a/bin/mpd-feeder b/bin/mpd-feeder index 95227e9..be0fc2c 100755 --- a/bin/mpd-feeder +++ b/bin/mpd-feeder @@ -3,12 +3,15 @@ use v5.32; use Getopt::Long (); +use Log::Any qw($log); +use Log::Any::Adapter Stderr => log_level => 'trace'; use Object::Pad; use Syntax::Keyword::Try; class Options { use Time::Duration qw(duration_exact); use Time::Duration::Parse qw(parse_duration); + has $log_level :reader = 'warn'; has $target_queue_length :reader = 10; has $mpd_host :reader = undef; has $mpd_port :reader = undef; @@ -18,32 +21,18 @@ class Options { has $min_album_interval :reader = parse_duration('5h'); has $min_song_interval :reader = parse_duration('13d'); has $min_artist_interval :reader = parse_duration('1h 15m'); - has $verbose :reader = 0; - has $single :reader = 0; - has $one_shot :reader = 0; - has $dump_config :reader = 0; - - method verb($message) { - return unless $self->opt->verbose; - warn "$message\n"; - } - method dbg($message) { - return unless $self->opt->verbose > 1; - warn "$message\n"; - } + has $skip_db_update :reader = 0; method parse_command_line { Getopt::Long::GetOptions( - 'v|verbose+' => \$verbose, - 'dump-config!' => \$dump_config, - 's|single!' => \$single, - 'one-shot!' => \$one_shot, + 'log-level=s' => \$log_level, + 'skip-db-update!' => \$skip_db_update, 'tql|target-queue-length=n' => \$target_queue_length, 'mpd-host=s' => \$mpd_host, 'mpd-port=s' => \$mpd_port, 'db-path=s' => \$db_path, 'db-user=s' => \$db_user, - 'min-album-interval=s' => sub { + 'min-album-interval=s' => sub { $min_album_interval = parse_duration(pop); }, 'min-sing-interval=s' => sub { @@ -69,7 +58,7 @@ class Options { method dump { say "[mpd-feeder]"; - say "verbose = $verbose"; + say "log_level = $log_level"; say ""; say "[mpd]"; say "host = " . ( $mpd_host // '' ); @@ -95,7 +84,7 @@ class Options { handle_config_option( $ini => mpd => host => \$mpd_host ); handle_config_option( $ini => mpd => port => \$mpd_port ); - handle_config_option( $ini => 'mpd-feeder' => verbose => \$verbose ); + handle_config_option( $ini => 'mpd-feeder' => log_level => \$log_level ); handle_config_option( $ini => queue => 'target-length' => \$target_queue_length ); @@ -124,10 +113,16 @@ class Feeder { has $opt :reader; has $db; has $db_generation; - has $mpd; + has $db_needs_update = 1; + has $mpd :reader; use constant DEFAULT_CONFIG_FILE => '/etc/mpd-feeder/mpd-feeder.conf'; +use DBD::Pg; +use DBI; +use Log::Any qw($log); +use Net::Async::MPD; + ADJUST { $opt = Options->new; @@ -144,27 +139,32 @@ use constant DEFAULT_CONFIG_FILE => '/etc/mpd-feeder/mpd-feeder.conf'; $opt->parse_command_line; - unless ($opt->dump_config) { - $mpd = Net::Async::MPD->new( - host => $opt->mpd_host, - port => $opt->mpd_port, - auto_connect => 1, - ); + $db_needs_update = 0 if $opt->skip_db_update; - $self->connect_db; - $self->update_db; - } + Log::Any::Adapter->set( Stderr => log_level => $opt->log_level ); + } + + method connect_mpd { + return if $mpd; + + my %conn = ( auto_connect => 1 ); + $conn{host} = $opt->mpd_host if $opt->mpd_host; + $conn{port} = $opt->mpd_port if $opt->mpd_port; + + $mpd = Net::Async::MPD->new(%conn); } method connect_db { return if $db; - $db = - DBI->connect( "dbi:Pg:dbname=" . $opt->db_path, + $db = DBI->connect( "dbi:Pg:dbname=" . $opt->db_path, $opt->db_user, $opt->db_password, - { RaiseError => 1, AutoCommit => 1 } ); + { RaiseError => 1, PrintError => 0, AutoCommit => 1 } ); + $log->info( "Connected to database " . $opt->db_path ); $db_generation = $self->db_get_option('generation'); + $log->debug("DB generation is $db_generation"); + $self->update_db; } method db_get_option($name) { @@ -186,7 +186,7 @@ use constant DEFAULT_CONFIG_FILE => '/etc/mpd-feeder/mpd-feeder.conf'; $db->prepare_cached( <<'SQL')->execute( $song, $artist, $album, $db_generation ); INSERT INTO songs(path, artist, album, generation) -VALUES($1, $2, $3, $3) +VALUES($1, $2, $3, $4) ON CONFLICT ON CONSTRAINT songs_pkey DO UPDATE SET artist = $2 , album = $3 @@ -207,12 +207,19 @@ SQL } method db_remove_stale_entries { - $db->prepare_cached('DELETE FROM songs WHERE generation <> ?') - ->execute($db_generation); - $db->prepare_cached('DELETE FROM albums WHERE generation <> ?') - ->execute($db_generation); - $db->prepare_cached('DELETE FROM artists WHERE generation <> ?') - ->execute($db_generation); + my $sth = + $db->prepare_cached('DELETE FROM songs WHERE generation <> ?'); + $sth->execute($db_generation); + $log->debug( sprintf( "Deleted %d stale songs", $sth->rows ) ); + + $sth = $db->prepare_cached('DELETE FROM albums WHERE generation <> ?'); + $sth->execute($db_generation); + $log->debug( sprintf( "Deleted %d stale albums", $sth->rows ) ); + + $sth = + $db->prepare_cached('DELETE FROM artists WHERE generation <> ?'); + $sth->execute($db_generation); + $log->debug( sprintf( "Deleted %d stale artists", $sth->rows ) ); } method db_note_song_qeued($item) { @@ -227,72 +234,74 @@ SQL )->execute( $item->{artist}, $item->{album} ); } - method update_db() { - $mpd->send('listallinfo')->on_done( - sub { - try { - $db->begin; - - $db_generation++; - - my ($song, $artist, $album); - - foreach my $row (@_) { - chomp($row); - - if ($row =~ s/^file:\s*//) { - $self->db_store_song( $song, $artist, $album ); - $song = $row; - $artist = $album = undef; - } - elsif ( $row =~ s/^Artist:\s*// ) { - $artist = $row; - } - elsif ( $row =~ s/^Album:\s*// ) { - $album = $row; - } - } + method update_db($force = undef) { + if (!$db_needs_update and !$force) { + $log->debug("Skipping DB update"); + return; + } - $self->db_store_song($song, $artist, $album); + $log->info('Updating song database'); + $self->connect_mpd; + $self->connect_db; - $self->db_remove_stale_entries; + my $rows = $mpd->send('listallinfo')->get; + try { + $db->begin_work; - $self->db_set_option( generation => $db_generation ); + $db_generation++; - $db->commit; - } - catch { - my $err = $@; + my $song_count; - $db_generation--; + foreach my $entry (@$rows) { + next unless exists $entry->{file}; + $self->db_store_song( $entry->{file}, + $entry->{Artist}, $entry->{Album} ); + $song_count++; + } - $db->rollback; + $log->info("Updated data about $song_count songs"); - die $err; - } - } - ); + $self->db_remove_stale_entries; + + $self->db_set_option( generation => $db_generation ); + + $db->commit; + + $db_needs_update = 0; + } + catch { + my $err = $@; + + $db_generation--; + + $db->rollback; + + die $err; + } } method db_find_suitable_songs($num) { + $self->connect_db; + $self->update_db; + my @result; - my $sth = $self->db->prepare_cached(<prepare_cached(<execute( - $self->opt->min_song_interval / 3600.0 / 24.0, - $self->opt->min_artist_interval / 3600.0 / 24.0, - $self->opt->min_album_interval / 3600.0 / 24.0, + $opt->min_song_interval, + $opt->min_artist_interval, + $opt->min_album_interval, $num, ); while ( my @row = $sth->fetchrow_array ) { @@ -303,57 +312,132 @@ SQL return @result; } - method queue_songs($num = undef) { + method queue_songs($num = undef, $callback = undef) { if (!defined $num) { - $mpd->send('playlist')->on_done( sub { - my $present = scalar @_; + $self->connect_mpd; + $mpd->send('playlist')->on_done( + sub { + my $present = scalar @{ $_[0] }; + + $log->notice("Playlist contains $present songs"); + if ( $present < $opt->target_queue_length ) { + $self->queue_songs( + $opt->target_queue_length - $present, $callback ); + } + else { + $callback->() if $callback; + } + } + ); - $self->queue_songs( $opt->target_queue_length - $present ) - if $present < $opt->target_queue_length; - } ); + return; } - else { - my @list = $self->db_find_suitable_songs($num); - - if (@list < $num) { - $mpd->loop->add( - IO::Async::Timer::Countdown->new( - delay => 15, - on_expire => sub { $self->queue_songs($num) }, - ) - ); - } - else { - $mpd->send( [ map {"add $_->{song}"} @list ] ); + + my @list = $self->db_find_suitable_songs($num); + + die "Found no suitable songs" unless @list; + + if ( @list < $num ) { + $log->warn( + sprintf( + 'Found only %d suitable songs instead of %d', + scalar(@list), $num + ) + ); + } + + $log->info("About to add $num songs to the playlist"); + + my @paths; + for my $song (@list) { + my $path = $song->{song}; + $path =~ s/"/\\"/g; + push @paths, $path; + } + + $log->debug( "Adding " . join( ', ', map {"«$_»"} @paths ) ); + my @commands; + for (@paths) { + push @commands, [ add => "\"$_\"" ]; + } + $self->connect_mpd; + my $f = $mpd->send( \@commands ); + $f->on_fail( sub { die @_ } ); + $f->on_done( + sub { + warn $_ for @_; $self->db_note_song_qeued($_) for @list; + $callback->(@_) if $callback; } - } + ); } -} -my $feeder = Feeder->new(); + method prepare_to_wait_idle { + $log->trace('declaring idle mode'); + $mpd->send('idle database playlist')->on_done( + sub { + warn $_ for @_; + my $result = shift; -$feeder->opt->dump, exit if $feeder->opt->dump_config; + if ( $result->{changed} eq 'database' ) { + $db_needs_update = 1; + $self->prepare_to_wait_idle; + } + elsif ( $result->{changed} eq 'playlist' ) { + $self->queue_songs( undef, + sub { $self->prepare_to_wait_idle } ); + } + else { + use JSON; + $log->warn( + "Unknown result from idle: " . to_json($result) ); + $self->prepare_to_wait_idle; + } + } + ); + } + + method run { + $mpd->on( + close => sub { + die "Connection to MPD lost"; + } + ); -$feeder->queue_songs(1), exit if $feeder->opt->single; + $self->prepare_to_wait_idle; + } +} -# FIXME: handle blacklist manipulation +my $feeder = Feeder->new(); + +sub usage { + die "Usage: mpd-feeder [option...] [command]\n"; +} -$feeder->queue_songs; +if (@ARGV) { + usage if @ARGV > 1; -exit if $feeder->opt->one_shot; + my $cmd = shift @ARGV; -$feeder->mpd->on( - database => sub { - $feeder->update_db; + if ($cmd eq 'dump-config') { + $feeder->opt->dump; + exit; } -); +# FIXME: handle blacklist manipulation -$feeder->mpd->on( - playlist => sub { - $feeder->queue_songs; + if ( $cmd eq 'one-shot' ) { + $feeder->queue_songs(undef, sub { exit }); + $feeder->mpd->loop->run; } -); + elsif ( $cmd eq 'single' ) { + $feeder->queue_songs(1, sub { exit }); + $feeder->mpd->loop->run; + } + else { + die "Unknown command '$cmd'"; + } +} + +$feeder->queue_songs( undef, sub { $feeder->run } ); -$feeder->mpd->idle(qw(database playlist)); -$feeder->mpd->get; +$feeder->mpd->loop->run;