summaryrefslogtreecommitdiffstats
path: root/clerk
diff options
context:
space:
mode:
authorRasmus Steinke <rasi@xssn.at>2017-08-05 20:49:25 +0200
committerRasmus Steinke <rasi@xssn.at>2017-08-05 20:49:25 +0200
commit6a123c3139255f79947bb53561d995ab2f334d3a (patch)
tree0547e0dd700775f5612c487bf6b90d51ba946a9c /clerk
parentea6d94d4b64245d84b62bd8a2ec6b1085a05a427 (diff)
downloadperl-app-clerk-6a123c3139255f79947bb53561d995ab2f334d3a.tar.gz
perl-app-clerk-6a123c3139255f79947bb53561d995ab2f334d3a.tar.xz
add executable
Diffstat (limited to 'clerk')
-rwxr-xr-xclerk278
1 files changed, 278 insertions, 0 deletions
diff --git a/clerk b/clerk
new file mode 100755
index 0000000..4aee075
--- /dev/null
+++ b/clerk
@@ -0,0 +1,278 @@
+#!/usr/bin/env perl
+
+binmode(STDOUT, ":utf8");
+use v5.10;
+use warnings;
+use strict;
+use utf8;
+use Config::Simple;
+use DDP;
+use Data::Dumper;
+use Data::MessagePack;
+use File::Basename;
+use File::Path qw(make_path);
+use File::Slurper 'read_binary';
+use File::stat;
+use Getopt::Std;
+use HTTP::Date;
+use IO::Select;
+use IPC::Run qw( timeout start );
+use List::Util qw(any);
+use Net::MPD;
+use autodie;
+
+$ENV{TMUX_TMPDIR}='/tmp/clerk/tmux';
+make_path($ENV{TMUX_TMPDIR}) unless(-d $ENV{TMUX_TMPDIR});
+
+my $config_file = $ENV{'HOME'} . "/.config/clerk/clerk.conf";
+
+if ($ENV{CLERK_CONF}) {
+ $config_file = $ENV{CLERK_CONF};
+}
+
+# read configuration file
+my $cfg = new Config::Simple(filename=>"$config_file");
+
+my $general_cfg = $cfg->param(-block=>"General");
+my $mpd_host = $general_cfg->{mpd_host};
+my $tmux_config = $general_cfg->{tmux_config};
+my $db_file = $general_cfg->{database};
+my $backend = $general_cfg->{backend};
+my $chunksize = $general_cfg->{chunksize};
+
+my $columns_cfg = $cfg->param(-block=>"Columns");
+my $albumartist_l = $columns_cfg->{albumartist_l};
+my $album_l = $columns_cfg->{album_l};
+my $date_l = $columns_cfg->{date_l};
+my $title_l = $columns_cfg->{title_l};
+my $track_l = $columns_cfg->{track_l};
+my $artist_l = $columns_cfg->{artist_l};
+
+print "$tmux_config\n";
+if ($ENV{CLERK_BACKEND}) {
+ $backend = $ENV{CLERK_BACKEND};
+}
+
+# open connection to MPD
+my $mpd = Net::MPD->connect($ENV{MPD_HOST} // $mpd_host // 'localhost');
+
+sub main {
+ create_db();
+ if ($backend eq "fzf") {
+ system('tmux', 'has-session', '-t', 'music');
+ if ($? != -0) {
+ system('tmux', '-f', $tmux_config, 'new-session', '-s', 'music', '-n', 'albums', '-d', 'clerk', '-a');
+ system('tmux', 'new-window', '-t', 'music', '-n', 'tracks', 'clerk', '-t');
+ system('tmux', 'new-window', '-t', 'music', '-n', 'latest', 'clerk', '-l');
+ system('tmux', 'new-window', '-t', 'music', '-n', 'playlists', 'clerk', '-p');
+ system('tmux', 'new-window', '-t', 'music', '-n', 'queue', 'ncmpcpp');
+ }
+ system('tmux', 'attach', '-t', 'music');
+ }
+# elsif ($backend eq "rofi") {
+ my %options=();
+ getopts("talp", \%options);
+
+ if (defined $options{t}) {
+ list_db_entries_for("Tracks");
+ } elsif (defined $options{a}) {
+ list_db_entries_for("Albums");
+ } elsif (defined $options{p}) {
+ list_playlists();
+ } elsif (defined $options{l}) {
+ list_db_entries_for("Latest");
+ }
+}
+
+
+sub create_db {
+ # Get database copy and save as messagepack file, if file is either missing
+ # or older than latest mpd database update.
+ # get number of songs to calculate number of searches needed to copy mpd database
+ my $mpd_stats = $mpd->stats();
+ my $songcount = $mpd_stats->{songs};
+ my $last_update = $mpd_stats->{db_update};
+
+ if (!-f "$db_file" || stat("$db_file")->mtime < $last_update) {
+ print STDERR "::: No cache found or cache file outdated\n";
+ print STDERR "::: Chunksize set to $chunksize songs\n";
+ my $times = int($songcount / $chunksize + 1);
+ print STDERR "::: Requesting $times chunks from MPD\n";
+ my @db;
+ # since mpd will silently fail, if response is larger than command buffer, let's split the search.
+ my $chunk_size = $chunksize;
+ for (my $i=0;$i<=$songcount;$i+=$chunk_size) {
+ my $endnumber = $i+$chunk_size;
+ my @temp_db = $mpd->search('filename', '', 'window', "$i:$endnumber");
+ push @db, @temp_db;
+ }
+
+ # only save relevant tags to keep messagepack file small
+ # note: maybe use a proper database instead? See list_album function.
+ my @filtered = map { $_->{mtime} = str2time($_->{'Last-Modified'}); +{$_->%{qw/Album Artist Date AlbumArtist Title Track uri mtime/}} } @db;
+ pack_msgpack(\@filtered);
+ }
+}
+
+sub backend_call {
+ my ($in, $fields) = @_;
+ my $input;
+ my $out;
+ $fields //= "1,2,3";
+ my %backends = (
+ fzf => [ qw(fzf
+ --reverse
+ --no-sort
+ -m
+ -e
+ -i
+ -d
+ \t
+ --tabstop=4
+ +s
+ --ansi),
+ "--with-nth=$fields"
+ ],
+ rofi => [
+ 'rofi',
+ '-width',
+ '1300',
+ '-dmenu',
+ '-multi-select',
+ '-i',
+ '-p',
+ '> '
+ ]
+ );
+ my $handle = start $backends{$backend} // die('backend not found'), \$input, \$out;
+ $input = join "", (@{$in});
+ finish $handle or die "No selection";
+ return $out;
+}
+
+sub pack_msgpack {
+ my ($filtered_db) = @_;
+ my $msg = Data::MessagePack->pack($filtered_db);
+ my $filename = "$db_file";
+ open(my $out, '>:raw', $filename) or die "Could not open file '$filename' $!";
+ print $out $msg;
+ close $out;
+}
+
+sub unpack_msgpack {
+ my $mp = Data::MessagePack->new->utf8();
+ my $msgpack = read_binary("$db_file");
+ my $rdb = $mp->unpack($msgpack);
+ return $rdb;
+}
+
+sub do_action {
+ my ($in, $context) = @_;
+ my @action_items = ("Add\n", "Replace\n");
+ my $action = backend_call(\@action_items);
+ if ($action eq "Replace\n") {
+ $mpd->clear();
+ }
+ my $input;
+ if ($context eq "playlist") {
+ chomp $in;
+ $mpd->load("$in");
+ } elsif ($context eq "tracks") {
+ foreach my $line (split /\n/, $in) {
+ my $uri = (split /[\t\n]/, $line)[-1];
+ $mpd->add($uri);
+ }
+ }
+ if ($action eq "Replace\n") {
+ $mpd->play();
+ }
+ my @queue_cmd = ('tmux', 'findw', '-t', 'music', 'queue');
+ system(@queue_cmd);
+}
+
+sub list_playlists {
+ my @playlists = $mpd->list_playlists();
+ my $output = formated_playlists(\@playlists);
+
+ for (;;) {
+ my $out = backend_call($output);
+ do_action($out, "playlist");
+ }
+}
+
+sub formated_albums {
+ my ($rdb, $sorted) = @_;
+
+ my %uniq_albums;
+ for my $i (@$rdb) {
+ my $newkey = join "", $i->@{qw/AlbumArtist Album Date/};
+ if (!exists $uniq_albums{$newkey}) {
+ my $dir = (dirname($i->{uri}) =~ s/\/CD.*$//r);
+ $uniq_albums{$newkey} = {$i->%{qw/AlbumArtist Album Date mtime/}, Dir => $dir};
+ } else {
+ if ($uniq_albums{$newkey}->{'mtime'} < $i->{'mtime'}) {
+ $uniq_albums{$newkey}->{'mtime'} = $i->{'mtime'}
+ }
+ }
+ }
+
+ my @albums;
+ my $fmtstr = join "", map {"%-${_}.${_}s\t"} ($albumartist_l, $date_l, $album_l);
+
+ my @skeys;
+ if ($sorted) {
+ @skeys = sort { $uniq_albums{$b}->{mtime} <=> $uniq_albums{$a}->{mtime} } keys %uniq_albums;
+ } else {
+ @skeys = sort keys %uniq_albums;
+ }
+
+ for my $k (@skeys) {
+ my @vals = (map { "$_" // "Unknown" } $uniq_albums{$k}->@{qw/AlbumArtist Date Album/}), $uniq_albums{$k}->{Dir};
+ my $strval = sprintf $fmtstr."%s\n", @vals;
+ push @albums, $strval;
+
+ }
+
+ return \@albums;
+}
+
+sub formated_tracks {
+ my ($rdb) = @_;
+ my $fmtstr = join "", map {"%-${_}.${_}s\t"} ($track_l, $title_l, $artist_l, $album_l);
+ my @tracks = map {
+ sprintf $fmtstr."%-s\n", (map { "$_" // "unknown" } $_->@{qw/Track Title Artist Album/}), $_->{uri};
+# sprintf $fmtstr."%-s\n", $_->@{qw/Track Title Artist Album uri/}
+ } @{$rdb};
+
+ return \@tracks;
+}
+
+sub formated_playlists {
+ my ($rdb) = @_;
+ my @playlists = map {
+ sprintf "%s\n", $_->{playlist}
+ } @{$rdb};
+
+ return \@playlists;
+}
+
+sub list_db_entries_for {
+ my ($kind) = @_;
+ die "Wrong kind" unless any {; $_ eq $kind} qw/Albums Latest Tracks/;
+
+ my $rdb = unpack_msgpack();
+ my %fields = (Albums=> "1,2,3", Latest => "1,2,3", Tracks => "1,2,3,4");
+ my %formater = (
+ Albums => sub {formated_albums(@_, 0)},
+ Latest => sub {formated_albums(@_, 1)},
+ Tracks => \&formated_tracks
+ );
+
+ my $output = $formater{$kind}->($rdb);
+ for (;;) {
+ my $out = backend_call($output, $fields{$kind});
+ do_action($out, "tracks");
+ }
+}
+
+main;