diff options
-rwxr-xr-x | clerk | 781 | ||||
-rw-r--r-- | clerk.conf | 10 | ||||
-rw-r--r-- | clerk.tmux | 25 |
3 files changed, 418 insertions, 398 deletions
@@ -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 @@ -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 @@ -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 |