X-Git-Url: https://git.draconx.ca/gitweb/mpdhacks.git/blobdiff_plain/cf96918d75b08d0767efff3ec886bfd931b480fe..020f9f800a804c638d6432711be319c5b9bfb988:/mpdmenu.pl diff --git a/mpdmenu.pl b/mpdmenu.pl index d22193c..5968843 100755 --- a/mpdmenu.pl +++ b/mpdmenu.pl @@ -21,7 +21,7 @@ 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 List::Util qw(any max); use FindBin; use constant { @@ -37,6 +37,8 @@ my $host = $ENV{MPD_HOST} // "localhost"; my $port = $ENV{MPD_PORT} // "6600"; my $sock; +my ($albumid, $title); +my %artistids; my $menu; my $mode = "top"; @@ -262,9 +264,15 @@ sub top_track_title { # Generate the "Artist:" entry in the top menu. sub top_track_artist { my ($entry) = @_; + my @submenu; - my @submenu = make_submenu("$menu-TopArtist", - "--artist=$entry->{Artist}"); + # TODO: multi-artist tracks should get multiple artist menus; for now + # just combine the releases from all artists. + my @mbids = get_track_metadata($entry, "MUSICBRAINZ_ARTISTID"); + if (@mbids) { + @submenu = make_submenu("$menu-TopArtist", + map { "--artist-id=$_" } @mbids); + } fvwm_cmd("AddToMenu", $menu, fvwm_label_escape("Artist:\t$entry->{Artist}"), @@ -276,20 +284,123 @@ sub top_track_album { my ($entry) = @_; my @submenu; - my @submenu = make_submenu("$menu-TopAlbum", - "--artist=$entry->{Artist}", - "--album=$entry->{Album}"); + my ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_ALBUMID"); + @submenu = make_submenu("$menu-$mbid", "--album-id=$mbid") if $mbid; fvwm_cmd("AddToMenu", $menu, fvwm_label_escape("Album:\t$entry->{Album}"), @submenu); } -# Global hash for tracking what is to be "accepted". -my %accept = (); +# Given a release MBID, return a hash reference containing all its +# associated tracks in the MPD database. The hash keys are filenames. +sub get_tracks_by_release_mbid { + my ($mbid) = @_; + my %matches; + my $entry; + + return \%matches unless ($mbid); + mpd_exec("search", "(MUSICBRAINZ_ALBUMID == \"$mbid\")"); + while (<$sock>) { + last if (/^OK/); + die($_) if (/^ACK/); + + if (/^(\w+): (.*)$/) { + if ($1 eq "file") { + if (exists($matches{$2})) { + $entry = $matches{$2}; + } else { + $entry = {}; + $matches{$2} = $entry; + } + } + + add_track_metadata($entry, $1, $2); + } + } -# Default values for stuff. -my ($album, $artist, $title); + return \%matches; +} + +# Given an artist MBID, return a hash reference containing associated +# releases in the MPD database. The hash keys are release MBIDs. +# +# Since MPD returns results on a per-track basis, each entry in the +# hash has the metadata for one unspecified track from that release. +sub get_releases_by_artist_mbid { + my %releases; + my $entry; + + foreach my $mbid (@_) { + mpd_exec("search", "(MUSICBRAINZ_ARTISTID == \"$mbid\")"); + while (<$sock>) { + last if (/^OK/); + die($_) if (/^ACK/); + + if (/^(\w+): (.*)$/) { + if ($1 eq "file") { + $entry = {}; + } elsif ($1 eq "MUSICBRAINZ_ALBUMID") { + $releases{$2} //= $entry; + } + + add_track_metadata($entry, $1, $2); + } + } + } + + return \%releases; +} + +# Given a filename, return the IDs (if any) for that file in the +# current MPD play queue. +sub get_ids_by_filename { + my ($file) = @_; + my @results = (); + + mpd_exec("playlistfind", "file", $file); + while (<$sock>) { + last if (/^OK/); + die($_) if (/^ACK/); + + if (/^(\w+): (.*)$/) { + push @results, $2 if ($1 eq "Id"); + } + } + + return @results; +} + +# albumsort(matches, a, b) +# +# Sort hash keys (a, b) by disc/track number for album menus. +sub albumsort { + my ($matches, $a, $b) = @_; + + return $matches->{$a}->{Disc} <=> $matches->{$b}->{Disc} + || $matches->{$a}->{Track} <=> $matches->{$b}->{Track} + || $a cmp $b; +} + +# datesort(matches, a, b) +# +# Sort hash keys (a, b) by release date +sub datesort { + my ($matches, $a, $b) = @_; + + return $matches->{$a}->{Date} cmp $matches->{$b}->{Date} + || $a cmp $b; +} + +# menu_trackname(entry) +# +# Format the track name for display in an FVWM menu, where entry +# is a hash reference containing the track metadata. +sub menu_trackname { + my ($entry) = @_; + my $fmt = "$entry->{trackfmt}$entry->{Artist} - $entry->{Title}"; + return "$entry->{thumb}" . fvwm_label_escape($fmt); +} sub print_version { print < \$host, - 'port|p=s' => \$port, - 'menu|m=s' => \$menu, + '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"; }, + 'artist-id=s' => sub { $artistids{$_[1]} = 1; $mode = "artist"; }, + 'album-id=s' => sub { $albumid = $_[1]; $mode = "album"; }, + 'title=s' => sub { $title = $_[1]; $mode = "track"; }, - 'V|version' => sub { print_version(); exit }, - 'H|help' => sub { print_help(); exit }, + 'V|version' => sub { print_version(); exit }, + 'H|help' => sub { print_help(); exit }, ) or do { print_usage; exit 1 }; # Connect to MPD. @@ -412,106 +524,77 @@ if ($mode eq "top") { "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "play"); } } elsif ($mode eq "album") { - # Create an album menu. - my @playlist = (); - my $entry; - - $menu = "MenuMPDAlbum" unless defined $menu; - - $album =~ s/"/\\"/g; - print $sock "playlistfind album \"$album\"\n"; - while (<$sock>) { - last if (/^OK/); - die($_) if (/^ACK/); - - if (/^(\w+): (.*)$/) { - if ($1 eq "file") { - if (keys(%$entry) > 0) { - addalbumentry(\@playlist, $entry) - } - - $entry = {}; + my $matches = get_tracks_by_release_mbid($albumid); + my @notqueued = (); + + $menu //= "MenuMPDAlbum"; + + my $track_max = max(map { $_->{Track} } values %$matches); + my $disc_max = max(map { $_->{Disc} } values %$matches); + + # CDs have a max of 99 tracks and I hope 100+-disc-releases + # don't exist so this is fine. + my $track_digits = $track_max >= 10 ? 2 : 1; + my $disc_digits = $disc_max > 1 ? $disc_max >= 10 ? 2 : 1 : 0; + my $currentdisc; + + fvwm_cmd("AddToMenu", $menu); + fvwm_cmd("+", "Release not found", "Title") unless keys %$matches; + foreach my $file (sort { albumsort($matches, $a, $b) } keys %$matches) { + my $entry = $matches->{$file}; + + # Format disc/track numbers + $entry->{trackfmt} = sprintf("%*.*s%.*s%*d\t", + $disc_digits, $disc_digits, $entry->{Disc}, + $disc_digits, "-", + $track_digits, $entry->{Track}); + $entry->{trackfmt} =~ s/ /\N{U+2007}/g; + + unless (exists $entry->{Id}) { + my ($id) = get_ids_by_filename($file); + if (defined $id) { + $entry->{Id} = $id; + } else { + push @notqueued, $entry; + next; } - - $entry->{$1} = $2; } - } - addalbumentry(\@playlist, $entry) if (keys(%$entry) > 0); - - die("No tracks found.\n") if (!@playlist); - foreach (sort albumsort @playlist) { - my ($t_file, $t_trackno, $t_artist, $t_title, $t_id) = ( - $_->{file}, - $_->{Track}, - $_->{Artist}, - $_->{Title}, - $_->{Id}, - ); - next if (defined $artist && !$accept{albumdir($t_file)}); - - $t_artist = sanitise($t_artist, 0); - $t_title = sanitise($t_title, 0); + if (defined $currentdisc && $currentdisc != $entry->{Disc}) { + fvwm_cmd("+", "", "Nop"); + } + $currentdisc = $entry->{Disc}; - my $cmd = sprintf "AddToMenu $menu \"%d\t%s - %s\"" - ." Exec exec $FindBin::Bin/mpdexec.pl" - ." playid %d", - $t_trackno, $t_artist, $t_title, $t_id; + fvwm_cmd("+", menu_trackname($entry), "Exec", + "exec", "$FindBin::Bin/mpdexec.pl", + "playid", $entry->{Id}); + } - cmd($cmd); + fvwm_cmd("+", "Not in play queue", "Title") if @notqueued; + foreach my $entry (@notqueued) { + fvwm_cmd("+", menu_trackname($entry)); } } elsif ($mode eq "artist") { # Create an artist menu. - my %albums = (); - my $file; - my $quoteartist = $artist; - - $menu = "MenuMPDArtist" unless defined $menu; - - $quoteartist =~ s/"/\\"/g; - print $sock "playlistfind artist \"$quoteartist\"\n"; - while (<$sock>) { - last if (/^OK/); - die($_) if (/^ACK/); - - if (/^(\w+): (.*)$/) { - $file = $2 if ($1 eq "file"); - $albums{$2} = $file if ($1 eq "Album"); - } - } + my $matches = get_releases_by_artist_mbid(keys %artistids); + my $entry; - die("No albums found.\n") if (!keys(%albums)); + $menu //= "MenuMPDArtist"; -{ # work around 'use locale' breaking s///i - my $i = 0; - use locale; + my @mbids = sort { datesort($matches, $a, $b) } keys %$matches; + my @files = map { $matches->{$_}->{file} } @mbids; + my @thumbs = get_item_thumbnails({ small => 1 }, @files); + fvwm_cmd("AddToMenu", $menu, "No releases found", "Title") unless @mbids; - my @keys = sort keys %albums; - my @thumbs = get_item_thumbnails({ small => 1 }, - map { $albums{$_} } @keys); - - foreach my $key (@keys) { - my $a_album = sanitise($key, 1); + foreach my $mbid (@mbids) { + my $entry = $matches->{$mbid}; my $thumb = shift @thumbs; - cmd("AddToMenu $menu \"$thumb$a_album\" Popup MenuMPDArt_$i"); - - cmd("AddToMenu MenuMPDArt_$i DynamicPopUpAction MakeMenuMPDArt_$i"); - - cmd("DestroyFunc MakeMenuMPDArt_$i"); - cmd("AddToFunc MakeMenuMPDArt_$i - + I DestroyMenu MenuMPDArt_$i - + I -PipeRead \"exec $SELF " - ."--menu MenuMPDArt_$i " - ."--album ".shellify($key, 1)." " - ."--artist ".shellify($artist, 1)."\""); - - cmd("AddToFunc KillMenuMPD I DestroyMenu MenuMPDArt_$i"); - cmd("AddToFunc KillMenuMPD I DestroyFunc MakeMenuMPDArt_$i"); - - $i++; + my @submenu = make_submenu("$menu-$mbid", "--album-id=$mbid"); + fvwm_cmd("AddToMenu", $menu, + $thumb . fvwm_label_escape($entry->{Album}), + @submenu); } -} # end use locale workaround } elsif ($mode eq "track") { # Create a title menu. my @titles; @@ -613,32 +696,6 @@ sub sanitise return $_; } -sub addalbumentry -{ - my ($playlist, $entry) = @_; - - push(@$playlist, $entry); - - if (defined $artist && $artist eq $entry->{Artist}) { - my $albumdir = albumdir($entry->{file}); - $accept{$albumdir} = 1; - } -} - -sub albumdir -{ - my $file = $_[0]; - - $file =~ s:(/Disk [0-9]+[^/]*)?/[^/]*$::; - return $file -} - -sub albumsort -{ - return ($a->{Disc} <=> $b->{Disc}) if ($a->{Disc} != $b->{Disc}); - return ($a->{Track} <=> $b->{Track}); -} - sub titlesort { return ($a->{Album} cmp $b->{Album}) if($a->{Album} ne $b->{Album}); @@ -646,17 +703,6 @@ sub titlesort return ($a->{Title} cmp $b->{Title}); } -sub shellify -{ - my ($str, $quoted) = @_; - $str =~ s/'/'\\''/g; - if ($quoted) { - $str =~ s/\\/\\\\/g; - $str =~ s/"/\\"/g; - } - return "'$str'"; -} - sub cmd { print "$_[0]\n";