From cf96918d75b08d0767efff3ec886bfd931b480fe Mon Sep 17 00:00:00 2001 From: Nick Bowler Date: Fri, 28 Jun 2019 00:39:12 -0400 Subject: [PATCH] mpdmenu: Restructure things a bit. Restructure the basic program flow and update the top menu generation using more helper functions and stuff, including a slight change to the menu layout. More to come. --- mpdmenu.pl | 448 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 308 insertions(+), 140 deletions(-) diff --git a/mpdmenu.pl b/mpdmenu.pl index 2311a53..d22193c 100755 --- a/mpdmenu.pl +++ b/mpdmenu.pl @@ -11,9 +11,17 @@ use strict; -use Getopt::Long; +use utf8; + +use Encode qw(decode encode); +use Encode::Locale qw(decode_argv); +decode_argv(Encode::FB_CROAK); +binmode(STDOUT, ":utf8"); + use IO::Socket::INET6; +use Getopt::Long qw(:config gnu_getopt); use Scalar::Util qw(reftype); +use List::Util qw(any); use FindBin; use constant { @@ -22,13 +30,105 @@ use constant { MPD_REV_MIN => 0, }; -use utf8; -use open qw(:std :utf8); -binmode(STDOUT, ":utf8"); -use Encode; - my $SELF = "$FindBin::Bin/$FindBin::Script"; -my $MUSIC = $ENV{MUSIC} // "/srv/music"; + +my $MUSIC = $ENV{MUSIC} // "/srv/music"; +my $host = $ENV{MPD_HOST} // "localhost"; +my $port = $ENV{MPD_PORT} // "6600"; +my $sock; + +my $menu; +my $mode = "top"; + +# Quotes the argument so that it is presented as a single argument to MPD +# at the protocol level. This also works OK for most FVWM arguments. +sub escape { + my $s = @_[0] // $_; + + # No way to encode literal newlines in the protocol, so we + # convert any newlines in the arguments into a space, which + # can help with quoting. + $s =~ s/\n/ /g; + + if (/[ \t\\"]/) { + $s =~ s/[\\"]/\\$&/g; + return "\"$s\""; + } + + $s =~ s/^\s*$/"$&"/; + return $s; +} + +# Submit a command to the MPD server; each argument to this function +# is quoted and sent as a single argument to MPD. +sub mpd_exec { + my $cmd = join(' ', map { escape } @_); + + print $sock "$cmd\n"; +} + +sub fvwm_cmd_unquoted { + print join(' ', @_), "\n"; +} + +sub fvwm_cmd { + fvwm_cmd_unquoted(map { escape } @_); +} + +# Quotes the argument in such a way that it is passed unadulterated by +# both FVWM and the shell to a command as a single argument (for use as +# an # argument for e.g., the Exec or PipeRead FVWM commands). +# +# The result must be used with fvwm_cmd_unquoted; +sub fvwm_shell_literal { + my $s = @_[0] // $_; + + $s =~ s/\$/\$\$/g; + if ($s =~ /[' \t]/) { + $s =~ s/'/'\\''/g; + return "'$s'"; + } + $s =~ s/^\s*$/'$&'/; + return "$s"; +} + +# Escapes metacharacters in the argument used in FVWM menu labels. The +# string must still be quoted (e.g., by using fvwm_cmd). +sub fvwm_label_escape { + my @tokens = split /\t/, $_[0]; + @tokens[0] =~ s/&/&&/g; + my $ret = join "\t", @tokens; + $ret =~ s/[\$@%^*]/$&$&/g; + return $ret; +} + +# make_submenu(name, [args ...]) +# +# Creates a submenu (with the specified name) constructed by invoking this +# script with the given arguments. Returns a list that can be passed to +# fvwm_cmd to display the menu. +sub make_submenu { + my $name = shift; + $name =~ s/-/_/g; + unshift @_, ("exec", $SELF, "--menu=$name"); + + fvwm_cmd("DestroyFunc", "Make$name"); + fvwm_cmd("AddToFunc", "Make$name"); + fvwm_cmd("+", "I", "DestroyMenu", $name); + + fvwm_cmd("DestroyMenu", $name); + fvwm_cmd("AddToMenu", $name, "DynamicPopupAction", "Make$name"); + fvwm_cmd("AddToFunc", "KillMenuMPD", "I", "DestroyMenu", $name); + + fvwm_cmd("DestroyFunc", "Make$name"); + fvwm_cmd("AddToFunc", "Make$name"); + fvwm_cmd("+", "I", "DestroyMenu", $name); + fvwm_cmd("+", "I", "-PipeRead", + join(' ', map { fvwm_shell_literal } @_)); + fvwm_cmd("AddToFunc", "KillMenuMPD", "I", "DestroyFunc", "Make$name"); + + return ("Popup", $name); +} # get_item_thumbnails({ options }, file, ...) # get_item_thumbnails(file, ...) @@ -73,6 +173,43 @@ sub get_item_thumbnails { return @results; } +# add_track_metadata(hashref, key, value) +# +# Inserts the given key into the referenced hash; if the key already exists +# in the hash then the hash element is converted to an array reference (if +# it isn't already) and the value is appended to that array. +sub add_track_metadata { + my ($entry, $key, $value) = @_; + + if (exists($entry->{$key})) { + my $ref = $entry->{$key}; + + if (reftype($ref) ne "ARRAY") { + return if ($ref eq $value); + + $ref = [$ref]; + $entry->{$key} = $ref; + } + + push(@$ref, $value) unless (any {$_ eq $value} @$ref); + } else { + $entry->{$key} = $value; + } +} + +# get_track_metadata(hashref, key) +# +# Return the values associated with the given metadata key as a list. +sub get_track_metadata { + my ($entry, $key) = @_; + + return () unless (exists($entry->{$key})); + + my $ref = $entry->{$key}; + return @$ref if (reftype($ref) eq "ARRAY"); + return $ref +} + # Given a music filename, search for the cover art in the same directory. sub mpd_cover_filename { my ($dir) = @_; @@ -93,38 +230,114 @@ sub mpd_cover_filename { return $file; } -sub cmd -{ - print "$_[0]\n"; +# Generate the cover art entry in the top menu. +sub top_track_cover { + my ($entry) = @_; + + ($entry->{thumb}) = get_item_thumbnails($entry->{file}); + print "$entry->{thumb}\n"; + if ($entry->{thumb}) { + my $file = "$MUSIC/$entry->{file}"; + my $cover = mpd_cover_filename($file); + + $cover = fvwm_shell_literal($cover // $file); + fvwm_cmd_unquoted("AddToMenu", escape($menu), + escape($entry->{thumb}), + "Exec", "exec", "geeqie", $cover); + } +} + +# Generate the "Title:" entry in the top menu. +sub top_track_title { + my ($entry) = @_; + + my @submenu = make_submenu("$menu-TopTrack", + "--title=$entry->{Title}"); + + fvwm_cmd("AddToMenu", $menu, + fvwm_label_escape("Title:\t$entry->{Title}"), + @submenu); +} + +# Generate the "Artist:" entry in the top menu. +sub top_track_artist { + my ($entry) = @_; + + my @submenu = make_submenu("$menu-TopArtist", + "--artist=$entry->{Artist}"); + + fvwm_cmd("AddToMenu", $menu, + fvwm_label_escape("Artist:\t$entry->{Artist}"), + @submenu); +} + +# Generate the "Album:" entry in the top menu. +sub top_track_album { + my ($entry) = @_; + my @submenu; + + my @submenu = make_submenu("$menu-TopAlbum", + "--artist=$entry->{Artist}", + "--album=$entry->{Album}"); + + fvwm_cmd("AddToMenu", $menu, + fvwm_label_escape("Album:\t$entry->{Album}"), + @submenu); } # Global hash for tracking what is to be "accepted". my %accept = (); -my $FVWM = (defined $ENV{FVWM_USERDIR}) ? $ENV{FVWM_USERDIR} - : $ENV{HOME}."/.fvwm"; -my $icons = "$FVWM/icons"; - # Default values for stuff. -my ($album, $artist, $title, $menu) = (undef, undef, undef, undef); -my $host = (defined $ENV{MPD_HOST}) ? $ENV{MPD_HOST} : "localhost"; -my $port = (defined $ENV{MPD_PORT}) ? $ENV{MPD_PORT} : "6600"; +my ($album, $artist, $title); + +sub print_version { + print < 0); +} + +sub print_help { + print_usage(*STDOUT); + print < \&host, # Host that MPD is running on. - 'port|p=s' => \&port, # Port that MPD is listening on. - 'menu|m=s' => \$menu, # Name of the menu to create. - 'album=s' => \$album, # Album to get tracks from - 'artist=s' => \$artist, # Artist to limit results to - 'title=s' => \$title, # Title to create menu for -); - -$album = decode_utf8($album) if defined($album); -$artist = decode_utf8($artist) if defined($artist); -$title = decode_utf8($title) if defined($title);; + 'host|h=s' => \$host, + 'port|p=s' => \$port, + 'menu|m=s' => \$menu, + + 'album=s' => sub { $album = $_[1]; $mode = "album"; }, + 'artist=s' => sub { $artist = $_[1]; + $mode = "artist" unless $mode eq "album"; }, + 'title=s' => sub { $title = $_[1]; $mode = "track"; }, + + 'V|version' => sub { print_version(); exit }, + 'H|help' => sub { print_help(); exit }, +) or do { print_usage; exit 1 }; # Connect to MPD. -my $sock = new IO::Socket::INET6( +$sock = new IO::Socket::INET6( PeerAddr => $host, PeerPort => $port, Proto => 'tcp', @@ -140,7 +353,65 @@ die("MPD version $1.$2.$3 insufficient.\n") || ($1 == MPD_MJR_MIN && $2 < MPD_MNR_MIN) || ($1 == MPD_MJR_MIN && $2 == MPD_MNR_MIN && $3 < MPD_REV_MIN)); -if (defined $album) { +if ($mode eq "top") { + my %current; + my %state; + + $menu //= "MenuMPD"; + + mpd_exec("status"); + while (<$sock>) { + last if (/^OK/); + die($_) if (/^ACK/); + + if (/^(\w+): (.*)$/) { + $state{$1} = $2; + } + } + + mpd_exec("currentsong"); + while (<$sock>) { + last if (/^OK/); + die($_) if (/^ACK/); + + if (/^(\w+): (.*)$/) { + add_track_metadata(\%current, $1, $2); + } + } + + my $playstate = $state{state} eq "play" ? "Playing" + : $state{state} eq "stop" ? "Stopped" + : $state{state} eq "pause" ? "Paused" + : "Unknown"; + fvwm_cmd("AddToMenu", $menu, $playstate, "Title"); + + if (exists($current{file})) { + top_track_cover(\%current); + top_track_title(\%current); + top_track_artist(\%current); + top_track_album(\%current); + } else { + fvwm_cmd("AddToMenu", $menu, "[current track unavailable]"); + } + + if ($state{state} =~ /^p/) { + my $pp = $state{state} eq "pause" ? "lay" : "ause"; + + fvwm_cmd("AddToMenu", $menu, "", "Nop"); + fvwm_cmd("AddToMenu", $menu, "Next%next.svg:16x16%", + "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "next"); + fvwm_cmd("AddToMenu", $menu, "P$pp%p$pp.svg:16x16%", + "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "p$pp"); + fvwm_cmd("AddToMenu", $menu, "Stop%stop.svg:16x16%", + "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "stop"); + fvwm_cmd("AddToMenu", $menu, "Prev%prev.svg:16x16%", + "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "previous"); + } elsif ($state{state} eq "stop") { + fvwm_cmd("AddToMenu", $menu, "", "Nop"); + fvwm_cmd("AddToMenu", $menu, "Play%play.svg:16x16%", + "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "play"); + } +} elsif ($mode eq "album") { # Create an album menu. my @playlist = (); my $entry; @@ -189,7 +460,7 @@ if (defined $album) { cmd($cmd); } -} elsif (defined $artist) { +} elsif ($mode eq "artist") { # Create an artist menu. my %albums = (); my $file; @@ -241,7 +512,7 @@ if (defined $album) { $i++; } } # end use locale workaround -} elsif (defined $title) { +} elsif ($mode eq "track") { # Create a title menu. my @titles; my $entry; @@ -328,114 +599,6 @@ if (defined $album) { ." playid $t_id"); } } # end use locale workaround -} else { - # Make MPD base menu - my ($state, $songid) = (undef, undef); - my %entry = (); - - $menu = "MenuMPD" unless defined $menu; - - print $sock "status\n"; - while (<$sock>) { - last if (/^OK/); - die($_) if (/^ACK/); - - if (/^(\w+): (.*)$/) { - $state = $2 if ($1 eq "state"); - $songid = $2 if ($1 eq "songid"); - } - } - die("Failed status query\n") unless (defined $state); - - cmd("AddToMenu $menu Playing Title") if ($state eq "play"); - cmd("AddToMenu $menu Paused Title") if ($state eq "pause"); - cmd("AddToMenu $menu Stopped Title") if ($state eq "stop"); - - if (defined $songid) { - print $sock "playlistid $songid\n"; - while (<$sock>) { - last if (/^OK/); - die($_) if (/^ACK/); - - if (/^(\w+): (.*)$/) { - $entry{$1} = $2; - } - } - die("Failed data query\n") unless (keys(%entry) > 0); - - my ($thumb) = get_item_thumbnails($entry{file}); - if ($thumb) { - my $cover = mpd_cover_filename("$MUSIC/$entry{file}"); - - cmd("AddToMenu $menu \"$thumb\" " - ."Exec exec geeqie ".shellify($cover, 0)); - } - - cmd("AddToMenu $menu \"Title: ".sanitise($entry{Title}, 0) - ."\" Popup MenuMPDTitle"); - cmd("AddToMenu $menu \"Artist: ".sanitise($entry{Artist}, 0) - ."\" Popup MenuMPDArtist"); - cmd("AddToMenu $menu \"Album: ".sanitise($entry{Album}, 0) - ."\" Popup MenuMPDAlbum"); - cmd("AddToMenu $menu \"\" Nop"); - } else { - cmd("AddToMenu $menu \"\""); - cmd("AddToMenu $menu \"\" Nop"); - } - - if ($state eq "play" || $state eq "pause") { - cmd("AddToMenu $menu \"\t\tNext%$icons/next.svg:16x16%\" " - ."Exec exec $FindBin::Bin/mpdexec.pl next"); - cmd("AddToMenu $menu \"\t\tPause%$icons/pause.svg:16x16%\" " - ."Exec exec $FindBin::Bin/mpdexec.pl pause"); - cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" " - ."Exec exec $FindBin::Bin/mpdexec.pl play"); - cmd("AddToMenu $menu \"\t\tStop%$icons/stop.svg:16x16%\" " - ."Exec exec $FindBin::Bin/mpdexec.pl stop"); - cmd("AddToMenu $menu \"\t\tPrev%$icons/prev.svg:16x16%\" " - ."Exec exec $FindBin::Bin/mpdexec.pl previous"); - } elsif ($state eq "stop") { - cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" " - ."Exec exec $FindBin::Bin/mpdexec.pl play"); - } else { - die("Unknown MPD state!\n"); - } - - cmd("AddToMenu $menu \"\" Nop"); - cmd("AddToMenu $menu \"\t\tShuffle%$icons/shuffle.svg:16x16%\" " - ."Exec exec $FindBin::Bin/mpdexec.pl shuffle"); - - cmd("DestroyMenu MenuMPDTitle"); - cmd("AddToMenu MenuMPDTitle DynamicPopUpAction MakeMenuMPDTitle"); - cmd("DestroyMenu MenuMPDArtist"); - cmd("AddToMenu MenuMPDArtist DynamicPopUpAction MakeMenuMPDArtist"); - cmd("DestroyMenu MenuMPDAlbum"); - cmd("AddToMenu MenuMPDAlbum DynamicPopUpAction MakeMenuMPDAlbum"); - - cmd("DestroyFunc MakeMenuMPDTitle"); - cmd("AddToFunc MakeMenuMPDTitle - + I DestroyMenu MenuMPDTitle - + I -PipeRead \"exec $SELF " - ."--menu MenuMPDTitle " - ."--title ".shellify($entry{Title}, 1)."\""); - - cmd("DestroyFunc MakeMenuMPDAlbum"); - cmd("AddToFunc MakeMenuMPDAlbum - + I DestroyMenu MenuMPDAlbum - + I -PipeRead \"exec $SELF " - ."--menu MenuMPDAlbum " - ."--album ".shellify($entry{Album}, 1)." " - ."--artist ".shellify($entry{Artist}, 1)."\""); - - cmd("DestroyFunc MakeMenuMPDArtist"); - cmd("AddToFunc MakeMenuMPDArtist - + I DestroyMenu MenuMPDArtist - + I -PipeRead \"exec $SELF " - ."--menu MenuMPDArtist " - ."--artist ".shellify($entry{Artist}, 1)."\""); - - cmd("DestroyFunc KillMenuMPD"); - cmd("AddToFunc KillMenuMPD I Nop"); } # Finished. @@ -493,3 +656,8 @@ sub shellify } return "'$str'"; } + +sub cmd +{ + print "$_[0]\n"; +} -- 2.43.2