]> git.draconx.ca Git - mpdhacks.git/blob - mpdmenu.pl
mpdthumb: Fix failure when readpicture/albumart both return data.
[mpdhacks.git] / mpdmenu.pl
1 #!/usr/bin/perl
2 #
3 # Copyright © 2008,2010,2012,2020-2021 Nick Bowler
4 #
5 # Silly little script to generate an FVWM menu with various bits of MPD
6 # status information and controls.
7 #
8 # License GPLv3+: GNU General Public License version 3 or any later version.
9 # This is free software: you are free to change and redistribute it.
10 # There is NO WARRANTY, to the extent permitted by law.
11
12 use strict;
13
14 use utf8;
15
16 use Encode qw(decode encode);
17 use Encode::Locale qw(decode_argv);
18 decode_argv(Encode::FB_CROAK);
19 binmode(STDOUT, ":utf8");
20
21 use Getopt::Long qw(:config gnu_getopt);
22 use Scalar::Util qw(reftype);
23 use List::Util qw(any max);
24 use FindBin;
25
26 use lib "$FindBin::Bin";
27 use MPDHacks;
28
29 use constant {
30         MPD_MJR_MIN => 0,
31         MPD_MNR_MIN => 21,
32         MPD_REV_MIN => 0,
33 };
34
35 my $SELF = "$FindBin::Bin/$FindBin::Script";
36
37 my $MUSIC = $ENV{MUSIC}    // "/srv/music";
38 my ($sock, $mpd_have_binarylimit);
39
40 my ($albumid, $albumname, $trackid, $recordingid);
41 my ($topmenu, $menu);
42 my $mode = "top";
43 my %artistids;
44
45 sub fvwm_cmd_unquoted {
46         print join(' ', @_), "\n";
47 }
48
49 sub fvwm_cmd {
50         fvwm_cmd_unquoted(map { MPD::escape } @_);
51 }
52
53 # Quotes the argument in such a way that it is passed unadulterated by
54 # both FVWM and the shell to a command as a single argument (for use as
55 # an # argument for e.g., the Exec or PipeRead FVWM commands).
56 #
57 # The result must be used with fvwm_cmd_unquoted;
58 sub fvwm_shell_literal {
59         my $s = @_[0] // $_;
60
61         $s =~ s/\$/\$\$/g;
62         if ($s =~ /[' \t]/) {
63                 $s =~ s/'/'\\''/g;
64                 return "'$s'";
65         }
66         $s =~ s/^\s*$/'$&'/;
67         return "$s";
68 }
69
70 # Escapes metacharacters in the argument used in FVWM menu labels.  The
71 # string must still be quoted (e.g., by using fvwm_cmd).
72 sub fvwm_label_escape {
73         my @tokens = split /\t/, $_[0];
74         @tokens[0] =~ s/&/&&/g;
75         my $ret = join "\t", @tokens;
76         $ret =~ s/[\$@%^*]/$&$&/g;
77         return $ret;
78 }
79
80 # make_submenu(name, [args ...])
81 #
82 # Creates a submenu (with the specified name) constructed by invoking this
83 # script with the given arguments.  Returns a list that can be passed to
84 # fvwm_cmd to display the menu.
85 sub make_submenu {
86         my $name = shift;
87         $name =~ s/-/_/g;
88         unshift @_, ("exec", $SELF, "--topmenu=$topmenu", "--menu=$name");
89
90         fvwm_cmd("DestroyFunc", "Make$name");
91         fvwm_cmd("AddToFunc", "Make$name");
92         fvwm_cmd("+", "I", "DestroyMenu", $name);
93
94         fvwm_cmd("DestroyMenu", $name);
95         fvwm_cmd("AddToMenu", $name, "DynamicPopupAction", "Make$name");
96         fvwm_cmd("AddToFunc", "Kill$topmenu", "I", "DestroyMenu", $name);
97
98         fvwm_cmd("DestroyFunc", "Make$name");
99         fvwm_cmd("AddToFunc", "Make$name");
100         fvwm_cmd("+", "I", "DestroyMenu", $name);
101         fvwm_cmd("+", "I", "-PipeRead",
102                  join(' ', map { fvwm_shell_literal } @_));
103         fvwm_cmd("AddToFunc", "Kill$topmenu", "I", "DestroyFunc", "Make$name");
104
105         return ("Popup", $name);
106 }
107
108 # get_item_thumbnails({ options }, file, ...)
109 # get_item_thumbnails(file, ...)
110 #
111 # For each music file listed, obtain a thumbnail (if any) for the cover art.
112 #
113 # The first argument is a hash reference to control the mode of operation;
114 # it may be omitted for default options.
115 #
116 #   get_item_thumbnails({ small => 1 }, ...) - smaller thumbnails
117 #
118 # The returned list consists of strings (in the same order as the filename
119 # arguments) suitable for use directly in FVWM menus; by default the filename
120 # is bracketed by asterisks (e.g., "*thumbnail.png*"); in small mode it is
121 # surrounded by % (e.g., "%thumbnail.png%").  If no cover art was found, the
122 # empty string is returned for that file.
123 sub get_item_thumbnails {
124         my @results = ();
125         my $flags = {};
126         my @opts = ();
127
128         $flags = shift if (reftype($_[0]) eq "HASH");
129         return @results unless @_;
130
131         my $c = "*";
132         if ($flags->{small}) {
133                 push @opts, "--small";
134                 $c = "%";
135         }
136
137         if ($mpd_have_binarylimit) {
138                 # --embedded implies and requires binarylimit support
139                 push @opts, "--embedded";
140         } else {
141                 push @opts, "--no-binarylimit";
142         }
143
144         open THUMB, "-|", "$FindBin::Bin/mpdthumb.sh", @opts, "--", @_;
145         foreach (@_) {
146                 my $thumb = <THUMB>;
147                 chomp $thumb;
148
149                 $thumb = "$c$thumb$c" if (-f $thumb);
150                 push @results, $thumb;
151         }
152         close THUMB;
153         die("mpdthumb failed") if ($?);
154
155         return @results;
156 }
157
158 # add_track_metadata(hashref, key, value)
159 #
160 # Inserts the given key into the referenced hash; if the key already exists
161 # in the hash then the hash element is converted to an array reference (if
162 # it isn't already) and the value is appended to that array.
163 sub add_track_metadata {
164         my ($entry, $key, $value) = @_;
165
166         if (exists($entry->{$key})) {
167                 my $ref = $entry->{$key};
168
169                 if (reftype($ref) ne "ARRAY") {
170                         return if ($ref eq $value);
171
172                         $ref = [$ref];
173                         $entry->{$key} = $ref;
174                 }
175
176                 push(@$ref, $value) unless (any {$_ eq $value} @$ref);
177         } else {
178                 $entry->{$key} = $value;
179         }
180 }
181
182 # get_track_metadata(hashref, key)
183 #
184 # Return the values associated with the given metadata key as a list.
185 sub get_track_metadata {
186         my ($entry, $key) = @_;
187
188         return () unless (exists($entry->{$key}));
189
190         my $ref = $entry->{$key};
191         return @$ref if (reftype($ref) eq "ARRAY");
192         return $ref
193 }
194
195 # Given a music filename, search for the cover art in the same directory.
196 sub mpd_cover_filename {
197         my ($dir) = @_;
198         my $file;
199
200         $dir =~ s/\/[^\/]*$//;
201         foreach ("cover.png", "cover.jpg", "cover.tiff", "cover.bmp") {
202                 if (-f "$dir/$_") {
203                         $file = "$dir/$_";
204                         last;
205                 }
206         }
207         return unless defined $file;
208
209         # Follow one level of symbolic link to get to the scans directory.
210         $file = readlink($file) // $file;
211         $file = "$dir/$file" unless ($file =~ /^\//);
212         return $file;
213 }
214
215 # Generate the cover art entry in the top menu.
216 sub top_track_cover {
217         my ($entry) = @_;
218
219         ($entry->{thumb}) = get_item_thumbnails($entry->{file});
220         print "$entry->{thumb}\n";
221         if ($entry->{thumb}) {
222                 my $file = "$MUSIC/$entry->{file}";
223                 my $cover = mpd_cover_filename($file);
224
225                 $cover = fvwm_shell_literal($cover // $file);
226                 fvwm_cmd_unquoted("AddToMenu", MPD::escape($menu),
227                                   MPD::escape($entry->{thumb}),
228                                   "Exec", "exec", "geeqie", $cover);
229         }
230 }
231
232 # Generate the "Title:" entry in the top menu.
233 sub top_track_title {
234         my ($entry) = @_;
235         my @submenu;
236
237         my ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_RELEASETRACKID");
238         if ($mbid) {
239                 @submenu = make_submenu("$menu-$mbid", "--track-id=$mbid")
240         } else {
241                 ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID");
242                 @submenu = make_submenu("$menu-track-$mbid", "--recording-id=$mbid")
243                         if ($mbid);
244         }
245
246         fvwm_cmd("AddToMenu", $menu,
247                  fvwm_label_escape("Title:\t$entry->{Title}"),
248                  @submenu);
249 }
250
251 # Generate the "Artist:" entry in the top menu.
252 sub top_track_artist {
253         my ($entry) = @_;
254         my @submenu;
255
256         # TODO: multi-artist tracks should get multiple artist menus; for now
257         # just combine the releases from all artists.
258         my @mbids = get_track_metadata($entry, "MUSICBRAINZ_ARTISTID");
259         if (@mbids) {
260                 @submenu = make_submenu("$menu-TopArtist",
261                                         map { "--artist-id=$_" } @mbids);
262         }
263
264         fvwm_cmd("AddToMenu", $menu,
265                  fvwm_label_escape("Artist:\t$entry->{Artist}"),
266                  @submenu);
267 }
268
269 # Generate the "Album:" entry in the top menu.
270 sub top_track_album {
271         my ($entry) = @_;
272         my @submenu;
273         my $mbid;
274
275         if (($mbid) = get_track_metadata($entry, "MUSICBRAINZ_ALBUMID")) {
276                 @submenu = make_submenu("$menu-$mbid", "--album-id=$mbid");
277         } elsif (($mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID")) {
278                 # Standalone recording
279                 my @a = get_track_metadata($entry, "MUSICBRAINZ_ARTISTID");
280                 my ($album) = get_track_metadata($entry, "Album");
281
282                 @submenu = make_submenu("$menu-$mbid", "--album-name=$album",
283                                         map { "--artist-id=$_" } @a);
284
285         }
286
287         fvwm_cmd("AddToMenu", $menu,
288                  fvwm_label_escape("Album:\t$entry->{Album}"),
289                  @submenu);
290 }
291
292 # Generate the "MusicBrainz:" entry in the top menu.
293 sub top_track_musicbrainz {
294         my ($entry) = @_;
295         my ($track_mbid, $recording_mbid, $release_mbid);
296         my @artist_mbids;
297         my $label = "MB:";
298         my %idmap;
299
300         ($track_mbid) = get_track_metadata($entry, "MUSICBRAINZ_RELEASETRACKID");
301         ($recording_mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID");
302         ($release_mbid) = get_track_metadata($entry, "MUSICBRAINZ_ALBUMID");
303         @artist_mbids = get_track_metadata($entry, "MUSICBRAINZ_ARTISTID");
304         return unless $track_mbid // $recording_mbid
305                    // $release_mbid // @artist_mbids;
306
307         foreach (get_track_metadata($entry, "Comment")) {
308                 $idmap{$1} = $2 if /^([^=]*)=(.*) \(idmap\)$/;
309         }
310
311         fvwm_cmd("AddToMenu", $menu, "", "Nop");
312         if ($track_mbid) {
313                 fvwm_cmd("AddToMenu", $menu, "$label\tShow track",
314                         "Exec", "exec", "xdg-open",
315                         "https://musicbrainz.org/track/$track_mbid");
316                 $label = "";
317         } elsif ($recording_mbid) {
318                 fvwm_cmd("AddToMenu", $menu, "$label\tShow recording",
319                         "Exec", "exec", "xdg-open",
320                         "https://musicbrainz.org/recording/$recording_mbid");
321                 $label = "";
322         } elsif ($release_mbid) {
323                 fvwm_cmd("AddToMenu", $menu, "$label\tShow",
324                         "Exec", "exec", "xdg-open",
325                         "https://musicbrainz.org/release/$release_mbid");
326                 $label = "";
327         }
328
329         foreach my $mbid (@artist_mbids) {
330                 my $name = " $idmap{$mbid}" if $idmap{$mbid};
331
332                 fvwm_cmd("AddToMenu", $menu, "$label\tShow artist$name",
333                         "Exec", "exec", "xdg-open",
334                         "https://musicbrainz.org/artist/$mbid");
335                 $label = "";
336         }
337 }
338
339 # Given a work MBID, return a hash reference containing all tracks
340 # linked to that work.  The hash keys are filenames.
341 sub get_tracks_by_work_mbid {
342         my %matches;
343         my $entry;
344
345         foreach my $mbid (@_) {
346                 MPD::exec("search", "(MUSICBRAINZ_WORKID == \"$mbid\")");
347                 while (<$sock>) {
348                         last if (/^OK/);
349                         die($_) if (/^ACK/);
350
351                         if (/^(\w+): (.*)$/) {
352                                 if ($1 eq "file") {
353                                         if (exists($matches{$2})) {
354                                                 $entry = $matches{$2};
355                                         } else {
356                                                 $entry = {};
357                                                 $matches{$2} = $entry;
358                                         }
359                                 }
360
361                                 add_track_metadata($entry, $1, $2);
362                         }
363                 }
364         }
365
366         return \%matches;
367 }
368
369 # Given a track MBID, return a hash reference containing all "related"
370 # tracks in the MPD database.  The hash keys are filenames.
371 #
372 # Currently tracks are considered "related" if their associated recordings
373 # have at least one work in common.
374 sub get_tracks_by_track_mbid {
375         my ($mbid, $tagname) = (@_, "MUSICBRAINZ_RELEASETRACKID");
376         my %source;
377         my %matches;
378         my $entry;
379
380         return \%matches unless ($mbid);
381         MPD::exec("search", "($tagname == \"$mbid\")");
382         while (<$sock>) {
383                 last if (/^OK/);
384                 die($_) if (/^ACK/);
385
386                 if (/^(\w+): (.*)$/) {
387                         add_track_metadata(\%source, $1, $2);
388                 }
389         }
390
391         # Always include the current track
392         $matches{$source{file}} = \%source;
393
394         # Find all tracks related by work
395         foreach my $mbid (get_track_metadata(\%source, "MUSICBRAINZ_WORKID")) {
396                 my $related = get_tracks_by_work_mbid($mbid);
397                 foreach (keys %$related) {
398                         $matches{$_} //= $related->{$_};
399                 }
400         }
401
402         return \%matches;
403 }
404
405 sub get_tracks_by_recording_mbid {
406         return get_tracks_by_track_mbid($_[0], "MUSICBRAINZ_TRACKID");
407 }
408
409 # Given a release MBID, return a hash reference containing all its
410 # associated tracks in the MPD database.  The hash keys are filenames.
411 sub get_tracks_by_release_mbid {
412         my ($mbid) = @_;
413         my %matches;
414         my $entry;
415
416         return \%matches unless ($mbid);
417         MPD::exec("search", "(MUSICBRAINZ_ALBUMID == \"$mbid\")");
418         while (<$sock>) {
419                 last if (/^OK/);
420                 die($_) if (/^ACK/);
421
422                 if (/^(\w+): (.*)$/) {
423                         if ($1 eq "file") {
424                                 if (exists($matches{$2})) {
425                                         $entry = $matches{$2};
426                                 } else {
427                                         $entry = {};
428                                         $matches{$2} = $entry;
429                                 }
430                         }
431
432                         add_track_metadata($entry, $1, $2);
433                 }
434         }
435
436         return \%matches;
437 }
438
439 # Insert the given entry into the referenced hash if it represents a
440 # standalone recording (not associated with a release).  The recording
441 # MBID is used as the hash key.
442 sub check_standalone {
443         my ($outhash, $entry) = @_;
444         my ($mbid) = get_track_metadata($entry, "MUSICBRAINZ_TRACKID");
445
446         return if exists $entry->{MUSICBRAINZ_ALBUMID};
447         $outhash->{$mbid} = $entry if ($mbid);
448 }
449
450 # Given an artist MBID, return a list of two hash refererences.  The
451 # first contains the associated releases in the MPD database and the
452 # hash keys are release MBIDs.  The second contains the artist's
453 # standalone recordings and the hash keys are recording MBIDs.
454 #
455 # In scalar context only the release hash is returned.
456 #
457 # Since MPD returns results on a per-track basis, each entry in the
458 # hash has the metadata for one unspecified track from that release.
459 sub get_releases_by_artist_mbid {
460         my (%releases, %standalones);
461         my $entry;
462
463         foreach my $mbid (@_) {
464                 MPD::exec("search", "(MUSICBRAINZ_ARTISTID == \"$mbid\")");
465                 while (<$sock>) {
466                         last if (/^OK/);
467                         die($_) if (/^ACK/);
468
469                         if (/^(\w+): (.*)$/) {
470                                 if ($1 eq "file") {
471                                         check_standalone(\%standalones, $entry);
472                                         $entry = {};
473                                 } elsif ($1 eq "MUSICBRAINZ_ALBUMID") {
474                                         $releases{$2} //= $entry;
475                                 }
476
477                                 add_track_metadata($entry, $1, $2);
478                         }
479                 }
480                 check_standalone(\%standalones, $entry);
481         }
482
483         return wantarray ? (\%releases, values %standalones) : \%releases;
484 }
485
486 # Given a filename, return the IDs (if any) for that file in the
487 # current MPD play queue.
488 sub get_ids_by_filename {
489         my ($file) = @_;
490         my @results = ();
491
492         MPD::exec("playlistfind", "file", $file);
493         while (<$sock>) {
494                 last if (/^OK/);
495                 die($_) if (/^ACK/);
496
497                 if (/^(\w+): (.*)$/) {
498                         push @results, $2 if ($1 eq "Id");
499                 }
500         }
501
502         return @results;
503 }
504
505 sub update_entry_ids {
506         my @notqueued = ();
507
508         foreach my $entry (@_) {
509                 unless (exists $entry->{Id}) {
510                         my ($id) = get_ids_by_filename($entry->{file});
511                         if (defined $id) {
512                                 $entry->{Id} = $id;
513                         } else {
514                                 push @notqueued, $entry;
515                                 next;
516                         }
517                 }
518         }
519
520         return @notqueued;
521 }
522
523 # albumsort(matches, a, b)
524 #
525 # Sort hash keys (a, b) by disc/track number for album menus.
526 sub albumsort {
527         my ($matches, $a, $b) = @_;
528
529         return $matches->{$a}->{Disc} <=> $matches->{$b}->{Disc}
530             || $matches->{$a}->{Track} <=> $matches->{$b}->{Track}
531             || $a cmp $b;
532 }
533
534 # datesort(matches, a, b)
535 #
536 # Sort hash keys (a, b) by release date
537 sub datesort {
538         my ($matches, $a, $b) = @_;
539
540         return $matches->{$a}->{Date} cmp $matches->{$b}->{Date}
541             || $a cmp $b;
542 }
543
544 # menu_trackname(entry)
545 #
546 # Format the track name for display in an FVWM menu, where entry
547 # is a hash reference containing the track metadata.
548 sub menu_trackname {
549         my ($entry) = @_;
550         my $fmt = "$entry->{trackfmt}$entry->{Artist} - $entry->{Title}";
551         return "$entry->{thumb}" . fvwm_label_escape($fmt);
552 }
553
554 sub print_version {
555         print <<EOF
556 mpdmenu.pl 0.9
557 Copyright © 2021 Nick Bowler
558 License GPLv3+: GNU General Public License version 3 or any later version.
559 This is free software: you are free to change and redistribute it.
560 There is NO WARRANTY, to the extent permitted by law.
561 EOF
562 }
563
564 sub print_usage {
565         my ($fh) = (@_, *STDERR);
566
567         print $fh "Usage: $0 [options]\n";
568         print $fh "Try $0 --help for more information.\n" unless (@_ > 0);
569 }
570
571 sub print_help {
572         print_usage(*STDOUT);
573         print <<EOF
574 This is "mpdmenu": a menu-based MPD client for FVWM.
575
576 Options:
577   -h, --host=HOST   Connect to the MPD server on HOST, overriding defaults.
578   -p, --port=PORT   Connect to the MPD server on PORT, overriding defaults.
579   -m, --menu=NAME   Set the name of the generated menu.
580   --album-id=MBID   Generate a menu for the given release MBID.
581   --album-name=NAME
582                     Generate a menu for standalone tracks with the given
583                     "album" NAME.  An artist MBID must be supplied.
584   --artist-id=MBID  Generate a menu for the given artist MBID.
585   --track-id=MBID   Generate a menu for the given track MBID.
586   --recording-id=MBID
587                     Generate a menu for the given recording MBID.
588   -V, --version     Print a version message and then exit.
589   -H, --help        Print this message and then exit.
590 EOF
591 }
592
593 GetOptions(
594         'host|h=s'       => \$MPD::host,
595         'port|p=s'       => \$MPD::port,
596         'menu|m=s'       => \$menu,
597
598         'artist-id=s'    => sub { $artistids{$_[1]} = 1; $mode = "artist"; },
599         'album-id=s'     => sub { $albumid = $_[1]; $mode = "album"; },
600         'album-name=s'   => sub { $albumname = $_[1]; $mode = "albumname"; },
601         'track-id=s'     => sub { $trackid = $_[1]; $mode = "track"; },
602         'recording-id=s' => sub { $recordingid = $_[1]; $mode = "recording"; },
603
604         'V|version'      => sub { print_version(); exit },
605         'H|help'         => sub { print_help(); exit },
606
607         'topmenu=s'      => \$topmenu, # top menu name (for submenu generation)
608 ) or do { print_usage; exit 1 };
609
610 $mode = "albumname" if ($albumname && $mode eq "artist");
611
612 unless (defined $menu) {
613         $topmenu //= "MenuMPD";
614         $menu = $topmenu . ($mode ne "top" ? $mode : "");
615 }
616 $topmenu //= $menu;
617
618 # Connect to MPD.
619 $sock = MPD::connect();
620 die("MPD version $MPD::major.$MPD::minor.$MPD::revision insufficient.")
621         unless MPD::min_version(MPD_MJR_MIN, MPD_MNR_MIN, MPD_REV_MIN);
622
623 MPD::exec("binarylimit", 64);
624 while (<$sock>) {
625         $mpd_have_binarylimit = 1 if /^OK/;
626         last if /^OK/ or /^ACK/;
627 }
628
629 if ($mode eq "top") {
630         my %current;
631         my %state;
632
633         $menu //= "MenuMPD";
634
635         MPD::exec("status");
636         while (<$sock>) {
637                 last if (/^OK/);
638                 die($_) if (/^ACK/);
639
640                 if (/^(\w+): (.*)$/) {
641                         $state{$1} = $2;
642                 }
643         }
644
645         MPD::exec("currentsong");
646         while (<$sock>) {
647                 last if (/^OK/);
648                 die($_) if (/^ACK/);
649
650                 if (/^(\w+): (.*)$/) {
651                         add_track_metadata(\%current, $1, $2);
652                 }
653         }
654
655         my $playstate = $state{state} eq "play"  ? "Playing"
656                       : $state{state} eq "stop"  ? "Stopped"
657                       : $state{state} eq "pause" ? "Paused"
658                       : "Unknown";
659         fvwm_cmd("AddToMenu", $menu, $playstate, "Title");
660
661         if (exists($current{file})) {
662                 top_track_cover(\%current);
663                 top_track_title(\%current);
664                 top_track_artist(\%current);
665                 top_track_album(\%current);
666                 top_track_musicbrainz(\%current);
667         } else {
668                 fvwm_cmd("AddToMenu", $menu, "[current track unavailable]");
669         }
670
671         if ($state{state} =~ /^p/) {
672                 my $pp = $state{state} eq "pause" ? "lay" : "ause";
673
674                 fvwm_cmd("AddToMenu", $menu, "", "Nop");
675                 fvwm_cmd("AddToMenu", $menu, "Next%next.svg:16x16%",
676                        "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "next");
677                 fvwm_cmd("AddToMenu", $menu, "P$pp%p$pp.svg:16x16%",
678                        "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "p$pp");
679                 fvwm_cmd("AddToMenu", $menu, "Stop%stop.svg:16x16%",
680                        "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "stop");
681                 fvwm_cmd("AddToMenu", $menu, "Prev%prev.svg:16x16%",
682                        "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "previous");
683         } elsif ($state{state} eq "stop") {
684                 fvwm_cmd("AddToMenu", $menu, "", "Nop");
685                 fvwm_cmd("AddToMenu", $menu, "Play%play.svg:16x16%",
686                        "Exec", "exec", "$FindBin::Bin/mpdexec.pl", "play");
687         }
688 } elsif ($mode eq "album") {
689         my $matches = get_tracks_by_release_mbid($albumid);
690         my @notqueued = ();
691
692         $menu //= "MenuMPDAlbum";
693
694         my $track_max = max(map { $_->{Track} } values %$matches);
695         my $disc_max = max(map { $_->{Disc} } values %$matches);
696
697         # CDs have a max of 99 tracks and I hope 100+-disc-releases
698         # don't exist so this is fine.
699         my $track_digits = $track_max >= 10 ? 2 : 1;
700         my $disc_digits = $disc_max > 1 ? $disc_max >= 10 ? 2 : 1 : 0;
701         my $currentdisc;
702
703         fvwm_cmd("AddToMenu", $menu);
704         fvwm_cmd("+", "Release not found", "Title") unless keys %$matches;
705         foreach my $file (sort { albumsort($matches, $a, $b) } keys %$matches) {
706                 my $entry = $matches->{$file};
707
708                 # Format disc/track numbers
709                 $entry->{trackfmt} = sprintf("%*.*s%.*s%*d\t",
710                                   $disc_digits, $disc_digits, $entry->{Disc},
711                                   $disc_digits, "-",
712                                   $track_digits, $entry->{Track});
713                 $entry->{trackfmt} =~ s/ /\N{U+2007}/g;
714
715                 unless (exists $entry->{Id}) {
716                         my ($id) = get_ids_by_filename($file);
717                         if (defined $id) {
718                                 $entry->{Id} = $id;
719                         } else {
720                                 push @notqueued, $entry;
721                                 next;
722                         }
723                 }
724
725                 if (defined $currentdisc && $currentdisc != $entry->{Disc}) {
726                         fvwm_cmd("+", "", "Nop");
727                 }
728                 $currentdisc = $entry->{Disc};
729
730                 fvwm_cmd("+", menu_trackname($entry), "Exec",
731                          "exec", "$FindBin::Bin/mpdexec.pl",
732                          "playid", $entry->{Id});
733         }
734
735         fvwm_cmd("+", "Not in play queue", "Title") if @notqueued;
736         foreach my $entry (@notqueued) {
737                 fvwm_cmd("+", menu_trackname($entry));
738         }
739 } elsif ($mode eq "artist") {
740         # Create an artist menu.
741         my ($matches, @recs) = get_releases_by_artist_mbid(keys %artistids);
742
743         $menu //= "MenuMPDArtist";
744
745         my @mbids = sort { datesort($matches, $a, $b) } keys %$matches;
746         my @files = map { $matches->{$_}->{file} } @mbids;
747         my @thumbs = get_item_thumbnails({ small => 1 }, @files);
748
749         unless (@mbids) {
750                 fvwm_cmd("AddToMenu", $menu, "No releases found", "Title")
751         }
752
753         foreach my $mbid (@mbids) {
754                 my $entry = $matches->{$mbid};
755                 my $thumb = shift @thumbs;
756
757                 my @submenu = make_submenu("$topmenu-$mbid",
758                                            "--album-id=$mbid");
759                 fvwm_cmd("AddToMenu", $menu,
760                          $thumb . fvwm_label_escape($entry->{Album}),
761                          @submenu);
762         }
763
764         my @artists = map { "--artist-id=$_" } keys %artistids;
765         my %nonalbums = map { $_->{Album} => $_ } @recs;
766         foreach my $name (sort keys %nonalbums) {
767                 my $mbid = $nonalbums{$name}->{MUSICBRAINZ_TRACKID};
768                 my @submenu = make_submenu("$topmenu-$mbid", @artists,
769                                            "--album-name=$name");
770                 fvwm_cmd("AddToMenu", $menu, fvwm_label_escape($name), @submenu);
771         }
772 } elsif ($mode eq "albumname") {
773         # Create a standalone recordings menu
774         my ($releases, @recs) = get_releases_by_artist_mbid(keys %artistids);
775
776         $menu //= "MenuMPDRecordings";
777         my @tracks = sort { $a->{Title} cmp $b->{Title} }
778                      grep { $_->{Album} eq $albumname } @recs;
779
780         # Show thumbnails for standalone recordings
781         my @thumbs = get_item_thumbnails({ small => 1 },
782                                          map { $_->{file} } @tracks);
783         foreach my $entry (@tracks) {
784                 $entry->{thumb} = shift @thumbs;
785         }
786
787         my @notqueued = update_entry_ids(@tracks);
788
789         fvwm_cmd("AddToMenu", $menu);
790         fvwm_cmd("+", "No tracks found", "Title") unless @tracks;
791
792         foreach my $entry (@tracks) {
793                 next unless exists $entry->{Id};
794
795                 fvwm_cmd("+", menu_trackname($entry), "Exec",
796                          "exec", "$FindBin::Bin/mpdexec.pl",
797                          "playid", $entry->{Id});
798         }
799
800         fvwm_cmd("+", "Not in play queue", "Title") if @notqueued;
801         foreach my $entry (@notqueued) {
802                 fvwm_cmd("+", menu_trackname($entry));
803         }
804 } elsif ($mode eq "track" || $mode eq "recording") {
805         my ($matches, $mbid);
806         my @notqueued;
807
808         if ($mode eq "track") {
809                 $matches = get_tracks_by_track_mbid($trackid)
810         } else {
811                 $matches = get_tracks_by_recording_mbid($recordingid)
812         }
813
814         $menu //= "MenuMPDTrack";
815         fvwm_cmd("DestroyMenu", $menu);
816
817         my @files = sort { datesort($matches, $a, $b) } keys %$matches;
818         my @thumbs = get_item_thumbnails({ small => 1 }, @files);
819
820         fvwm_cmd("AddToMenu", $menu);
821         fvwm_cmd("+", "No tracks found", "Title") unless @files;
822         foreach my $file (@files) {
823                 my $entry = $matches->{$file};
824                 $entry->{thumb} = shift @thumbs;
825
826                 unless (exists $entry->{Id}) {
827                         my ($id) = get_ids_by_filename($file);
828                         if (defined $id) {
829                                 $entry->{Id} = $id;
830                         } else {
831                                 push @notqueued, $entry;
832                                 next;
833                         }
834                 }
835
836                 fvwm_cmd("+", menu_trackname($entry), "Exec",
837                          "exec", "$FindBin::Bin/mpdexec.pl",
838                          "playid", $entry->{Id});
839         }
840
841         fvwm_cmd("+", "Not in play queue", "Title") if @notqueued;
842         foreach my $entry (@notqueued) {
843                 fvwm_cmd("+", menu_trackname($entry));
844         }
845 }
846
847 # Finished.
848 print $sock "close\n";