]> git.draconx.ca Git - mpdhacks.git/blobdiff - mpdmenu.pl
mpdmenu: Use MBIDs for track matching instead of name matching.
[mpdhacks.git] / mpdmenu.pl
index 241ab8e39076d49d7a07f3502a7c4d2a6b363849..8e597e7f9c704c3b585ecd29043aace22fe61b35 100755 (executable)
@@ -37,7 +37,8 @@ my $host  = $ENV{MPD_HOST} // "localhost";
 my $port  = $ENV{MPD_PORT} // "6600";
 my $sock;
 
-my ($albumid, $artist, $title);
+my ($albumid, $trackid);
+my %artistids;
 my $menu;
 my $mode = "top";
 
@@ -251,9 +252,10 @@ 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");
+       @submenu = make_submenu("$menu-$mbid", "--track-id=$mbid") if $mbid;
 
        fvwm_cmd("AddToMenu", $menu,
                 fvwm_label_escape("Title:\t$entry->{Title}"),
@@ -263,9 +265,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}"),
@@ -285,6 +293,72 @@ sub top_track_album {
                 @submenu);
 }
 
+# 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) = @_;
+       my %source;
+       my %matches;
+       my $entry;
+
+       return \%matches unless ($mbid);
+       mpd_exec("search", "(MUSICBRAINZ_RELEASETRACKID == \"$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;
+}
+
 # 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 {
@@ -315,6 +389,36 @@ sub get_tracks_by_release_mbid {
        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 {
@@ -392,22 +496,24 @@ 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.
+  --artist-id=MBID  Generate a menu for the given artist MBID.
+  --track-id=MBID   Generate a menu for the given track 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'    => \$host,
+       'port|p=s'    => \$port,
+       'menu|m=s'    => \$menu,
 
-       'album-id=s' => sub { $albumid = $_[1]; $mode = "album"; },
-       'artist=s'   => sub { $artist = $_[1]; $mode = "artist"; },
-       'title=s'    => sub { $title = $_[1]; $mode = "track"; },
+       'artist-id=s' => sub { $artistids{$_[1]} = 1; $mode = "artist"; },
+       'album-id=s'  => sub { $albumid = $_[1]; $mode = "album"; },
+       'track-id=s'  => sub { $trackid = $_[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.
@@ -538,30 +644,18 @@ if ($mode eq "top") {
        }
 } elsif ($mode eq "artist") {
        # Create an artist menu.
-       my %matches;
+       my $matches = get_releases_by_artist_mbid(keys %artistids);
        my $entry;
 
        $menu //= "MenuMPDArtist";
 
-       mpd_exec("playlistfind", "artist", $artist);
-       while (<$sock>) {
-               last if (/^OK/);
-               die($_) if (/^ACK/);
-
-               if (/^(\w+): (.*)$/) {
-                       $entry = {} if ($1 eq "file");
-                       $matches{$2} //= $entry if ($1 eq "MUSICBRAINZ_ALBUMID");
-                       add_track_metadata($entry, $1, $2);
-               }
-       }
-
-       my @mbids = sort { datesort(\%matches, $a, $b) } keys %matches;
-       my @files = map { $matches{$_}->{file} } @mbids;
+       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");
@@ -570,114 +664,40 @@ if ($mode eq "top") {
                         @submenu);
        }
 } elsif ($mode eq "track") {
-       # Create a title menu.
-       my @titles;
-       my $entry;
-
-       $menu = "MenuMPDTitle" unless defined $menu;
-
-       # Open and close brackets.
-       my ($ob, $cb) = ("[\[~〜<〈(ー−-]", "[\]~〜>〉)ー−-]");
-
-       $_ = $title;
-
-       # 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 $matches = get_tracks_by_track_mbid($trackid);
+       my @notqueued;
 
-       s/\s*-remix-$//; # Otherwise "D-THREAD -remix-" doesn't work right.
+       $menu //= "MenuMPDTrack";
 
-       # Deal with titles like "blah (ABC version)".
-       s/\s*$ob.*(style|mix|edit|edition|ver\.?|version|melody|カラオケ)$cb?$//i;
-
-       # 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;
-
-       # Deal with titles like "blah instrumental".
-       s/\s+(instrumental|off vocal|short|tv)([\s-]+(mix|size|version))?$//i;
-       s/\s+without\s+\w+$//i;
-
-       # Deal with separate movements in classical pieces.
-       s/: [IVX]+\..*//;
-
-       my $basetitle  = $_;
-       my $_basetitle = $basetitle;
+       my @files = sort { datesort($matches, $a, $b) } keys %$matches;
+       my @thumbs = get_item_thumbnails({ small => 1 }, @files);
 
-       $_basetitle =~ s/"/\\"/g;
-       print $sock "playlistsearch title \"$_basetitle\"\n";
-       while (<$sock>) {
-               last if (/^OK/);
-               die($_) if (/^ACK/);
+       fvwm_cmd("AddToMenu", $menu);
+       fvwm_cmd("+", "No tracks found", "Title") unless @files;
+       foreach my $file (@files) {
+               my $entry = $matches->{$file};
+               $entry->{thumb} = shift @thumbs;
 
-               if (/^(\w+): (.*)$/) {
-                       if ($1 eq "file") {
-                               push @titles, $entry if (keys(%$entry) > 0);
-                               $entry = {};
+               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;
                }
-       }
-       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";
-}