From: Nick Bowler Date: Sat, 29 Jun 2019 16:04:05 +0000 (-0400) Subject: mpdmenu: Use MBIDs for albums instead of name matching. X-Git-Url: https://git.draconx.ca/gitweb/mpdhacks.git/commitdiff_plain/4c471596ff0f8f1c9d1e64813c8f9a04ef515691 mpdmenu: Use MBIDs for albums instead of name matching. This enables much more accurate album menus, e.g., fully distinguishing between multiple versions of the same release that would previously be merged together into a single display. This also adjusts the album menu to use modern MPD database query features, so we can correctly display all tracks from the album regardless of the state of the play queue. --- diff --git a/mpdmenu.pl b/mpdmenu.pl index d22193c..241ab8e 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,7 @@ my $host = $ENV{MPD_HOST} // "localhost"; my $port = $ENV{MPD_PORT} // "6600"; my $sock; +my ($albumid, $artist, $title); my $menu; my $mode = "top"; @@ -276,20 +277,93 @@ 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); + } + } + + return \%matches; +} + +# 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; +} -# Default values for stuff. -my ($album, $artist, $title); +# 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 < \$port, 'menu|m=s' => \$menu, - 'album=s' => sub { $album = $_[1]; $mode = "album"; }, - 'artist=s' => sub { $artist = $_[1]; - $mode = "artist" unless $mode eq "album"; }, + 'album-id=s' => sub { $albumid = $_[1]; $mode = "album"; }, + 'artist=s' => sub { $artist = $_[1]; $mode = "artist"; }, 'title=s' => sub { $title = $_[1]; $mode = "track"; }, 'V|version' => sub { print_version(); exit }, @@ -412,106 +486,89 @@ 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; + my %matches; + my $entry; - $menu = "MenuMPDArtist" unless defined $menu; + $menu //= "MenuMPDArtist"; - $quoteartist =~ s/"/\\"/g; - print $sock "playlistfind artist \"$quoteartist\"\n"; + mpd_exec("playlistfind", "artist", $artist); while (<$sock>) { last if (/^OK/); die($_) if (/^ACK/); if (/^(\w+): (.*)$/) { - $file = $2 if ($1 eq "file"); - $albums{$2} = $file if ($1 eq "Album"); + $entry = {} if ($1 eq "file"); + $matches{$2} //= $entry if ($1 eq "MUSICBRAINZ_ALBUMID"); + add_track_metadata($entry, $1, $2); } } - die("No albums found.\n") if (!keys(%albums)); - -{ # work around 'use locale' breaking s///i - my $i = 0; - use locale; - - my @keys = sort keys %albums; - my @thumbs = get_item_thumbnails({ small => 1 }, - map { $albums{$_} } @keys); + 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; - 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 +670,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 +677,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";