]> git.draconx.ca Git - mpdhacks.git/blobdiff - mpdmenu.pl
mpdthumb: Fix failure when readpicture/albumart both return data.
[mpdhacks.git] / mpdmenu.pl
index 241ab8e39076d49d7a07f3502a7c4d2a6b363849..71613c5fb015d8a9e5489999f0934c88b9850454 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/perl
 #
-# Copyright © 2008,2010,2012,2019 Nick Bowler
+# Copyright © 2008,2010,2012,2020-2021 Nick Bowler
 #
 # Silly little script to generate an FVWM menu with various bits of MPD
 # status information and controls.
@@ -18,12 +18,14 @@ 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 max);
 use FindBin;
 
+use lib "$FindBin::Bin";
+use MPDHacks;
+
 use constant {
        MPD_MJR_MIN => 0,
        MPD_MNR_MIN => 21,
@@ -33,47 +35,19 @@ use constant {
 my $SELF = "$FindBin::Bin/$FindBin::Script";
 
 my $MUSIC = $ENV{MUSIC}    // "/srv/music";
-my $host  = $ENV{MPD_HOST} // "localhost";
-my $port  = $ENV{MPD_PORT} // "6600";
-my $sock;
+my ($sock, $mpd_have_binarylimit);
 
-my ($albumid, $artist, $title);
-my $menu;
+my ($albumid, $albumname, $trackid, $recordingid);
+my ($topmenu, $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";
-}
+my %artistids;
 
 sub fvwm_cmd_unquoted {
        print join(' ', @_), "\n";
 }
 
 sub fvwm_cmd {
-       fvwm_cmd_unquoted(map { escape } @_);
+       fvwm_cmd_unquoted(map { MPD::escape } @_);
 }
 
 # Quotes the argument in such a way that it is passed unadulterated by
@@ -111,7 +85,7 @@ sub fvwm_label_escape {
 sub make_submenu {
        my $name = shift;
        $name =~ s/-/_/g;
-       unshift @_, ("exec", $SELF, "--menu=$name");
+       unshift @_, ("exec", $SELF, "--topmenu=$topmenu", "--menu=$name");
 
         fvwm_cmd("DestroyFunc", "Make$name");
         fvwm_cmd("AddToFunc", "Make$name");
@@ -119,14 +93,14 @@ sub make_submenu {
 
         fvwm_cmd("DestroyMenu", $name);
         fvwm_cmd("AddToMenu", $name, "DynamicPopupAction", "Make$name");
-        fvwm_cmd("AddToFunc", "KillMenuMPD", "I", "DestroyMenu", $name);
+        fvwm_cmd("AddToFunc", "Kill$topmenu", "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");
+        fvwm_cmd("AddToFunc", "Kill$topmenu", "I", "DestroyFunc", "Make$name");
 
        return ("Popup", $name);
 }
@@ -160,6 +134,13 @@ sub get_item_thumbnails {
                $c = "%";
        }
 
+       if ($mpd_have_binarylimit) {
+               # --embedded implies and requires binarylimit support
+               push @opts, "--embedded";
+       } else {
+               push @opts, "--no-binarylimit";
+       }
+
        open THUMB, "-|", "$FindBin::Bin/mpdthumb.sh", @opts, "--", @_;
        foreach (@_) {
                my $thumb = <THUMB>;
@@ -242,8 +223,8 @@ sub top_track_cover {
                my $cover = mpd_cover_filename($file);
 
                $cover = fvwm_shell_literal($cover // $file);
-               fvwm_cmd_unquoted("AddToMenu", escape($menu),
-                                 escape($entry->{thumb}),
+               fvwm_cmd_unquoted("AddToMenu", MPD::escape($menu),
+                                 MPD::escape($entry->{thumb}),
                                  "Exec", "exec", "geeqie", $cover);
        }
 }
@@ -251,9 +232,16 @@ sub top_track_cover {
 # Generate the "Title:" entry in the top menu.
 sub top_track_title {
        my ($entry) = @_;
+       my @submenu;
 
-       my @submenu = make_submenu("$menu-TopTrack",
-                                  "--title=$entry->{Title}");
+       my ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_RELEASETRACKID");
+       if ($mbid) {
+               @submenu = make_submenu("$menu-$mbid", "--track-id=$mbid")
+       } else {
+               ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID");
+               @submenu = make_submenu("$menu-track-$mbid", "--recording-id=$mbid")
+                       if ($mbid);
+       }
 
        fvwm_cmd("AddToMenu", $menu,
                 fvwm_label_escape("Title:\t$entry->{Title}"),
@@ -263,9 +251,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,15 +270,142 @@ sub top_track_artist {
 sub top_track_album {
        my ($entry) = @_;
        my @submenu;
+       my $mbid;
 
-       my ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_ALBUMID");
-       @submenu = make_submenu("$menu-$mbid", "--album-id=$mbid") if $mbid;
+       if (($mbid) = get_track_metadata($entry, "MUSICBRAINZ_ALBUMID")) {
+               @submenu = make_submenu("$menu-$mbid", "--album-id=$mbid");
+       } elsif (($mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID")) {
+               # Standalone recording
+               my @a = get_track_metadata($entry, "MUSICBRAINZ_ARTISTID");
+               my ($album) = get_track_metadata($entry, "Album");
+
+               @submenu = make_submenu("$menu-$mbid", "--album-name=$album",
+                                       map { "--artist-id=$_" } @a);
+
+       }
 
        fvwm_cmd("AddToMenu", $menu,
                 fvwm_label_escape("Album:\t$entry->{Album}"),
                 @submenu);
 }
 
+# Generate the "MusicBrainz:" entry in the top menu.
+sub top_track_musicbrainz {
+       my ($entry) = @_;
+       my ($track_mbid, $recording_mbid, $release_mbid);
+       my @artist_mbids;
+       my $label = "MB:";
+       my %idmap;
+
+       ($track_mbid) = get_track_metadata($entry, "MUSICBRAINZ_RELEASETRACKID");
+       ($recording_mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID");
+       ($release_mbid) = get_track_metadata($entry, "MUSICBRAINZ_ALBUMID");
+       @artist_mbids = get_track_metadata($entry, "MUSICBRAINZ_ARTISTID");
+       return unless $track_mbid // $recording_mbid
+                  // $release_mbid // @artist_mbids;
+
+       foreach (get_track_metadata($entry, "Comment")) {
+               $idmap{$1} = $2 if /^([^=]*)=(.*) \(idmap\)$/;
+       }
+
+       fvwm_cmd("AddToMenu", $menu, "", "Nop");
+       if ($track_mbid) {
+               fvwm_cmd("AddToMenu", $menu, "$label\tShow track",
+                       "Exec", "exec", "xdg-open",
+                       "https://musicbrainz.org/track/$track_mbid");
+               $label = "";
+       } elsif ($recording_mbid) {
+               fvwm_cmd("AddToMenu", $menu, "$label\tShow recording",
+                       "Exec", "exec", "xdg-open",
+                       "https://musicbrainz.org/recording/$recording_mbid");
+               $label = "";
+       } elsif ($release_mbid) {
+               fvwm_cmd("AddToMenu", $menu, "$label\tShow",
+                       "Exec", "exec", "xdg-open",
+                       "https://musicbrainz.org/release/$release_mbid");
+               $label = "";
+       }
+
+       foreach my $mbid (@artist_mbids) {
+               my $name = " $idmap{$mbid}" if $idmap{$mbid};
+
+               fvwm_cmd("AddToMenu", $menu, "$label\tShow artist$name",
+                       "Exec", "exec", "xdg-open",
+                       "https://musicbrainz.org/artist/$mbid");
+               $label = "";
+       }
+}
+
+# Given a work MBID, return a hash reference containing all tracks
+# linked to that work.  The hash keys are filenames.
+sub get_tracks_by_work_mbid {
+       my %matches;
+       my $entry;
+
+       foreach my $mbid (@_) {
+               MPD::exec("search", "(MUSICBRAINZ_WORKID == \"$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 track MBID, return a hash reference containing all "related"
+# tracks in the MPD database.  The hash keys are filenames.
+#
+# Currently tracks are considered "related" if their associated recordings
+# have at least one work in common.
+sub get_tracks_by_track_mbid {
+       my ($mbid, $tagname) = (@_, "MUSICBRAINZ_RELEASETRACKID");
+       my %source;
+       my %matches;
+       my $entry;
+
+       return \%matches unless ($mbid);
+       MPD::exec("search", "($tagname == \"$mbid\")");
+       while (<$sock>) {
+               last if (/^OK/);
+               die($_) if (/^ACK/);
+
+               if (/^(\w+): (.*)$/) {
+                       add_track_metadata(\%source, $1, $2);
+               }
+       }
+
+       # Always include the current track
+       $matches{$source{file}} = \%source;
+
+       # Find all tracks related by work
+       foreach my $mbid (get_track_metadata(\%source, "MUSICBRAINZ_WORKID")) {
+               my $related = get_tracks_by_work_mbid($mbid);
+               foreach (keys %$related) {
+                       $matches{$_} //= $related->{$_};
+               }
+       }
+
+       return \%matches;
+}
+
+sub get_tracks_by_recording_mbid {
+       return get_tracks_by_track_mbid($_[0], "MUSICBRAINZ_TRACKID");
+}
+
 # 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 {
@@ -293,7 +414,7 @@ sub get_tracks_by_release_mbid {
        my $entry;
 
        return \%matches unless ($mbid);
-       mpd_exec("search", "(MUSICBRAINZ_ALBUMID == \"$mbid\")");
+       MPD::exec("search", "(MUSICBRAINZ_ALBUMID == \"$mbid\")");
        while (<$sock>) {
                last if (/^OK/);
                die($_) if (/^ACK/);
@@ -315,13 +436,60 @@ sub get_tracks_by_release_mbid {
        return \%matches;
 }
 
+# Insert the given entry into the referenced hash if it represents a
+# standalone recording (not associated with a release).  The recording
+# MBID is used as the hash key.
+sub check_standalone {
+       my ($outhash, $entry) = @_;
+       my ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID");
+
+       return if exists $entry->{MUSICBRAINZ_ALBUMID};
+       $outhash->{$mbid} = $entry if ($mbid);
+}
+
+# Given an artist MBID, return a list of two hash refererences.  The
+# first contains the associated releases in the MPD database and the
+# hash keys are release MBIDs.  The second contains the artist's
+# standalone recordings and the hash keys are recording MBIDs.
+#
+# In scalar context only the release hash is returned.
+#
+# 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, %standalones);
+       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") {
+                                       check_standalone(\%standalones, $entry);
+                                       $entry = {};
+                               } elsif ($1 eq "MUSICBRAINZ_ALBUMID") {
+                                       $releases{$2} //= $entry;
+                               }
+
+                               add_track_metadata($entry, $1, $2);
+                       }
+               }
+               check_standalone(\%standalones, $entry);
+       }
+
+       return wantarray ? (\%releases, values %standalones) : \%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);
+       MPD::exec("playlistfind", "file", $file);
        while (<$sock>) {
                last if (/^OK/);
                die($_) if (/^ACK/);
@@ -334,6 +502,24 @@ sub get_ids_by_filename {
        return @results;
 }
 
+sub update_entry_ids {
+       my @notqueued = ();
+
+       foreach my $entry (@_) {
+               unless (exists $entry->{Id}) {
+                       my ($id) = get_ids_by_filename($entry->{file});
+                       if (defined $id) {
+                               $entry->{Id} = $id;
+                       } else {
+                               push @notqueued, $entry;
+                               next;
+                       }
+               }
+       }
+
+       return @notqueued;
+}
+
 # albumsort(matches, a, b)
 #
 # Sort hash keys (a, b) by disc/track number for album menus.
@@ -367,8 +553,8 @@ sub menu_trackname {
 
 sub print_version {
        print <<EOF
-mpdmenu.pl 0.8
-Copyright © 2019 Nick Bowler
+mpdmenu.pl 0.9
+Copyright © 2021 Nick Bowler
 License GPLv3+: GNU General Public License version 3 or any later version.
 This is free software: you are free to change and redistribute it.
 There is NO WARRANTY, to the extent permitted by law.
@@ -376,10 +562,10 @@ EOF
 }
 
 sub print_usage {
-       my $fh = $_[1] // *STDERR;
+       my ($fh) = (@_, *STDERR);
 
        print $fh "Usage: $0 [options]\n";
-       print "Try $0 --help for more information.\n" unless (@_ > 0);
+       print $fh "Try $0 --help for more information.\n" unless (@_ > 0);
 }
 
 sub print_help {
@@ -392,40 +578,53 @@ Options:
   -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.
+  --album-name=NAME
+                    Generate a menu for standalone tracks with the given
+                   "album" NAME.  An artist MBID must be supplied.
+  --artist-id=MBID  Generate a menu for the given artist MBID.
+  --track-id=MBID   Generate a menu for the given track MBID.
+  --recording-id=MBID
+                    Generate a menu for the given recording MBID.
   -V, --version     Print a version message and then exit.
   -H, --help        Print this message and then exit.
 EOF
 }
 
 GetOptions(
-       'host|h=s'   => \$host,
-       'port|p=s'   => \$port,
-       'menu|m=s'   => \$menu,
+       'host|h=s'       => \$MPD::host,
+       'port|p=s'       => \$MPD::port,
+       'menu|m=s'       => \$menu,
+
+       'artist-id=s'    => sub { $artistids{$_[1]} = 1; $mode = "artist"; },
+       'album-id=s'     => sub { $albumid = $_[1]; $mode = "album"; },
+       'album-name=s'   => sub { $albumname = $_[1]; $mode = "albumname"; },
+       'track-id=s'     => sub { $trackid = $_[1]; $mode = "track"; },
+       'recording-id=s' => sub { $recordingid = $_[1]; $mode = "recording"; },
 
-       '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 },
+       'H|help'         => sub { print_help(); exit },
 
-       'V|version'  => sub { print_version(); exit },
-       'H|help'     => sub { print_help(); exit },
+       'topmenu=s'      => \$topmenu, # top menu name (for submenu generation)
 ) or do { print_usage; exit 1 };
 
+$mode = "albumname" if ($albumname && $mode eq "artist");
+
+unless (defined $menu) {
+       $topmenu //= "MenuMPD";
+       $menu = $topmenu . ($mode ne "top" ? $mode : "");
+}
+$topmenu //= $menu;
+
 # Connect to MPD.
-$sock = new IO::Socket::INET6(
-       PeerAddr => $host,
-       PeerPort => $port,
-       Proto => 'tcp',
-       Timeout => 2
-) or die("could not open socket: $!.\n");
-binmode($sock, ":utf8");
-
-die("could not connect to MPD: $!.\n")
-       if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/));
-
-die("MPD version $1.$2.$3 insufficient.\n")
-       if (  ($1 <  MPD_MJR_MIN)
-          || ($1 == MPD_MJR_MIN && $2 <  MPD_MNR_MIN)
-          || ($1 == MPD_MJR_MIN && $2 == MPD_MNR_MIN && $3 < MPD_REV_MIN));
+$sock = MPD::connect();
+die("MPD version $MPD::major.$MPD::minor.$MPD::revision insufficient.")
+       unless MPD::min_version(MPD_MJR_MIN, MPD_MNR_MIN, MPD_REV_MIN);
+
+MPD::exec("binarylimit", 64);
+while (<$sock>) {
+       $mpd_have_binarylimit = 1 if /^OK/;
+       last if /^OK/ or /^ACK/;
+}
 
 if ($mode eq "top") {
        my %current;
@@ -433,7 +632,7 @@ if ($mode eq "top") {
 
        $menu //= "MenuMPD";
 
-       mpd_exec("status");
+       MPD::exec("status");
        while (<$sock>) {
                last if (/^OK/);
                die($_) if (/^ACK/);
@@ -443,7 +642,7 @@ if ($mode eq "top") {
                }
        }
 
-       mpd_exec("currentsong");
+       MPD::exec("currentsong");
        while (<$sock>) {
                last if (/^OK/);
                die($_) if (/^ACK/);
@@ -464,6 +663,7 @@ if ($mode eq "top") {
                top_track_title(\%current);
                top_track_artist(\%current);
                top_track_album(\%current);
+               top_track_musicbrainz(\%current);
        } else {
                fvwm_cmd("AddToMenu", $menu, "[current track unavailable]");
        }
@@ -538,146 +738,111 @@ if ($mode eq "top") {
        }
 } elsif ($mode eq "artist") {
        # Create an artist menu.
-       my %matches;
-       my $entry;
+       my ($matches, @recs) = get_releases_by_artist_mbid(keys %artistids);
 
        $menu //= "MenuMPDArtist";
 
-       mpd_exec("playlistfind", "artist", $artist);
-       while (<$sock>) {
-               last if (/^OK/);
-               die($_) if (/^ACK/);
+       my @mbids = sort { datesort($matches, $a, $b) } keys %$matches;
+       my @files = map { $matches->{$_}->{file} } @mbids;
+       my @thumbs = get_item_thumbnails({ small => 1 }, @files);
 
-               if (/^(\w+): (.*)$/) {
-                       $entry = {} if ($1 eq "file");
-                       $matches{$2} //= $entry if ($1 eq "MUSICBRAINZ_ALBUMID");
-                       add_track_metadata($entry, $1, $2);
-               }
+       unless (@mbids) {
+               fvwm_cmd("AddToMenu", $menu, "No releases found", "Title")
        }
 
-       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 $mbid (@mbids) {
-               my $entry = $matches{$mbid};
+               my $entry = $matches->{$mbid};
                my $thumb = shift @thumbs;
 
-               my @submenu = make_submenu("$menu-$mbid", "--album-id=$mbid");
+               my @submenu = make_submenu("$topmenu-$mbid",
+                                          "--album-id=$mbid");
                fvwm_cmd("AddToMenu", $menu,
                         $thumb . fvwm_label_escape($entry->{Album}),
-                        @submenu);
+                        @submenu);
        }
-} elsif ($mode eq "track") {
-       # Create a title menu.
-       my @titles;
-       my $entry;
 
-       $menu = "MenuMPDTitle" unless defined $menu;
+       my @artists = map { "--artist-id=$_" } keys %artistids;
+       my %nonalbums = map { $_->{Album} => $_ } @recs;
+       foreach my $name (sort keys %nonalbums) {
+               my $mbid = $nonalbums{$name}->{MUSICBRAINZ_TRACKID};
+               my @submenu = make_submenu("$topmenu-$mbid", @artists,
+                                          "--album-name=$name");
+               fvwm_cmd("AddToMenu", $menu, fvwm_label_escape($name), @submenu);
+       }
+} elsif ($mode eq "albumname") {
+       # Create a standalone recordings menu
+       my ($releases, @recs) = get_releases_by_artist_mbid(keys %artistids);
 
-       # Open and close brackets.
-       my ($ob, $cb) = ("[\[~〜<〈(ー−-]", "[\]~〜>〉)ー−-]");
+       $menu //= "MenuMPDRecordings";
+       my @tracks = sort { $a->{Title} cmp $b->{Title} }
+                    grep { $_->{Album} eq $albumname } @recs;
 
-       $_ = $title;
+       # Show thumbnails for standalone recordings
+       my @thumbs = get_item_thumbnails({ small => 1 },
+                                        map { $_->{file} } @tracks);
+       foreach my $entry (@tracks) {
+               $entry->{thumb} = shift @thumbs;
+       }
 
-       # Deal with specific cases.
-       s/ちいさな(?=ヘミソフィア)//;                 # ヘミソフィア
-       s/ "mix on air flavor" dear EIKO SHIMAMIYA//; # Spiral wind
-       s/ "So,you need me" Style//;                  # I need you
-       s/ ::Symphony Second movement:://;            # Disintegration
-       s/-\[instrumental\]//;                        # 青い果実
-       s/ -Practice Track-//;                        # Fair Heaven
-       s/〜世界で一番アナタが好き〜//;               # Pure Heart
-       s/〜彼方への哀歌//;                           # 十二幻夢
-       s/ sora no uta ver.//;                       # 美しい星
+       my @notqueued = update_entry_ids(@tracks);
 
-       s/\s*-remix-$//; # Otherwise "D-THREAD -remix-" doesn't work right.
+       fvwm_cmd("AddToMenu", $menu);
+       fvwm_cmd("+", "No tracks found", "Title") unless @tracks;
 
-       # Deal with titles like "blah (ABC version)".
-       s/\s*$ob.*(style|mix|edit|edition|ver\.?|version|melody|カラオケ)$cb?$//i;
+       foreach my $entry (@tracks) {
+               next unless exists $entry->{Id};
 
-       # Deal with titles like "blah (without XYZ)".
-       s/\s*$ob\s*((e\.)?piano|english|japanese|inst|tv|without|w\/o|off|back|short|karaoke|game).*//i;
+               fvwm_cmd("+", menu_trackname($entry), "Exec",
+                        "exec", "$FindBin::Bin/mpdexec.pl",
+                        "playid", $entry->{Id});
+       }
 
-       # Deal with titles like "blah instrumental".
-       s/\s+(instrumental|off vocal|short|tv)([\s-]+(mix|size|version))?$//i;
-       s/\s+without\s+\w+$//i;
+       fvwm_cmd("+", "Not in play queue", "Title") if @notqueued;
+       foreach my $entry (@notqueued) {
+               fvwm_cmd("+", menu_trackname($entry));
+       }
+} elsif ($mode eq "track" || $mode eq "recording") {
+       my ($matches, $mbid);
+       my @notqueued;
 
-       # Deal with separate movements in classical pieces.
-       s/: [IVX]+\..*//;
+       if ($mode eq "track") {
+               $matches = get_tracks_by_track_mbid($trackid)
+       } else {
+               $matches = get_tracks_by_recording_mbid($recordingid)
+       }
 
-       my $basetitle  = $_;
-       my $_basetitle = $basetitle;
+       $menu //= "MenuMPDTrack";
+       fvwm_cmd("DestroyMenu", $menu);
 
-       $_basetitle =~ s/"/\\"/g;
-       print $sock "playlistsearch title \"$_basetitle\"\n";
-       while (<$sock>) {
-               last if (/^OK/);
-               die($_) if (/^ACK/);
+       my @files = sort { datesort($matches, $a, $b) } keys %$matches;
+       my @thumbs = get_item_thumbnails({ small => 1 }, @files);
 
-               if (/^(\w+): (.*)$/) {
-                       if ($1 eq "file") {
-                               push @titles, $entry if (keys(%$entry) > 0);
-                               $entry = {};
-                       }
+       fvwm_cmd("AddToMenu", $menu);
+       fvwm_cmd("+", "No tracks found", "Title") unless @files;
+       foreach my $file (@files) {
+               my $entry = $matches->{$file};
+               $entry->{thumb} = shift @thumbs;
 
-                       $entry->{$1} = $2;
+               unless (exists $entry->{Id}) {
+                       my ($id) = get_ids_by_filename($file);
+                       if (defined $id) {
+                               $entry->{Id} = $id;
+                       } else {
+                               push @notqueued, $entry;
+                               next;
+                       }
                }
-       }
-       push @titles, $entry if (keys(%$entry) > 0);
 
-{ # work around 'use locale' breaking s///i
-       use locale;
-
-       my @thumbs = get_item_thumbnails({ small => 1 },
-                                         map { $_->{file} } @titles);
-       for (my $i = 0; $i < @titles; $i++) {
-               $titles[$i]->{thumb} = $thumbs[$i];
+               fvwm_cmd("+", menu_trackname($entry), "Exec",
+                        "exec", "$FindBin::Bin/mpdexec.pl",
+                        "playid", $entry->{Id});
        }
 
-       foreach (sort titlesort @titles) {
-               my ($t_file, $t_artist, $t_title, $t_id, $thumb) = (
-                       $_->{file},
-                       $_->{Artist},
-                       $_->{Title},
-                       $_->{Id},
-                       $_->{thumb}
-               );
-
-               # MPD searches are case-insensitive.
-               next if (!($t_title =~ m/(\P{Latin}|^)\Q$basetitle\E(\P{Latin}|$)/ || $t_title =~ m/\Q$basetitle\E/i));
-
-               $t_artist = sanitise($t_artist, 1);
-               $t_title  = sanitise($t_title, 1);
-
-               cmd("AddToMenu $menu \"$thumb$t_artist - $t_title\""
-                   ." Exec exec $FindBin::Bin/mpdexec.pl"
-                   ." playid $t_id");
+       fvwm_cmd("+", "Not in play queue", "Title") if @notqueued;
+       foreach my $entry (@notqueued) {
+               fvwm_cmd("+", menu_trackname($entry));
        }
-} # end use locale workaround
 }
 
 # Finished.
 print $sock "close\n";
-
-sub sanitise
-{
-       $_ = $_[0];
-       s/&/&&/g if ($_[1]);
-       s/([\$@%^*])/\1\1/g;
-       s/"/\\"/g;
-       return $_;
-}
-
-sub titlesort
-{
-       return ($a->{Album}  cmp $b->{Album})  if($a->{Album}  ne $b->{Album});
-       return ($a->{Artist} cmp $b->{Artist}) if($a->{Artist} ne $b->{Artist});
-       return ($a->{Title}  cmp $b->{Title});
-}
-
-sub cmd
-{
-       print "$_[0]\n";
-}