]> git.draconx.ca Git - mpdhacks.git/commitdiff
mpdmenu: Use MBIDs for albums instead of name matching.
authorNick Bowler <nbowler@draconx.ca>
Sat, 29 Jun 2019 16:04:05 +0000 (12:04 -0400)
committerNick Bowler <nbowler@draconx.ca>
Sun, 30 Jun 2019 14:28:58 +0000 (10:28 -0400)
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.

mpdmenu.pl

index d22193c960fa2a778a4ef9b0227f76235830233c..241ab8e39076d49d7a07f3502a7c4d2a6b363849 100755 (executable)
@@ -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 <<EOF
@@ -317,6 +391,7 @@ Options:
   -h, --host=HOST   Connect to the MPD server on HOST, overriding defaults.
   -p, --port=PORT   Connect to the MPD server on PORT, overriding defaults.
   -m, --menu=NAME   Set the name of the generated menu.
+  --album-id=MBID   Generate a menu for the given release MBID.
   -V, --version     Print a version message and then exit.
   -H, --help        Print this message and then exit.
 EOF
@@ -327,9 +402,8 @@ GetOptions(
        '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"; },
+       '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";