summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xclerk781
-rw-r--r--clerk.conf10
-rw-r--r--clerk.tmux25
3 files changed, 418 insertions, 398 deletions
diff --git a/clerk b/clerk
index 51624f9..c71843c 100755
--- a/clerk
+++ b/clerk
@@ -6,7 +6,6 @@ use warnings;
use strict;
use utf8;
use Config::Simple;
-use Time::HiRes qw (sleep);
use Data::MessagePack;
#use DDP;
use Encode qw(decode encode);
@@ -15,173 +14,221 @@ use File::Path qw(make_path);
use File::Slurper 'read_binary';
use File::stat;
use Try::Tiny;
-use FindBin '$Bin';
-use FindBin '$Script';
-use List::Util qw(min any shuffle);
-use Getopt::Std;
+use FindBin qw($Bin $Script);
+use Getopt::Long qw(:config no_ignore_case bundling);
use HTTP::Date;
use IPC::Run qw( timeout start );
+use List::Util qw(any max);
use Net::MPD;
+use Pod::Usage qw(pod2usage);
use POSIX qw(tzset);
use autodie;
my $self="$Bin/$Script";
-
-$ENV{TMUX_TMPDIR}='/tmp/clerk/tmux';
-my $minimal = "";
-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};
-}
-my $backend="fzf";
-# 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 $songs = $general_cfg->{songs};
-my $tagging = $general_cfg->{tagging};
-my $chunksize = $general_cfg->{chunksize};
-my $player = $general_cfg->{player};
-
-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 $rating_l = $columns_cfg->{rating_l};
-my $title_l = $columns_cfg->{title_l};
-my $track_l = $columns_cfg->{track_l};
-my $artist_l = $columns_cfg->{artist_l};
-
-my $random;
-# open connection to MPD
+my ($cfg, $mpd);
+my %rvar; # runtime variables
sub main {
- my %options=();
- getopts("talpfrhuyxzATRZN", \%options);
- unless ($options{f}) {
- $backend = 'rofi'
- } else {
- $backend = 'fzf';
+ parse_config();
+ parse_options(@ARGV);
+ tmux_prerequisites();
+
+ renew_db() if $rvar{renewdb};
+ do_instaact() if $rvar{instaact};
+ do_instarand() if $rvar{instarand};
+
+ if ($rvar{tmux_ui}) {
+ tmux_ui();
+ } else {
+ my $go = select_action();
+ do {
+ maybe_renew_db();
+ $go->();
+ tmux_jump_to_queue_maybe();
+ } while ($rvar{endless});
}
- if ($options{t} // $options{a} // $options{p} // $options{l} // $options{N} // $options{R} // $options{Z} // $options{r} // $options{x} // $options{y} // $options{h} // $options{z} // $options{u}) {
- if (defined $options{t}) { create_db(); list_db_entries_for("Tracks") }
- elsif (defined $options{a}) { create_db(); list_db_entries_for("Albums") }
- elsif (defined $options{p}) { create_db(); list_playlists() }
- elsif (defined $options{z}) { help() }
- elsif (defined $options{h}) { help_output() }
- elsif (defined $options{R}) { rating() }
- elsif (defined $options{x}) {
- create_db();
- system('tmux', 'split-window', '-d', $self, '-f', '-r');
- system('tmux', 'select-pane', '-D');
- }
- elsif (defined $options{y}) {
- system('tmux', 'split-window', '-d', $self, '-f', '-z');
- system('tmux', 'select-pane', '-D');
- }
- elsif (defined $options{Z}) {
- system('tmux', 'split-window', '-d', $self, '-f', '-R');
- system('tmux', 'select-pane', '-D');
- }
- elsif (defined $options{r}) {
- if (defined $options{T}) { random_tracks(); }
- elsif (defined $options{A}) { random_album(); }
- elsif (defined $options{N}) { random_rated_tracks(); }
- else { random() }
- }
- elsif (defined $options{l}) { create_db(); list_db_entries_for("Latest") }
- elsif (defined $options{u}) {
- unlink $db_file;
- create_db();
+}
+
+sub parse_config {
+ $rvar{config_file} = $ENV{CLERK_CONF}
+ // $ENV{HOME} . '/.config/clerk/clerk.conf';
+ $cfg //= Config::Simple->new(filename=>$rvar{config_file});
+
+ my $g = $cfg->param(-block=>'General');
+ %rvar = (%rvar,
+ mpd_host => $g->{mpd_host},
+ tmux_config => $g->{tmux_config},
+ songs => $g->{songs},
+ chunksize => $g->{chunksize},
+ player => $g->{player},
+ jump_query => $g->{jump_query}
+ );
+
+ my $c = $cfg->param(-block=>'Columns');
+ $rvar{max_width} = {
+ album => $c->{album_l},
+ date => $c->{date_l},
+ title => $c->{title_l},
+ track => $c->{track_l},
+ artist => $c->{artist_l},
+ rating => $c->{rating_l},
+ albumartist => $c->{albumartist_l}
+ };
+
+ $rvar{db} = { file => $g->{database}, mtime => 0 };
+}
+
+sub parse_options {
+ local @ARGV = @_;
+ my $parse_act = sub {
+ my ($name, $bool) = @_;
+ if ($bool && defined $rvar{action}) {
+ warn "Will override already set action: $rvar{action}\n";
}
+ $rvar{action} = "$name" if $bool;
+ };
+
+ my $choices = sub {
+ my ($rvar, @choices) = @_;
+
+ return sub {
+ my ($name, $value) = @_;
+ if (any { $value eq $_ } @choices) {
+ $$rvar = $value;
+ } else {
+ die "Value: $value none of " . join(', ', @choices) . "\n";
+ }
+ };
+ };
+
+ $rvar{backend} = 'rofi';
+ GetOptions(
+ 'help|h' => sub { pod2usage(1) },
+
+ # general
+ 'renewdb|u' => \$rvar{renewdb},
+ 'tmux-ui!' => \$rvar{tmux_ui},
+ 'endless!' => \$rvar{endless},
+ 'backend=s' => $choices->(\$rvar{backend}, qw/fzf rofi/),
+ 'f' => sub { $rvar{backend} = 'fzf'; },
+
+ # action
+ 'tracks|t' => $parse_act,
+ 'albums|a' => $parse_act,
+ 'playlists|p' => $parse_act,
+ 'randoms|r' => $parse_act,
+ 'latests|l' => $parse_act,
+
+ # instaact
+ 'instaact=s' => $choices->(\$rvar{instaact},
+ qw/help_pane rand_pane tmux_help/),
+
+ # instarand
+ 'instarand=s' => $choices->(\$rvar{instarand}, qw/track album/),
+ 'T' => sub { $rvar{instarand} = 'track'; },
+ 'A' => sub { $rvar{instarand} = 'album'; },
+ ) or pod2usage(2);
+
+ $rvar{tmux_ui} = (
+ $rvar{action} ||
+ $rvar{instaact} ||
+ (defined $rvar{tmux_ui} && !$rvar{tmux_ui})
+ )? 0 : 1;
+
+ # verify combinations if options
+
+ if ($rvar{backend} eq 'fzf' && $rvar{instarand}) {
+ die "Backend $rvar{backend} and instant random for $rvar{instarand} is not possible\n";
}
- else {
- create_db();
- system('tmux', 'has-session', '-t', 'music');
- if ($? != -0) {
- system('tmux', '-f', $tmux_config, 'new-session', '-s', 'music', '-n', 'albums', '-d', $self, '-a', '-f');
- system('tmux', 'new-window', '-t', 'music', '-n', 'tracks', $self, '-t', '-f');
- system('tmux', 'new-window', '-t', 'music', '-n', 'latest', $self, '-l', '-f');
- system('tmux', 'new-window', '-t', 'music', '-n', 'playlists', $self, '-p', '-f');
- system('tmux', 'new-window', '-t', 'music', '-n', 'queue', $player);
- system('tmux', 'attach', '-t', 'music');
- system('tmux', 'select-window', '-t', 'queue')
- }
- else {
- system('tmux', 'attach', '-t', 'music');
- system('tmux', 'select-window', '-t', 'queue')
- }
+
+ if (($rvar{action} // '') ne 'randoms' && defined $rvar{instarand}) {
+ die "-T or -A without -r not allowed\n";
}
}
-sub help_output {
- my @output = (
- "Usage: clerk [command] [-f]",
- "clerk version 2.0",
- "",
- "Commands:",
- " -a Add/Replace album(s) to queue.",
- " -l Add/Replace album(s) to queue (sorted by mtime)",
- " -t Add/Replace track(s) to queue.",
- " -R Add/Replace rated track(s) to queue.",
- " -p Add stored playlist to queue",
- " -r [-A, -T, -N] Replace current playlist with random songs/album",
- " -u Update caches",
- "",
- "Options:",
- " -f Use fzf interface",
- " ",
- "Without further arguments, clerk starts a tabbed tmux interface");
-
- print join("\n", @output), "\n\n";
-}
-
-sub create_db {
- my $mpd = Net::MPD->connect($ENV{MPD_HOST} // $mpd_host // 'localhost');
+sub do_instaact {
+ local $_ = $rvar{instaact};
+ if (/help_pane/) { tmux_spawn_help_pane() }
+ elsif (/rand_pane/) { tmux_spawn_random_pane() }
+ elsif (/tmux_help/) { help() }
+ exit;
+}
+
+sub do_instarand {
+ local $_ = $rvar{instarand};
+ if (/track/) { random_tracks() }
+ elsif (/album/) { random_album() }
+ exit;
+}
+
+sub select_action {
+ local $_ = $rvar{action} // '';
+ if (/tracks/) { return sub { action_db_entries(ask_to_pick_tracks()) } }
+ elsif (/albums/) { return sub { action_db_entries(ask_to_pick_albums()) } }
+ elsif (/playlists/) { return sub { action_playlist(ask_to_pick_playlists()) } }
+ elsif (/randoms/) { return sub { action_random(ask_to_pick_random()) } }
+ elsif (/latests/) { return sub { action_db_entries(ask_to_pick_latests()) } }
+
+ return sub {};
+}
+
+sub db_needs_update {
+ mpd_reachable();
+ my $last = $mpd->stats->{db_update};
+ return !-f $rvar{db}{file} || stat($rvar{db}{file})->mtime < $last;
+}
+
+sub renew_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
+ mpd_reachable();
my @track_ratings = $mpd->sticker_find("song", "rating");
my %track_ratings = map {$_->{file} => $_->{sticker}} @track_ratings;
+
my $mpd_stats = $mpd->stats();
my $songcount = $mpd_stats->{songs};
- my $last_update = $mpd_stats->{db_update};
+ my $times = int($songcount / $rvar{chunksize} + 1);
- if (!-f "$db_file" || stat("$db_file")->mtime < $last_update) {
- my $times = int($songcount / $chunksize + 1);
- if ($backend eq "rofi") {
- system('notify-send', '-t', '5', 'clerk', 'Updating Cache File');
- }
- elsif ($backend eq "fzf") {
- print STDERR "::: No cache found or cache file outdated\n";
- print STDERR "::: Chunksize set to $chunksize songs\n";
- 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;
- }
+ if ($rvar{backend} eq "rofi") {
+ system('notify-send', '-t', '5', 'clerk', 'Updating Cache File');
+ }
+ elsif ($rvar{backend} eq "fzf") {
+ print STDERR "::: No cache found or cache file outdated\n";
+ print STDERR "::: Chunksize set to $rvar{chunksize} songs\n";
+ print STDERR "::: Requesting $times chunks from MPD\n";
+ }
- # 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'}); $_->{rating} = $track_ratings{$_->{uri}}; +{$_->%{qw/Album Artist Date AlbumArtist Title Track rating uri mtime/}} } @db;
-# my @filtered = map { $_->{mtime} = str2time($_->{'Last-Modified'}); +{$_->%{qw/Album Artist Date AlbumArtist Title Track uri mtime/}} } @db;
- pack_msgpack(\@filtered);
+ my @db;
+ # since mpd will silently fail, if response is larger than command buffer, let's split the search.
+ my $chunk_size = $rvar{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'});
+ $_->{rating} = $track_ratings{$_->{uri}};
+ +{$_->%{qw/Album Artist Date AlbumArtist Title Track rating uri mtime/}}
+ } @db;
+ pack_msgpack(\@filtered);
+}
+
+sub maybe_renew_db {
+ renew_db() if db_needs_update();
}
sub help {
- my $help_txt = system('grep', 'bind-key', $tmux_config);
- print "$help_txt\n";
+ open (my $fh, '<', $rvar{tmux_config});
+ my @out;
+ while (my $l = <$fh>) {
+ push @out, $l if $l =~ /bind-key/;
+ }
+ print @out;
<STDIN>;
}
@@ -207,9 +254,9 @@ sub backend_call {
"--bind=esc:$random,alt-a:toggle-all,alt-n:deselect-all",
"--with-nth=$fields"
],
- rofi => [ "rofi", "-matching", "regex", "-dmenu", "-multi-select", "-i", "-p", "> " , "-filter", $minimal ]
+ rofi => [ "rofi", "-width", "1300", "-matching", "regex", "-dmenu", "-kb-row-tab", "", "-kb-move-word-forward", "", "-kb-accept-alt", "Tab", "-multi-select", "-no-levensthein-sort", "-i", "-p", "> " ]
);
- my $handle = start $backends{$backend} // die('backend not found'), \$input, \$out;
+ my $handle = start $backends{$rvar{backend}} // die('backend not found'), \$input, \$out;
$input = join "", (@{$in});
finish $handle or die "No selection";
return $out;
@@ -218,7 +265,7 @@ sub backend_call {
sub pack_msgpack {
my ($filtered_db) = @_;
my $msg = Data::MessagePack->pack($filtered_db);
- my $filename = "$db_file";
+ my $filename = $rvar{db}{file};
open(my $out, '>:raw', $filename) or die "Could not open file '$filename' $!";
print $out $msg;
close $out;
@@ -226,14 +273,22 @@ sub pack_msgpack {
sub unpack_msgpack {
my $mp = Data::MessagePack->new->utf8();
- my $msgpack = read_binary("$db_file");
+ my $msgpack = read_binary($rvar{db}{file});
my $rdb = $mp->unpack($msgpack);
return $rdb;
}
+sub get_rdb {
+ my $mtime = stat($rvar{db}{file})->mtime;
+ if ($rvar{db}{mtime} < $mtime) {
+ $rvar{db}{ref} = unpack_msgpack();
+ $rvar{db}{mtime} = $mtime;
+ }
+ return $rvar{db}{ref};
+}
+
sub random_album {
- my $mpd = Net::MPD->connect($ENV{MPD_HOST} // $mpd_host // 'localhost');
- my @queue_cmd = ('tmux', 'findw', '-t', 'music', 'queue');
+ mpd_reachable();
$mpd->clear();
my @album_artists = $mpd->list('albumartist');
my $artist_r = $album_artists[rand @album_artists];
@@ -241,14 +296,13 @@ sub random_album {
my $album_r = $album[rand @album];
$mpd->find_add('albumartist', $artist_r, 'album', $album_r);
$mpd->play();
- system(@queue_cmd);
+ tmux_jump_to_queue_maybe();
}
sub random_tracks {
- my $mpd = Net::MPD->connect($ENV{MPD_HOST} // $mpd_host // 'localhost');
- my @queue_cmd = ('tmux', 'findw', '-t', 'music', 'queue');
+ mpd_reachable();
$mpd->clear();
- for (my $i=0; $i <= $songs; $i++) {
+ for (my $i=0; $i <= $rvar{songs}; $i++) {
my @artists = $mpd->list('artist');
my $artist_r = $artists[rand @artists];
my @albums = $mpd->list('album', 'artist', $artist_r);
@@ -257,191 +311,9 @@ sub random_tracks {
my $track_r = $tracks[rand @tracks];
my $foo = $track_r->{uri};
$mpd->add($foo);
- system(@queue_cmd);
$mpd->play();
}
-}
-
-sub random_rated_tracks {
- my $mpd = Net::MPD->connect($ENV{MPD_HOST} // $mpd_host // 'localhost');
- my @queue_cmd = ('tmux', 'findw', '-t', 'music', 'queue');
- try { $mpd->close(); };
- $mpd->{socket}->close;
- my @result;
- my @action_items = ("1\n", "2\n", "3\n", "4\n", "5\n", "6\n", "7\n", "8\n", "9\n", "10\n");
- my $minimal_rating = backend_call(\@action_items, "1,2,3", "cancel");
- chomp $minimal_rating;
- $minimal = "$minimal_rating";
- my $rdb = unpack_msgpack();
- my @rating_rdb = grep { ($_->{rating} // 0) >= $minimal } @$rdb;
- my @random_rated_tracks = shuffle @rating_rdb;
- $mpd->add($_->{uri}) for (splice(@random_rated_tracks, -min($#random_rated_tracks, $songs)));
-}
-
-sub random {
- my $mpd = Net::MPD->connect($ENV{MPD_HOST} // $mpd_host // 'localhost');
- my @queue_cmd = ('tmux', 'findw', '-t', 'music', 'queue');
- try { $mpd->close(); };
- $mpd->{socket}->close;
- my @action_items = ("Album\n", "Tracks\n", "Rated Tracks\n", "Settings\n");
- my $action = backend_call(\@action_items, "1,2,3", "cancel");
- if ($action eq "Album\n") {
- random_album();
- system(@queue_cmd);
- $mpd->play();
- }
- if ($action eq "Tracks\n") {
- random_tracks();
- system(@queue_cmd);
- $mpd->play();
- }
- if ($action eq "Rated Tracks\n") {
- random_rated_tracks();
- system(@queue_cmd);
- $mpd->play();
- }
- if ($action eq "Settings\n") {
- my @action_items = ("5\n", "10\n", "15\n", "20\n", "25\n", "30\n");
- my $action = backend_call(\@action_items);
- chomp $action;
- $cfg->param("General.songs", $action);
- $cfg->save();
- }
-}
-
-sub rating {
- my $mpd = Net::MPD->connect($ENV{MPD_HOST} // $mpd_host // 'localhost');
- my @queue_cmd = ('tmux', 'findw', '-t', 'music', 'queue');
- try { $mpd->close(); };
- $mpd->{socket}->close;
- my @action_items = ("1\n", "2\n", "3\n", "4\n", "5\n", "6\n", "7\n", "8\n", "9\n", "10\n");
- my $minimal_rating = backend_call(\@action_items, "1,2,3", "cancel");
- chomp $minimal_rating;
- $minimal = "$minimal_rating";
- list_db_entries_for("Ratings");
-}
-
-sub do_action {
- $minimal = "";
- my $mpd = Net::MPD->connect($ENV{MPD_HOST} // $mpd_host // 'localhost');
- my @queue_cmd = ('tmux', 'findw', '-t', 'music', 'queue');
- my ($in, $context) = @_;
- my $action;
- try { $mpd->close(); };
- $mpd->{socket}->close;
- if ($context eq "playlist") {
- if ($in eq "Save current Queue\n") {
- tzset();
- my $filename = localtime();
- $mpd->save($filename);
- system(@queue_cmd);
- } else {
- my @action_items = ("Add\n", "Replace\n", "Delete\n");
- $action = backend_call(\@action_items);
- if ($action eq "Replace\n") {
- $mpd->clear();
- chomp $in;
- $mpd->load("$in");
- $mpd->play();
- system(@queue_cmd);
- }
- elsif ($action eq "Add\n") {
- chomp $in;
- $mpd->load("$in");
- }
- elsif ($action eq "Delete\n") {
- chomp $in;
- $mpd->rm("$in");
- }
- }
- } elsif ($context eq "Tracks") {
- my @action_items = ("Add\n", "Replace\n", "---\n", "Rate Track\n");
- $action = backend_call(\@action_items);
- if ($action eq "Replace\n") {
- $mpd->clear();
- }
- if ($action eq "Rate Track\n") {
- my @rating_value = ("1\n", "2\n", "3\n", "4\n", "5\n", "6\n", "7\n", "8\n", "9\n", "10\n");
- my $rating;
- $rating = backend_call(\@rating_value);
- chomp $rating;
- my $input;
- foreach my $line (split /\n/, $in) {
- my $uri = (split /[\t\n]/, $line)[-1];
- $uri = decode('UTF-8', $uri );
- if ($tagging eq "true") {
- $mpd->sticker_value("song", "$uri", "rating", "$rating");
- }
- $mpd->send_message('rating', encode('UTF-8', "${uri}\tRATING\t${rating}"));
- }
- }
- elsif ($action eq "Add\n" || $action eq "Replace\n") {
- my $input;
- foreach my $line (split /\n/, $in) {
- my $uri = (split /[\t\n]/, $line)[-1];
- $uri = decode('UTF-8', $uri );
- $mpd->add($uri);
- system(@queue_cmd);
- }
- }
- if ($action eq "Replace\n") {
- $mpd->play();
- }
- } elsif ($context eq "Albums" || $context eq "Latest") {
- my @action_items = ("Add\n", "Replace\n", "---\n", "Rate Album\n");
- $action = backend_call(\@action_items);
- if ($action eq "Rate Album\n") {
- my @rating_value = ("1\n", "2\n", "3\n", "4\n", "5\n", "6\n", "7\n", "8\n", "9\n", "10\n");
- my $rating;
- $rating = backend_call(\@rating_value);
- chomp $rating;
- my $input;
- foreach my $line (split /\n/, $in) {
- my $song_tags;
- my $uri = (split /[\t\n]/, $line)[-1];
- $uri = decode('UTF-8', $uri );
- my @files = $mpd->search('filename', $uri);
- my @song_tags = $files[0];
- my $artist = $song_tags[0]->{AlbumArtist};
- my $album = $song_tags[0]->{Album};
- my $date = $song_tags[0]->{Date};
- my @songs_to_tag = $mpd->search('albumartist', $artist, 'album', $album, 'date', $date);
- foreach my $songs (@songs_to_tag) {
- my $filename = $songs->{uri};
- $mpd->sticker_value("song", $filename, "albumrating", "$rating");
- if ($tagging eq "true") {
- $mpd->send_message('rating', encode('UTF-8', "${filename}\tALBUMRATING\t${rating}"));
- }
- }
- }
- }
-
- if ($action eq "Replace\n") {
- $mpd->clear();
- }
- if ($action eq "Add\n" || $action eq "Replace\n") {
- my $input;
- foreach my $line (split /\n/, $in) {
- my $uri = (split /[\t\n]/, $line)[-1];
- $uri = decode('UTF-8', $uri );
- $mpd->add($uri);
- system(@queue_cmd);
- }
- }
- if ($action eq "Replace\n") {
- $mpd->play();
- }
- }
-}
-
-sub list_playlists {
- my $mpd = Net::MPD->connect($ENV{MPD_HOST} // $mpd_host // 'localhost');
- for (;;) {
- my @playlists = $mpd->list_playlists();
- my $output = formatted_playlists(\@playlists);
- my $out = backend_call($output);
- do_action($out, "playlist", "ignore");
- }
+ tmux_jump_to_queue_maybe();
}
sub formatted_albums {
@@ -461,7 +333,7 @@ sub formatted_albums {
}
my @albums;
- my $fmtstr = join "", map {"%-${_}.${_}s\t"} ($albumartist_l, $date_l, $album_l);
+ my $fmtstr = join "", map {"%-${_}.${_}s\t"} ($rvar{max_width}->@{qw/albumartist date album/});
my @skeys;
if ($sorted) {
@@ -479,34 +351,20 @@ sub formatted_albums {
return \@albums;
}
-sub formatted_ratings {
- my ($rating) = @_;
- my $rating_fmt = "r=$rating";
- return($rating_fmt);
-}
-
sub formatted_tracks {
my ($rdb) = @_;
- my $fmtstr = join "", map {"%-${_}.${_}s\t"} ($track_l, $title_l, $artist_l, $date_l, $album_l, $rating_l);
+ my $fmtstr = join "", map {"%-${_}.${_}s\t"} ($rvar{max_width}->@{qw/track title artist album rating/});
+ $fmtstr .= "%-s\n";
my @tracks = map {
- sprintf $fmtstr."%-s\n", (map { $_ // "-" } $_->@{qw/Track Title Artist Date Album/}), formatted_ratings($_->{rating} // 0), $_->{uri};
-# sprintf $fmtstr."%-s\n", $_->@{qw/Track Title Artist Album uri/}
+ sprintf $fmtstr,
+ (map { $_ // "-" } $_->@{qw/Track Title Artist Album/}),
+ "r=" . ($_->{rating} // '0'),
+ $_->{uri};
} @{$rdb};
return \@tracks;
}
-sub formatted_rating {
- my ($rdb) = @_;
- my @rating_rdb = grep { ($_->{rating} // 0) >= $minimal } @$rdb;
- $minimal = "";
- my $fmtstr = join "", map {"%-${_}.${_}s\t"} ($track_l, $title_l, $artist_l, $date_l, $album_l, $rating_l);
- my @tracks = map {
- sprintf $fmtstr."%-s\n", (map { $_ // "-" } $_->@{qw/Track Title Artist Date Album/}), formatted_ratings($_->{rating} // 0), $_->{uri};
- } @rating_rdb;
- return \@tracks;
-}
-
sub formatted_playlists {
my ($rdb) = @_;
my @save = ("Save");
@@ -520,36 +378,201 @@ sub formatted_playlists {
return \@playlists;
}
-sub list_db_entries_for {
- my ($kind) = @_;
- die "Wrong kind" unless any {; $_ eq $kind} qw/Albums Latest Tracks Ratings/;
+sub tmux_prerequisites {
+ $ENV{TMUX_TMPDIR} = '/tmp/clerk/tmux';
+ make_path($ENV{TMUX_TMPDIR}) unless(-d $ENV{TMUX_TMPDIR});
+}
- my $rdb = unpack_msgpack();
- my %fields = (Albums=> "1,2,3", Latest => "1,2,3", Tracks => "1,2,3,4,5,6", Ratings => "1,2,3,4,5,6");
- my %formater = (
- Albums => sub {formatted_albums(@_, 0)},
- Latest => sub {formatted_albums(@_, 1)},
- Tracks => \&formatted_tracks,
- Ratings => \&formatted_rating
- );
+sub tmux {
+ my @args = @_;
+ system 'tmux', @args;
+}
- my $output = $formater{$kind}->($rdb);
- if ($backend eq "rofi") {
- my $out = backend_call($output, $fields{$kind});
- do_action($out, $kind, "ignore");
+sub tmux_jump_to_queue_maybe {
+ tmux qw/findw -t music queue/ if $rvar{jump_queue};
+}
+
+sub tmux_spawn_random_pane {
+ tmux 'splitw', '-d', $self, '--backend=fzf', '--randoms';
+ tmux qw/select-pane -D/;
+}
+
+sub tmux_spawn_help_pane {
+ tmux 'splitw', '-d', $self, '--instaact=tmux_help';
+ tmux qw/select-pane -D/;
+}
+
+sub tmux_has_session {
+ tmux qw/has -t/, @_;
+ return $? == -0;
+}
+
+sub tmux_ui {
+ maybe_renew_db();
+ unless (tmux_has_session('music')) {
+ my @win = qw/neww -t music -n/;
+ my @clerk = ($self, '--backend=fzf', '--endless');
+ tmux '-f', $rvar{tmux_config}, qw/new -s music -n albums -d/, @clerk, '-a';
+ tmux @win, 'tracks', @clerk, '-t';
+ tmux @win, 'latest', @clerk, '-l';
+ tmux @win, 'playlists', @clerk, '-p';
+ tmux @win, 'queue', $rvar{player};
+ tmux qw/set-environment CLERKBIN/, $self;
}
- elsif ($backend eq "fzf") {
- for (;;) {
- create_db();
- my $out = backend_call($output, $fields{$kind});
- try {
- do_action($out, $kind, "ignore");
- if($kind eq "Ratings") {
- system('tmux', 'kill-pane');
- }
- }
+ tmux qw/attach -t music/;
+ tmux qw/selectw -t queue/;
+}
+
+sub ask_to_pick_tracks {
+ return backend_call(formatted_tracks(get_rdb()), "1,2,3,4");
+}
+
+sub ask_to_pick_albums {
+ return backend_call(formatted_albums(get_rdb(), 0), "1,2,3");
+}
+
+sub ask_to_pick_latests {
+ return backend_call(formatted_albums(get_rdb(), 1), "1,2,3");
+}
+
+sub ask_to_pick_playlists {
+ mpd_reachable();
+ my @pls = $mpd->list_playlists;
+ return backend_call(formatted_playlists(\@pls), "1,2,3");
+}
+
+sub ask_to_pick_random {
+ return backend_call(["Tracks\n", "Albums\n", "Settings\n"]);
+}
+
+sub ask_to_pick_settings {
+ return backend_call([map { $_ . "\n" } qw/5 10 15 20 25 30/]);
+}
+
+sub action_db_entries {
+ my ($out) = @_;
+
+ my @sel = util_parse_selection($out);
+
+ my $action = backend_call(["Add\n", "Replace\n"]);
+ mpd_reachable();
+ {
+ local $_ = $action;
+ if (/^Add/) { mpd_add_items(\@sel) }
+ elsif (/^Replace/) { mpd_replace_with_items(\@sel) }
+ }
+}
+
+sub action_playlist {
+ my ($out) = @_;
+
+ if ($out =~ /^Save current Queue/) {
+ mpd_save_cur_playlist();
+ maybe_renew_db();
+ } else {
+ my @sel = util_parse_selection($out);
+ my $action = backend_call(["Add\n", "Replace\n", "Delete\n"]);
+
+ mpd_reachable();
+ local $_ = $action;
+ if (/^Add/) { mpd_add_playlists(\@sel) }
+ elsif (/^Delete/) { mpd_delete_playlists(\@sel) }
+ elsif (/^Replace/) { mpd_replace_with_playlists(\@sel) }
+ }
+}
+
+sub action_random {
+ my ($out) = @_;
+
+ mpd_reachable();
+ {
+ local $_ = $out;
+ if (/^Track/) { random_tracks() }
+ elsif (/^Album/) { random_album() }
+ elsif (/^Settings/) { action_settings(ask_to_pick_settings()) }
}
}
+
+sub action_settings {
+ my ($out) = @_;
+
+ $rvar{songs} = max map { split /[\t\n]/ } (split /\n/, $out);
+
+ $cfg->param("General.songs", $rvar{songs});
+ $cfg->save();
+}
+
+sub util_parse_selection {
+ my ($sel) = @_;
+ map { (split /[\t\n]/, $_)[-1] } (split /\n/, decode('UTF-8', $sel));
+}
+
+sub mpd_add_items {
+ $mpd->add($_) for @{$_[0]};
+}
+
+sub mpd_replace_with_items {
+ $mpd->clear;
+ mpd_add_items(@_);
+ $mpd->play;
+}
+
+sub mpd_save_cur_playlist {
+ tzset();
+ $mpd->save(scalar localtime);
+}
+
+sub mpd_add_playlists {
+ $mpd->load($_) for @{$_[0]};
+}
+
+sub mpd_replace_with_playlists {
+ $mpd->clear;
+ mpd_add_playlists(@_);
+ $mpd->play;
+}
+
+sub mpd_delete_playlists {
+ $mpd->rm($_) for @{$_[0]};
+}
+
+# quirk to ensure mpd does not croak just because of timeout
+sub mpd_reachable {
+ $mpd //= Net::MPD->connect($ENV{MPD_HOST} // $rvar{mpd_host} // 'localhost');
+ try {
+ $mpd->ping;
+ } catch {
+ $mpd->_connect;
+ };
}
main;
+
+__END__
+
+=encoding utf8
+
+=head1 NAME
+
+clerk - mpd client, based on rofi
+
+=head1 SYNOPSIS
+
+clerk [command] [-f]
+
+ Commands:
+ -a Add/Replace album(s) to queue.
+ -l Add/Replace album(s) to queue (sorted by mtime)
+ -t Add/Replace track(s) to queue.
+ -p Add stored playlist to queue
+ -r [-A, -T] Replace current playlist with random songs/album
+ -u Update caches
+
+ Options:
+ -f Use fzf interface
+
+ Without further arguments, clerk starts a tabbed tmux interface
+
+clerk version 2.0
+
+=cut
diff --git a/clerk.conf b/clerk.conf
index 9cd3449..e28962b 100644
--- a/clerk.conf
+++ b/clerk.conf
@@ -2,9 +2,6 @@
# MPD_HOST will override this
mpd_host=localhost
-# music root for rating_client
-music_root=/mnt/Music
-
# define file paths
database=PLACEHOLDER/.config/clerk/database.mpk
tmux_config=PLACEHOLDER/.config/clerk/clerk.tmux
@@ -18,9 +15,10 @@ songs=20
# if mpd drops the connection while updating, reduce this.
chunksize=30000
-# write tags to audio files. Needs running clerk_rating_client on machine with audio files
-# ratings will always be written to sticker database.
-tagging=false
+# rofi theme to use for clerk
+rofi_theme=clerk
+
+jump_query=0
[Columns]
# width of columns
diff --git a/clerk.tmux b/clerk.tmux
index 7f58353..135abda 100644
--- a/clerk.tmux
+++ b/clerk.tmux
@@ -1,18 +1,17 @@
# !Dont move this section.
## Key Bindings
-bind-key -n F1 findw albums # show album list
-bind-key -n F2 findw tracks # show tracks
-bind-key -n F3 findw latest # show album list (latest first)
-bind-key -n F4 findw playlists # load playlist
-bind-key -n F5 findw queue # show queue
-bind-key -n C-F5 run-shell 'mpc prev --quiet' # previous song
-bind-key -n C-F6 run-shell 'mpc toggle --quiet' # toggle playback
-bind-key -n C-F7 run-shell 'mpc stop > /dev/null' # stop playback
-bind-key -n C-F8 run-shell 'mpc next --quiet' # next song
-bind-key -n F10 run-shell 'clerk -f -x' # play random album/songs
-bind-key -n C-F1 run-shell 'clerk -f -y' # show help
-bind-key -n F9 run-shell 'clerk -f -Z' # show rating menu
-bind-key -n C-q kill-session -t music # quit clerk
+bind-key -n F1 findw albums # show album list
+bind-key -n F2 findw tracks # show tracks
+bind-key -n F3 findw latest # show album list (latest first)
+bind-key -n F4 findw playlists # load playlist
+bind-key -n F5 findw queue # show queue
+bind-key -n C-F5 run-shell 'mpc prev --quiet' # previous song
+bind-key -n C-F6 run-shell 'mpc toggle --quiet' # toggle playback
+bind-key -n C-F7 run-shell 'mpc stop > /dev/null' # stop playback
+bind-key -n C-F8 run-shell 'mpc next --quiet' # next song
+bind-key -n F10 run-shell '$CLERKBIN --instaact=rand_pane' # play random album/songs
+bind-key -n C-F1 run-shell '$CLERKBIN --instaact=help_pane' # show help
+bind-key -n C-q kill-session -t music # quit clerk
# Status bar