App-DrivePlayer
view release on metacpan or search on metacpan
lib/App/DrivePlayer/GUI/MetadataFetch.pm view on Meta::CPAN
package App::DrivePlayer::GUI::MetadataFetch;
# Moo role: background metadata-fetch machinery.
use strict;
use warnings;
use utf8;
use Moo::Role;
use Glib qw( TRUE FALSE );
use Gtk3 '-init';
use JSON::MaybeXS qw( encode_json decode_json );
use POSIX qw( WNOHANG );
use App::DrivePlayer::MetadataFetcher;
my $log = do { eval { require Log::Log4perl; Log::Log4perl->get_logger(__PACKAGE__) } };
has _meta_watch_id => ( is => 'rw', default => sub { undef } );
has _meta_pid => ( is => 'rw', default => sub { undef } );
has _meta_reader => ( is => 'rw', default => sub { undef } );
has _meta_buf => ( is => 'rw', default => sub { q{} } );
has _meta_fetch_item => ( is => 'rw' );
sub _toggle_metadata_fetch {
my ($self) = @_;
if ($self->_meta_watch_id) {
$self->_stop_metadata_fetch();
} else {
$self->_fetch_all_metadata();
}
return;
}
sub _apply_meta_result {
my ($self, $msg) = @_;
my $track = $msg->{track};
my %upd;
if (my $meta = $msg->{meta}) {
# Embedded tags are authoritative â overwrite folder-inferred values.
# Text search / fingerprint only fill in fields that are missing.
my $trust_tags = ($msg->{source} // '') =~ /embedded tags/;
for my $key (qw( artist album year genre comment track_number )) {
next if !$trust_tags && $track->{$key} && length $track->{$key};
$upd{$key} = $meta->{$key} if $meta->{$key};
}
}
if ($msg->{duration_ms} && !$track->{duration_ms}) {
$upd{duration_ms} = $msg->{duration_ms};
}
my $title = $track->{title} // $msg->{track_id};
my @meta_fields = grep { $_ ne 'duration_ms' } sort keys %upd;
if (%upd) {
my $detail = "source=$msg->{source}";
$detail .= ' fields=' . join(',', @meta_fields) if @meta_fields;
$detail .= " duration=$msg->{dur_source}" if $msg->{dur_source};
$log->info("Metadata [$title]: $detail") if $log;
$self->db->update_track_metadata($msg->{track_id}, %upd);
$self->db->mark_metadata_fetched($msg->{track_id});
$self->_refresh_track_row($msg->{track_id});
return 1;
}
$log->info("Metadata [$title]: source=$msg->{source} â no new fields") if $log;
lib/App/DrivePlayer/GUI/MetadataFetch.pm view on Meta::CPAN
# ---- parent: reads results without blocking ----
close $writer;
$self->_meta_pid($pid);
$self->_meta_reader($reader);
$self->_meta_buf(q{});
my $updated = 0;
my $finish = sub {
if (my $wid = $self->_meta_watch_id) {
$self->_meta_watch_id(undef);
Glib::Source->remove($wid);
}
$self->_meta_pid(undef);
$self->_meta_fetch_item->set_label('Fetch All Metadata');
close $reader;
$self->_meta_reader(undef);
waitpid($pid, 0);
};
my $process_msg = sub {
my ($msg) = @_;
if ($msg->{status}) {
$self->_set_status(
"$msg->{status} $msg->{n}/$msg->{total} ($updated updated): $msg->{title}"
);
}
elsif ($msg->{result}) {
$updated += $self->_apply_meta_result($msg);
}
return;
};
my $watch_id = Glib::IO->add_watch(fileno($reader), ['in', 'hup'], sub {
my (undef, $cond) = @_;
my $chunk = q{};
my $bytes = sysread($reader, $chunk, 65536);
if (!defined $bytes || $bytes == 0) {
$finish->();
$self->_set_status("Metadata fetch done â $updated of $total updated.");
$self->_load_library();
$self->_auto_sync_to_sheet() if $updated;
return FALSE;
}
my $buf = $self->_meta_buf . $chunk;
while ($buf =~ s/\A([^\n]+)\n//) {
my $msg = eval { decode_json($1) } or next;
$process_msg->($msg);
}
$self->_meta_buf($buf);
return TRUE;
});
$self->_meta_watch_id($watch_id);
$self->_meta_fetch_item->set_label('Stop Metadata Fetch');
$self->_set_status("Fetching metadata for $total tracks in backgroundâ¦");
return;
}
sub _stop_metadata_fetch {
my ($self) = @_;
return unless $self->_meta_watch_id;
Glib::Source->remove($self->_meta_watch_id);
$self->_meta_watch_id(undef);
$self->_meta_fetch_item->set_label('Fetch All Metadata');
if (my $pid = $self->_meta_pid) {
kill 'TERM', $pid;
waitpid($pid, 0);
$self->_meta_pid(undef);
}
# Drain any result messages the child had already written before dying
if (my $reader = $self->_meta_reader) {
my $buf = $self->_meta_buf;
my $chunk = q{};
while (sysread($reader, $chunk, 65536)) {
$buf .= $chunk;
}
while ($buf =~ s/\A([^\n]+)\n//) {
my $msg = eval { decode_json($1) } or next;
next unless $msg->{result};
$self->_apply_meta_result($msg);
}
close $reader;
$self->_meta_reader(undef);
$self->_meta_buf(q{});
}
$self->_set_status('Metadata fetch stopped. Progress saved â will resume here next time.');
return;
}
sub _reset_metadata_fetch {
my ($self) = @_;
if ($self->_meta_watch_id) {
$self->_show_error('Cannot reset while a fetch is in progress.');
return;
}
$self->db->reset_metadata_fetched();
$self->_set_status('Metadata fetch progress reset â all tracks will be retried.');
return;
}
sub _retry_incomplete_metadata {
my ($self) = @_;
if ($self->_meta_watch_id) {
$self->_show_error('Cannot reset while a fetch is in progress.');
return;
}
$self->db->reset_metadata_fetched_incomplete();
$self->_fetch_all_metadata();
return;
}
sub _lookup_metadata {
my ($self, $track) = @_;
my $yield = sub { Gtk3::main_iteration_do(FALSE) while Gtk3::events_pending() };
lib/App/DrivePlayer/GUI/MetadataFetch.pm view on Meta::CPAN
my $fetcher = App::DrivePlayer::MetadataFetcher->new(
yield => $yield,
acoustid_key => $self->config->acoustid_key(),
token_fn => sub { $self->_bearer_token() },
);
my $meta = $fetcher->fetch(
title => $track->{title},
artist => $track->{artist},
album => $track->{album},
);
unless ($meta) {
my $key = $self->config->acoustid_key();
my $have_fp = App::DrivePlayer::MetadataFetcher::fpcalc_available();
if (!$key) {
$self->_set_status('Text search: no match. Fingerprinting skipped: no AcoustID key set.');
return;
}
if (!$have_fp) {
$self->_set_status('Text search: no match. Fingerprinting skipped: fpcalc not installed.');
return;
}
unless ($self->_init_api()) {
$self->_set_status('Text search: no match. Fingerprinting skipped: Google API not initialised.');
return;
}
$self->_set_status('Text search: no match. Downloading audio for fingerprintingâ¦');
$yield->();
my $err;
$meta = eval { $fetcher->fetch_by_fingerprint(drive_id => $track->{drive_id}) };
$err = $@ if $@;
unless ($meta) {
my $reason = $err ? "error: $err"
: $fetcher->last_fp_stage() // 'no match found';
$self->_set_status("Fingerprint lookup: $reason");
return;
}
}
if ($log) {
my $dump = join ' | ',
map { sprintf('%s=%s', $_, $meta->{$_} // '(undef)') }
sort keys %$meta;
$log->debug("Single fetch: result: $dump");
}
$self->_set_status('Metadata found.');
return $meta;
}
1;
__END__
=head1 NAME
App::DrivePlayer::GUI::MetadataFetch - Role for background metadata fetching
=head1 DESCRIPTION
A L<Moo::Role> consumed by L<App::DrivePlayer::GUI> that handles background
metadata fetching via a forked child process.
=cut
( run in 0.504 second using v1.01-cache-2.11-cpan-fe3c2283af0 )