3 # Copyright © 2008,2010,2012,2019 Nick Bowler
5 # Silly little script to generate an FVWM menu with various bits of MPD
6 # status information and controls.
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.
15 use IO::Socket::INET6;
25 use open qw(:std :utf8);
26 binmode(STDOUT, ":utf8");
29 my $SELF = "$FindBin::Bin/$FindBin::Script";
36 # Global hash for tracking what is to be "accepted".
39 my $FVWM = (defined $ENV{FVWM_USERDIR}) ? $ENV{FVWM_USERDIR}
40 : $ENV{HOME}."/.fvwm";
41 my $icons = "$FVWM/icons";
43 # Default values for stuff.
44 my ($album, $artist, $title, $menu) = (undef, undef, undef, undef);
45 my $host = (defined $ENV{MPD_HOST}) ? $ENV{MPD_HOST} : "localhost";
46 my $port = (defined $ENV{MPD_PORT}) ? $ENV{MPD_PORT} : "6600";
49 'host|h=s' => \&host, # Host that MPD is running on.
50 'port|p=s' => \&port, # Port that MPD is listening on.
51 'menu|m=s' => \$menu, # Name of the menu to create.
52 'album=s' => \$album, # Album to get tracks from
53 'artist=s' => \$artist, # Artist to limit results to
54 'title=s' => \$title, # Title to create menu for
57 $album = decode_utf8($album) if defined($album);
58 $artist = decode_utf8($artist) if defined($artist);
59 $title = decode_utf8($title) if defined($title);;
62 my $sock = new IO::Socket::INET6(
67 ) or die("could not open socket: $!.\n");
68 binmode($sock, ":utf8");
70 die("could not connect to MPD: $!.\n")
71 if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/));
73 die("MPD version $1.$2.$3 insufficient.\n")
74 if ( ($1 < MPD_MJR_MIN)
75 || ($1 == MPD_MJR_MIN && $2 < MPD_MNR_MIN)
76 || ($1 == MPD_MJR_MIN && $2 == MPD_MNR_MIN && $3 < MPD_REV_MIN));
79 # Create an album menu.
83 $menu = "MenuMPDAlbum" unless defined $menu;
86 print $sock "playlistfind album \"$album\"\n";
91 if (/^(\w+): (.*)$/) {
93 if (keys(%$entry) > 0) {
94 addalbumentry(\@playlist, $entry)
103 addalbumentry(\@playlist, $entry) if (keys(%$entry) > 0);
105 die("No tracks found.\n") if (!@playlist);
106 foreach (sort albumsort @playlist) {
107 my ($t_file, $t_trackno, $t_artist, $t_title, $t_id) = (
115 next if (defined $artist && !$accept{albumdir($t_file)});
117 $t_artist = sanitise($t_artist, 0);
118 $t_title = sanitise($t_title, 0);
120 my $cmd = sprintf "AddToMenu $menu \"%d\t%s - %s\""
121 ." Exec exec $FindBin::Bin/mpdexec.pl"
123 $t_trackno, $t_artist, $t_title, $t_id;
127 } elsif (defined $artist) {
128 # Create an artist menu.
131 my $quoteartist = $artist;
133 $menu = "MenuMPDArtist" unless defined $menu;
135 $quoteartist =~ s/"/\\"/g;
136 print $sock "playlistfind artist \"$quoteartist\"\n";
141 if (/^(\w+): (.*)$/) {
142 $file = $2 if ($1 eq "file");
143 $albums{$2} = $file if ($1 eq "Album");
147 die("No albums found.\n") if (!keys(%albums));
149 { # work around 'use locale' breaking s///i
152 foreach (sort keys(%albums)) {
154 my $a_album = sanitise($key, 1);
156 open THUMB, "-|", "$FindBin::Bin/thumbnail.zsh",
157 "--small", "--music", $albums{$key};
160 die("Incompetent use of thumbnail.zsh") if ($?);
163 $thumb = "%$thumb%" if (-f $thumb);
165 cmd("AddToMenu $menu \"$thumb$a_album\" Popup MenuMPDArt_$i");
167 cmd("AddToMenu MenuMPDArt_$i DynamicPopUpAction MakeMenuMPDArt_$i");
169 cmd("DestroyFunc MakeMenuMPDArt_$i");
170 cmd("AddToFunc MakeMenuMPDArt_$i
171 + I DestroyMenu MenuMPDArt_$i
172 + I -PipeRead \"exec $SELF "
173 ."--menu MenuMPDArt_$i "
174 ."--album ".shellify($key, 1)." "
175 ."--artist ".shellify($artist, 1)."\"");
177 cmd("AddToFunc KillMenuMPD I DestroyMenu MenuMPDArt_$i");
178 cmd("AddToFunc KillMenuMPD I DestroyFunc MakeMenuMPDArt_$i");
182 } # end use locale workaround
183 } elsif (defined $title) {
184 # Create a title menu.
188 $menu = "MenuMPDTitle" unless defined $menu;
190 # Open and close brackets.
191 my ($ob, $cb) = ("[\[~〜<〈(ー−-]", "[\]~〜>〉)ー−-]");
195 # Deal with specific cases.
196 s/ちいさな(?=ヘミソフィア)//; # ヘミソフィア
197 s/ "mix on air flavor" dear EIKO SHIMAMIYA//; # Spiral wind
198 s/ "So,you need me" Style//; # I need you
199 s/ ::Symphony Second movement:://; # Disintegration
200 s/-\[instrumental\]//; # 青い果実
201 s/ -Practice Track-//; # Fair Heaven
202 s/〜世界で一番アナタが好き〜//; # Pure Heart
204 s/ sora no uta ver.//; # 美しい星
206 s/\s*-remix-$//; # Otherwise "D-THREAD -remix-" doesn't work right.
208 # Deal with titles like "blah (ABC version)".
209 s/\s*$ob.*(style|mix|edit|edition|ver\.?|version|melody|カラオケ)$cb?$//i;
211 # Deal with titles like "blah (without XYZ)".
212 s/\s*$ob\s*((e\.)?piano|english|japanese|inst|tv|without|w\/o|off|back|short|karaoke|game).*//i;
214 # Deal with titles like "blah instrumental".
215 s/\s+(instrumental|off vocal|short|tv)([\s-]+(mix|size|version))?$//i;
216 s/\s+without\s+\w+$//i;
218 # Deal with separate movements in classical pieces.
222 my $_basetitle = $basetitle;
224 $_basetitle =~ s/"/\\"/g;
225 print $sock "playlistsearch title \"$_basetitle\"\n";
230 if (/^(\w+): (.*)$/) {
232 push @titles, $entry if (keys(%$entry) > 0);
239 push @titles, $entry if (keys(%$entry) > 0);
241 { # work around 'use locale' breaking s///i
243 foreach (sort titlesort @titles) {
244 my ($t_file, $t_artist, $t_title, $t_id) = (
251 # MPD searches are case-insensitive.
252 next if (!($t_title =~ m/(\P{Latin}|^)\Q$basetitle\E(\P{Latin}|$)/ || $t_title =~ m/\Q$basetitle\E/i));
254 $t_artist = sanitise($t_artist, 1);
255 $t_title = sanitise($t_title, 1);
257 open THUMB, "-|", "$FindBin::Bin/thumbnail.zsh",
258 "--small", "--music", $t_file;
261 die("Incompetent use of thumbnail.zsh") if ($?);
264 $thumb = "%$thumb%" if (-f $thumb);
266 cmd("AddToMenu $menu \"$thumb$t_artist - $t_title\""
267 ." Exec exec $FindBin::Bin/mpdexec.pl"
270 } # end use locale workaround
273 my ($state, $songid) = (undef, undef);
276 $menu = "MenuMPD" unless defined $menu;
278 print $sock "status\n";
283 if (/^(\w+): (.*)$/) {
284 $state = $2 if ($1 eq "state");
285 $songid = $2 if ($1 eq "songid");
288 die("Failed status query\n") unless (defined $state);
290 cmd("AddToMenu $menu Playing Title") if ($state eq "play");
291 cmd("AddToMenu $menu Paused Title") if ($state eq "pause");
292 cmd("AddToMenu $menu Stopped Title") if ($state eq "stop");
294 if (defined $songid) {
295 print $sock "playlistid $songid\n";
300 if (/^(\w+): (.*)$/) {
304 die("Failed data query\n") unless (keys(%entry) > 0);
306 open THUMB, "-|", "$FindBin::Bin/thumbnail.zsh",
307 "--image", "--music", $entry{file};
311 die("Incompetent use of thumbnail.zsh") if ($?);
317 cmd("AddToMenu $menu \"*$thumb*\" "
318 ."Exec exec geeqie ".shellify($scan, 0));
320 cmd("AddToMenu $menu \"Title: ".sanitise($entry{Title}, 0)
321 ."\" Popup MenuMPDTitle");
322 cmd("AddToMenu $menu \"Artist: ".sanitise($entry{Artist}, 0)
323 ."\" Popup MenuMPDArtist");
324 cmd("AddToMenu $menu \"Album: ".sanitise($entry{Album}, 0)
325 ."\" Popup MenuMPDAlbum");
326 cmd("AddToMenu $menu \"\" Nop");
328 cmd("AddToMenu $menu \"<Song info unavailable>\"");
329 cmd("AddToMenu $menu \"\" Nop");
332 if ($state eq "play" || $state eq "pause") {
333 cmd("AddToMenu $menu \"\t\tNext%$icons/next.svg:16x16%\" "
334 ."Exec exec $FindBin::Bin/mpdexec.pl next");
335 cmd("AddToMenu $menu \"\t\tPause%$icons/pause.svg:16x16%\" "
336 ."Exec exec $FindBin::Bin/mpdexec.pl pause");
337 cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" "
338 ."Exec exec $FindBin::Bin/mpdexec.pl play");
339 cmd("AddToMenu $menu \"\t\tStop%$icons/stop.svg:16x16%\" "
340 ."Exec exec $FindBin::Bin/mpdexec.pl stop");
341 cmd("AddToMenu $menu \"\t\tPrev%$icons/prev.svg:16x16%\" "
342 ."Exec exec $FindBin::Bin/mpdexec.pl previous");
343 } elsif ($state eq "stop") {
344 cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" "
345 ."Exec exec $FindBin::Bin/mpdexec.pl play");
347 die("Unknown MPD state!\n");
350 cmd("AddToMenu $menu \"\" Nop");
351 cmd("AddToMenu $menu \"\t\tShuffle%$icons/shuffle.svg:16x16%\" "
352 ."Exec exec $FindBin::Bin/mpdexec.pl shuffle");
354 cmd("DestroyMenu MenuMPDTitle");
355 cmd("AddToMenu MenuMPDTitle DynamicPopUpAction MakeMenuMPDTitle");
356 cmd("DestroyMenu MenuMPDArtist");
357 cmd("AddToMenu MenuMPDArtist DynamicPopUpAction MakeMenuMPDArtist");
358 cmd("DestroyMenu MenuMPDAlbum");
359 cmd("AddToMenu MenuMPDAlbum DynamicPopUpAction MakeMenuMPDAlbum");
361 cmd("DestroyFunc MakeMenuMPDTitle");
362 cmd("AddToFunc MakeMenuMPDTitle
363 + I DestroyMenu MenuMPDTitle
364 + I -PipeRead \"exec $SELF "
365 ."--menu MenuMPDTitle "
366 ."--title ".shellify($entry{Title}, 1)."\"");
368 cmd("DestroyFunc MakeMenuMPDAlbum");
369 cmd("AddToFunc MakeMenuMPDAlbum
370 + I DestroyMenu MenuMPDAlbum
371 + I -PipeRead \"exec $SELF "
372 ."--menu MenuMPDAlbum "
373 ."--album ".shellify($entry{Album}, 1)." "
374 ."--artist ".shellify($entry{Artist}, 1)."\"");
376 cmd("DestroyFunc MakeMenuMPDArtist");
377 cmd("AddToFunc MakeMenuMPDArtist
378 + I DestroyMenu MenuMPDArtist
379 + I -PipeRead \"exec $SELF "
380 ."--menu MenuMPDArtist "
381 ."--artist ".shellify($entry{Artist}, 1)."\"");
383 cmd("DestroyFunc KillMenuMPD");
384 cmd("AddToFunc KillMenuMPD I Nop");
388 print $sock "close\n";
401 my ($playlist, $entry) = @_;
403 push(@$playlist, $entry);
405 if (defined $artist && $artist eq $entry->{Artist}) {
406 my $albumdir = albumdir($entry->{file});
407 $accept{$albumdir} = 1;
415 $file =~ s:(/Disk [0-9]+[^/]*)?/[^/]*$::;
421 return ($a->{Disc} <=> $b->{Disc}) if ($a->{Disc} != $b->{Disc});
422 return ($a->{Track} <=> $b->{Track});
427 return ($a->{Album} cmp $b->{Album}) if($a->{Album} ne $b->{Album});
428 return ($a->{Artist} cmp $b->{Artist}) if($a->{Artist} ne $b->{Artist});
429 return ($a->{Title} cmp $b->{Title});
434 my ($str, $quoted) = @_;