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 )