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 {
my $port = $ENV{MPD_PORT} // "6600";
my $sock;
+my ($albumid, $artist, $title);
my $menu;
my $mode = "top";
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
-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
'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 },
"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;
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});
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";