X-Git-Url: https://git.ktnx.net/?a=blobdiff_plain;f=bin%2Fmpd-feeder;h=29a6d5b10f6247f2bf6e9ee54e65ae96f8e16605;hb=7bcfd7b37974fc1ce0fa6020117a6320f8451bfc;hp=6cc9c196dcf02d7cee1488e367251237cef97139;hpb=7cef318a488aea4d245725b4c47c4474a306aaae;p=mpd-feeder.git diff --git a/bin/mpd-feeder b/bin/mpd-feeder index 6cc9c19..29a6d5b 100755 --- a/bin/mpd-feeder +++ b/bin/mpd-feeder @@ -1,358 +1,26 @@ #!/usr/bin/perl -use v5.32; - -use Getopt::Long (); -use Object::Pad; -use Syntax::Keyword::Try; - -class Options { - use Time::Duration qw(duration_exact); - use Time::Duration::Parse qw(parse_duration); - has $target_queue_length :reader = 10; - has $mpd_host :reader = undef; - has $mpd_port :reader = undef; - has $db_path :reader = 'mpd-feeder'; - has $db_user :reader = undef; - has $db_password :reader = undef; - 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"; - } - - method parse_command_line { - Getopt::Long::GetOptions( - 'v|verbose+' => \$verbose, - 'dump-config!' => \$dump_config, - 's|single!' => \$single, - 'one-shot!' => \$one_shot, - '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 = parse_duration(pop); - }, - 'min-sing-interval=s' => sub { - $min_song_interval = parse_duration(pop); - }, - 'min-artist-interval=s' => sub { - $min_artist_interval = parse_duration(pop); - }, - ) or exit 1; - } - - sub handle_config_option( $ini, $section, $option, $target_ref, - $converter = undef ) - { - return undef unless exists $ini->{$section}{$option}; - - my $value = $ini->{$section}{$option}; - - $value = $converter->($value) if $converter; - - $$target_ref = $value; - } - - method dump { - say "[mpd-feeder]"; - say "verbose = $verbose"; - say ""; - say "[mpd]"; - say "host = " . ( $mpd_host // '' ); - say "port = " . ( $mpd_port // '' ); - say "target-queue-length = $target_queue_length"; - say ""; - say "[queue]"; - say "target-length = $target_queue_length"; - say "min-song-interval = " . duration_exact($min_song_interval); - say "min-album-interval = " . duration_exact($min_album_interval); - say "min-artist-interval = " . duration_exact($min_artist_interval); - say ""; - say "[db]"; - say "path = " . ( $db_path // '' ); - say "user = " . ( $db_user // '' ); - say "password = " . ( $db_password // '' ); - } - - method parse_config_file($path) { - use Config::INI::Reader; - my $ini = Config::INI::Reader->read_file($path); - - 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 => queue => 'target-length' => \$target_queue_length ); - handle_config_option( - $ini => queue => 'min-song-interval' => \$min_song_interval, - \&parse_duration - ); - handle_config_option( - $ini => queue => 'min-album-interval' => \$min_album_interval, - \&parse_duration - ); - handle_config_option( - $ini => queue => 'min-artist-interval' => \$min_artist_interval, - \&parse_duration - ); - - handle_config_option( $ini => db => path => \$db_path ); - handle_config_option( $ini => db => user => \$db_user ); - handle_config_option( $ini => db => password => \$db_password ); - - # FIXME: complain about unknown sections/parameters - } +use strict; +use warnings; +use utf8::all; + +use App::MPD::Feeder; +use Log::Any qw($log); +use Log::Any::Adapter Stderr => log_level => 'error'; + +{ # autoflush without IO::Handle + my $fh = select STDERR; + $| = 1; + select $fh; } -class Feeder { - has $opt :reader; - has $db; - has $db_generation; - has $mpd; - -use constant DEFAULT_CONFIG_FILE => '/etc/mpd-feeder/mpd-feeder.conf'; - - ADJUST { - $opt = Options->new; - - { - my $cfg_file; - Getopt::Long::Configure('pass_through'); - Getopt::Long::GetOptions('cfg|config=s' => \$cfg_file); - Getopt::Long::Configure('no_pass_through'); - - $cfg_file //= DEFAULT_CONFIG_FILE if -e DEFAULT_CONFIG_FILE; - - $opt->parse_config_file($cfg_file) if $cfg_file; - } - - $opt->parse_command_line; - - unless ($opt->dump_config) { - $mpd = Net::Async::MPD->new( - host => $opt->mpd_host, - port => $opt->mpd_port, - auto_connect => 1, - ); - - $self->connect_db; - $self->update_db; - } - } - - method connect_db { - return if $db; - - $db = DBI->connect( "dbi:" . $opt->db_path, - $opt->db_user, $opt->db_password, - { RaiseError => 1, AutoCommit => 1 } ); - - $db_generation = $self->db_get_option('generation'); - } - - method db_get_option($name) { - my $sth = $db->prepare_cached("select $name from options"); - $sth->execute; - my @result = $sth->fetchrow_array; - - return $result[0]; - } +my $feeder = App::MPD::Feeder->new(); - method db_set_option( $name, $value ) { - my $sth = $db->prepare_cached("update options set $name = ?"); - $sth->execute($value); - } +if (@ARGV) { + require App::MPD::Feeder::Command; + bless $feeder, 'App::MPD::Feeder::Command'; - method db_store_song($song, $artist, $album) { - return unless length($song) and length($artist) and length($album); - - $db->prepare_cached( - <<'SQL')->execute( $song, $artist, $album, $db_generation ); -INSERT INTO songs(path, artist, album, generation) -VALUES($1, $2, $3, $3) -ON CONFLICT ON CONSTRAINT songs_pkey DO -UPDATE SET artist = $2 - , album = $3 - , generation = $4 -SQL - $db->prepare_cached(<<'SQL')->execute( $artist, $album, $db_generation ); -INSERT INTO albums(artist, album, generation) -VALUES($1, $2, $3) -ON CONFLICT ON CONSTRAINT albums_pkey DO -UPDATE SET generation = $3 -SQL - $db->prepare_cached(<<'SQL')->execute( $artist, $db_generation ); -INSERT INTO artists(artist, generation) -VALUES($1, $2) -ON CONFLICT ON CONSTRAINT artists_pkey DO -UPDATE SET generation = $2 -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); - } - - method db_note_song_qeued($item) { - $db->prepare_cached( - 'UPDATE songs SET last_queued=current_timestamp WHERE path=?') - ->execute( $item->{song} ); - $db->prepare_cached( - 'UPDATE artists SET last_queued=CURRENT_TIMESTAMP WHERE artist=?') - ->execute( $item->{artist} ); - $db->prepare_cached( - 'UPDATE albums SET last_queued=CURRENT_TIMESTAMP WHERE artist=? AND album=?' - )->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; - } - } - - $self->db_store_song($song, $artist, $album); - - $self->db_remove_stale_entries; - - $self->db_set_option( generation => $db_generation ); - - $db->commit; - } - catch { - my $err = $@; - - $db_generation--; - - $db->rollback; - - die $err; - } - } - ); - } - - method db_find_suitable_songs($num) { - my @result; - my $sth = $self->db->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, - $num, - ); - while ( my @row = $sth->fetchrow_array ) { - push @result, - { song => $row[0], artist => $row[1], album => $row[2] }; - } - - return @result; - } - - method queue_songs($num = undef) { - if (!defined $num) { - $mpd->send('playlist')->on_done( sub { - my $present = scalar @_; - - $self->queue_songs( $opt->target_queue_length - $present ) - if $present < $opt->target_queue_length; - } ); - } - 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 ] ); - $self->db_note_song_qeued($_) for @list; - } - } - } + exit $feeder->run(@ARGV); } -my $feeder = Feeder->new(); - -$feeder->opt->dump, exit if $feeder->opt->dump_config; - -$feeder->queue_songs(1), exit if $feeder->opt->single; - -# FIXME: handle blacklist manipulation - -$feeder->queue_songs; - -exit if $feeder->opt->one_shot; - -$feeder->mpd->on( - database => sub { - $feeder->update_db; - } -); - -$feeder->mpd->on( - playlist => sub { - $feeder->queue_songs; - } -); - -$feeder->mpd->idle(qw(database playlist)); -$feeder->mpd->get; +$feeder->run_loop;