App-DrivePlayer

 view release on metacpan or  search on metacpan

lib/App/DrivePlayer/GUI.pm  view on Meta::CPAN


has _playlist        => ( is => 'rw', isa => ArrayRef, default => sub { [] } );
has _track_by_id     => ( is => 'rw', default => sub { {} } );
has _playing_row_ref => ( is => 'rw', default => sub { undef } );
has _playing_track_id => ( is => 'rw', default => sub { undef } );
has _progress_dragging => ( is => 'rw', isa => Bool, default => 0 );

# Widget accessors — set during _build_ui
has win                => ( is => 'rw' );
has sidebar_store      => ( is => 'rw' );
has sidebar_view       => ( is => 'rw' );
has alpha_view         => ( is => 'rw' );
has _alpha_category    => ( is => 'rw', default => sub { 'Artists' } );
has track_store        => ( is => 'rw' );
has _track_iter_map    => ( is => 'rw', default => sub { {} } );
has track_view         => ( is => 'rw' );
has track_count_label  => ( is => 'rw' );
has now_playing_label  => ( is => 'rw' );
has progress           => ( is => 'rw' );
has time_label         => ( is => 'rw' );
has dur_label          => ( is => 'rw' );
has play_btn           => ( is => 'rw' );
has prev_btn           => ( is => 'rw' );
has stop_btn           => ( is => 'rw' );
has next_btn           => ( is => 'rw' );
has vol_scale          => ( is => 'rw' );
has search_entry       => ( is => 'rw' );
has statusbar          => ( is => 'rw' );
has _status_ctx        => ( is => 'rw' );
with qw(
    App::DrivePlayer::GUI::MetadataFetch
    App::DrivePlayer::GUI::SheetSync
    App::DrivePlayer::GUI::FolderBrowse
);

sub _bearer_token {
    my ($self) = @_;
    return unless $self->rest_api;
    my %h = @{ $self->rest_api->auth->headers() };
    return $h{Authorization};
}

sub _build_db {
    my ($self) = @_;
    return App::DrivePlayer::DB->new(path => $self->config->db_path());
}

sub BUILD {
    my ($self) = @_;
    $self->_init_logging();
}

sub run {
    my ($self) = @_;
    my $db_is_new = !-f $self->config->db_path();
    $self->_build_ui();
    $self->_auto_sync_from_sheet_on_new_db() if $db_is_new;
    $self->_prune_removed_folders();
    $self->_load_library();

    Glib::Timeout->add($POLL_INTERVAL_MS, sub {
        $self->_player_poll();
        return TRUE;
    });

    Gtk3->main();
    $self->player->quit() if $self->player;
}

# ---- Initialisation ----

sub _init_logging {
    my ($self) = @_;
    $self->config->ensure_dirs();
    my $level = $self->config->log_level();
    my $file  = $self->config->log_file() // '/tmp/drive_player.log';

    my $log4perl_conf = "
        log4perl.rootLogger=$level, Screen, File
        log4perl.appender.Screen=Log::Log4perl::Appender::Screen
        log4perl.appender.Screen.layout=Log::Log4perl::Layout::PatternLayout
        log4perl.appender.Screen.layout.ConversionPattern=%d [%p] %m%n
        log4perl.appender.File=Log::Log4perl::Appender::File
        log4perl.appender.File.filename=$file
        log4perl.appender.File.utf8=1
        log4perl.appender.File.layout=Log::Log4perl::Layout::PatternLayout
        log4perl.appender.File.layout.ConversionPattern=%d [%p] %m%n
    ";
    if (eval { require Log::Log4perl; 1 }) {
        Log::Log4perl->init(\$log4perl_conf);
        binmode STDERR, ':encoding(UTF-8)';
    }
}

sub _init_api {
    my ($self) = @_;
    return $self->rest_api if $self->rest_api;

    my $auth_cfg = $self->config->auth_config();
    unless ($auth_cfg->{client_id} && $auth_cfg->{client_secret}) {
        $self->_show_error(
            "Google API credentials not configured.\n\n" .
            "Open File > Settings and enter your OAuth Client ID and Secret.\n\n" .
            "You can obtain these from the Google Cloud Console under\n" .
            "APIs & Services > Credentials (OAuth 2.0 Client ID, Desktop app type)."
        );
        return;
    }
    unless (-f ($auth_cfg->{token_file} // '')) {
        $self->_show_error("OAuth token file not found: $auth_cfg->{token_file}\n\n" .
            "Run the token creator from p5-google-restapi:\n" .
            "  bin/google_restapi_oauth_token_creator");
        return;
    }

    my $api = eval { Google::RestApi->new(auth => $auth_cfg) };
    if ($@) {
        $self->_show_error("Failed to initialise Google API: $@");
        return;
    }
    $self->rest_api($api);

lib/App/DrivePlayer/GUI.pm  view on Meta::CPAN

    $fp_frame->set_border_width(8);
    my $fp_grid = Gtk3::Grid->new();
    $fp_grid->set_row_spacing(8);
    $fp_grid->set_column_spacing(8);
    $fp_grid->set_border_width(8);
    $fp_frame->add($fp_grid);
    $vbox->pack_start($fp_frame, FALSE, FALSE, 0);

    # fpcalc status row
    my $fp_lbl = Gtk3::Label->new('fpcalc:');
    $fp_lbl->set_xalign(1.0);
    $fp_grid->attach($fp_lbl, 0, 0, 1, 1);

    my $fp_status = Gtk3::Label->new();
    $fp_status->set_xalign(0.0);

    my $install_btn = Gtk3::Button->new_with_label('Install…');
    $install_btn->set_tooltip_text(
        'Installs libchromaprint-tools via apt (requires administrator password)'
    );

    my $fp_hbox = Gtk3::Box->new('horizontal', 8);
    $fp_hbox->pack_start($fp_status,    FALSE, FALSE, 0);
    $fp_hbox->pack_start($install_btn,  FALSE, FALSE, 0);
    $fp_grid->attach($fp_hbox, 1, 0, 1, 1);

    # Helper: refresh the fpcalc status label
    my $refresh_fp_status = sub {
        if (App::DrivePlayer::MetadataFetcher::fpcalc_available()) {
            $fp_status->set_markup('<span foreground="#2d862d"><b>Installed</b></span>');
            $install_btn->hide();
        }
        else {
            $fp_status->set_markup(
                '<span foreground="#cc0000">Not installed</span>'
                . '  <span size="small" foreground="#666666">'
                . '(needed for fingerprint-based lookup)</span>'
            );
            $install_btn->show();
        }
    };
    $refresh_fp_status->();

    $install_btn->signal_connect(clicked => sub {
        $install_btn->set_sensitive(FALSE);
        $fp_status->set_markup('<span foreground="#666666">Installing…</span>');
        Gtk3::main_iteration_do(FALSE) while Gtk3::events_pending();

        my $pid = fork();
        if (!defined $pid) {
            $fp_status->set_markup('<span foreground="#cc0000">Fork failed</span>');
            $install_btn->set_sensitive(TRUE);
            return;
        }
        if ($pid == 0) {
            exec('pkexec', 'apt-get', 'install', '-y', 'libchromaprint-tools')
                or POSIX::_exit(1);
        }

        # Poll every 500 ms until the child exits
        Glib::Timeout->add(500, sub {
            my $res = waitpid($pid, WNOHANG());
            if ($res == $pid) {
                $refresh_fp_status->();
                $install_btn->set_sensitive(TRUE);
                return FALSE;   # remove timer
            }
            return TRUE;        # keep polling
        });
    });

    # AcoustID API key
    my $aid_lbl = Gtk3::Label->new('AcoustID API Key:');
    $aid_lbl->set_xalign(1.0);
    $fp_grid->attach($aid_lbl, 0, 1, 1, 1);

    my $aid_entry = Gtk3::Entry->new();
    $aid_entry->set_hexpand(TRUE);
    $aid_entry->set_text($self->config->acoustid_key());
    $aid_entry->set_placeholder_text('Get a free key at acoustid.org');
    $aid_entry->set_tooltip_text(
        'Free AcoustID API key — used with fpcalc to look up missing tags '
        . 'by acoustic fingerprint.'
    );
    $aid_lbl->set_tooltip_text($aid_entry->get_tooltip_text());
    $fp_grid->attach($aid_entry, 1, 1, 1, 1);

    $fp_lbl->set_tooltip_text(
        'fpcalc (from chromaprint) generates audio fingerprints for '
        . 'acoustic-ID lookup.  Install it via apt if missing.'
    );

    my $aid_note = Gtk3::Label->new();
    $aid_note->set_markup(
        '<span size="small" foreground="#555555">'
        . 'Register a free application at '
        . '<a href="https://acoustid.org/new-application">acoustid.org</a> '
        . 'to obtain a key.  '
        . '<a href="https://acoustid.org/">More info</a>.'
        . '</span>'
    );
    $aid_note->set_xalign(0.0);
    $aid_note->set_line_wrap(TRUE);
    $aid_note->set_max_width_chars(60);
    $fp_grid->attach($aid_note, 1, 2, 1, 1);

    # ---- Config file path (informational) ----
    my $info_grid = Gtk3::Grid->new();
    $info_grid->set_row_spacing(4);
    $info_grid->set_column_spacing(8);
    $info_grid->set_border_width(8);
    $vbox->pack_start($info_grid, FALSE, FALSE, 0);

    my $cfg_key = Gtk3::Label->new('Config file:');
    $cfg_key->set_xalign(1.0);
    $info_grid->attach($cfg_key, 0, 0, 1, 1);
    my $cfg_lbl = Gtk3::Label->new($self->config->config_file());
    $cfg_lbl->set_xalign(0.0);
    $cfg_lbl->set_selectable(TRUE);
    $info_grid->attach($cfg_lbl, 1, 0, 1, 1);



( run in 1.688 second using v1.01-cache-2.11-cpan-39bf76dae61 )