App-DrivePlayer

 view release on metacpan or  search on metacpan

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

package App::DrivePlayer::Config;

use App::DrivePlayer::Setup;
use File::Basename  qw( dirname );
use File::Path      qw( make_path );
use YAML::XS        qw( LoadFile DumpFile );

my $DEFAULT_CONFIG_DIR  = "$ENV{HOME}/.config/drive_player";
my $DEFAULT_CONFIG_FILE = "$DEFAULT_CONFIG_DIR/config.yaml";
my $DEFAULT_DATA_DIR    = "$ENV{HOME}/.local/share/drive_player";
my $DEFAULT_DB_PATH     = "$DEFAULT_DATA_DIR/music.db";
my $DEFAULT_LOG_FILE    = "$DEFAULT_DATA_DIR/drive_player.log";

has config_file => (
    is      => 'ro',
    isa     => Str,
    default => sub { $DEFAULT_CONFIG_FILE },
);

# Internal: the parsed YAML data hash
has _data => (
    is      => 'lazy',
    isa     => HashRef,
    builder => '_build_data',
);

sub _build_data {
    my ($self) = @_;
    my $data = $self->_defaults();
    if (-f $self->config_file) {
        my $file = LoadFile($self->config_file);
        # Migrate legacy root-level auth key into google_restapi.auth
        if ($file->{auth} && !($file->{google_restapi} && $file->{google_restapi}{auth})) {
            $file->{google_restapi} //= {};
            $file->{google_restapi}{auth} = delete $file->{auth};
        }
        _merge($data, $file);
    }
    _expand_paths($data, dirname($self->config_file));
    return $data;
}

# Recursively merge $src over $dst (scalar/array values in $src win).
sub _merge {
    my ($dst, $src) = @_;
    for my $key (keys %{ $src }) {
        if (ref $src->{$key} eq 'HASH' && ref $dst->{$key} eq 'HASH') {
            _merge($dst->{$key}, $src->{$key});
        } else {
            $dst->{$key} = $src->{$key};
        }
    }
}

sub _defaults {
    return {
        google_restapi => {
            class => 'OAuth2Client',
            auth  => {
                class         => 'OAuth2Client',
                client_id     => '',
                client_secret => '',
                token_file    => "$DEFAULT_CONFIG_DIR/token.dat",
                scope         => ['https://www.googleapis.com/auth/drive.readonly'],
            },
        },
        music_folders => [],
        database      => { path => $DEFAULT_DB_PATH },
        log_level     => 'WARN',
        log_file      => $DEFAULT_LOG_FILE,
        acoustid_key  => '',
        sheet_id      => '',
    };
}

sub _expand_paths {
    my ($data, $config_dir) = @_;
    for my $key (qw( log_file )) {
        $data->{$key} = _abs_path($data->{$key}, $config_dir) if defined $data->{$key};
    }
    $data->{database}{path} = _abs_path($data->{database}{path}, $config_dir)
        if defined $data->{database}{path};

    # Support auth under google_restapi.auth (preferred) or legacy root auth key
    my $auth = $data->{google_restapi}{auth} // $data->{auth};
    if ($auth && defined $auth->{token_file}) {
        $auth->{token_file} = _abs_path($auth->{token_file}, $config_dir);
    }
}

sub _abs_path {
    my ($path, $config_dir) = @_;
    $path =~ s|^~|$ENV{HOME}|;
    return File::Spec->rel2abs($path, $config_dir) unless File::Spec->file_name_is_absolute($path);
    return $path;
}

sub save {
    my ($self) = @_;
    my $dir = dirname($self->config_file);
    make_path($dir) unless -d $dir;
    DumpFile($self->config_file, $self->_data);
}

sub ensure_dirs {
    my ($self) = @_;
    for my $path ($self->db_path, $self->log_file, $self->token_file) {
        next unless defined $path && $path ne '';
        my $dir = dirname($path);
        make_path($dir) unless -d $dir;
    }
}

# Auth config hashref suitable for Google::RestApi->new(auth => ...)
# Prefers google_restapi.auth; falls back to legacy root-level auth key.
sub auth_config {
    my ($self) = @_;
    return $self->_data->{google_restapi}{auth} // $self->_data->{auth} // {};
}

# Full google_restapi config block for Google::RestApi->new(google_restapi => ...)
sub google_restapi_config { $_[0]->_data->{google_restapi} }

# Music folders: arrayref of { id => '...', name => '...' }
sub music_folders {
    my ($self, $folders) = @_;
    $self->_data->{music_folders} = $folders if defined $folders;
    return $self->_data->{music_folders} // [];
}

sub add_music_folder {
    my ($self, $id, $name) = @_;
    return if grep { $_->{id} eq $id } @{ $self->_data->{music_folders} };
    push @{ $self->_data->{music_folders} }, { id => $id, name => $name };
}

sub remove_music_folder {
    my ($self, $id) = @_;
    $self->_data->{music_folders} = [
        grep { $_->{id} ne $id } @{ $self->_data->{music_folders} }
    ];
}

sub db_path      { $_[0]->_data->{database}{path} }
sub log_level    { $_[0]->_data->{log_level} // 'WARN' }
sub log_file     { $_[0]->_data->{log_file} }
sub token_file   { $_[0]->auth_config->{token_file} }
sub acoustid_key { $_[0]->_data->{acoustid_key} // '' }
sub sheet_id     { $_[0]->_data->{sheet_id}     // '' }

1;

__END__

=head1 NAME

App::DrivePlayer::Config - Load, persist and query DrivePlayer configuration

=head1 SYNOPSIS

  use App::DrivePlayer::Config;

  my $cfg = App::DrivePlayer::Config->new();                      # default path
  my $cfg = App::DrivePlayer::Config->new(config_file => $path);  # explicit path

  # Read settings
  my $auth    = $cfg->auth_config;     # hashref for Google::RestApi->new
  my @folders = @{ $cfg->music_folders };

  # Manage music folders
  $cfg->add_music_folder($drive_id, 'My Music');
  $cfg->remove_music_folder($drive_id);

  $cfg->save;          # write changes back to disk
  $cfg->ensure_dirs;   # create parent directories for db, log, token

=head1 DESCRIPTION

Reads a YAML configuration file and provides typed accessors for every
setting.  Missing files are silently replaced by built-in defaults so the
application works out of the box before the user runs the setup wizard.

Tilde (C<~>) at the start of any path value is expanded to C<$HOME>.

=head1 ATTRIBUTES

=head2 config_file

  is: ro, isa: Str

Path to the YAML configuration file.  Defaults to
F<~/.config/drive_player/config.yaml>.

=head1 METHODS

=head2 new

  my $cfg = App::DrivePlayer::Config->new(%args);

Constructor.  Accepts C<config_file> as an optional named argument.

=head2 auth_config

  my $hashref = $cfg->auth_config;

Returns the C<auth> stanza from the config file as a plain hashref.  Pass
this directly to C<< Google::RestApi->new(auth => ...) >>.

=head2 music_folders

  my $aref   = $cfg->music_folders;
  $cfg->music_folders(\@folders);   # replace all

Getter/setter for the list of configured music folders.  Each element is a
hashref with C<id> (Google Drive folder ID) and C<name> keys.

=head2 add_music_folder

  $cfg->add_music_folder($drive_id, $name);

Appends a new folder to the music folder list.

=head2 remove_music_folder

  $cfg->remove_music_folder($drive_id);

Removes the folder with the given Drive ID from the list.

=head2 db_path

  my $path = $cfg->db_path;

Absolute path to the SQLite database file.

=head2 log_level

  my $level = $cfg->log_level;   # e.g. 'WARN', 'DEBUG'

Log4perl log level string.  Defaults to C<WARN>.

=head2 log_file

  my $path = $cfg->log_file;



( run in 1.853 second using v1.01-cache-2.11-cpan-5b529ec07f3 )