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