3 # Copyright © 2018,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 WTFPL2: Do What The Fuck You Want To Public License, version 2.
9 # This is free software: you are free to do what the fuck you want to.
10 # There is NO WARRANTY, to the extent permitted by law.
15 use IO::Socket::INET6;
24 use open qw(:std :utf8);
25 binmode(STDOUT, ":utf8");
33 # Global hash for tracking what is to be "accepted".
36 my $FVWM = (defined $ENV{FVWM_USERDIR}) ? $ENV{FVWM_USERDIR}
37 : $ENV{HOME}."/.fvwm";
38 my $icons = "$FVWM/icons";
40 # Default values for stuff.
41 my ($album, $artist, $title, $menu) = (undef, undef, undef, undef);
42 my $host = (defined $ENV{MPD_HOST}) ? $ENV{MPD_HOST} : "localhost";
43 my $port = (defined $ENV{MPD_PORT}) ? $ENV{MPD_PORT} : "6600";
46 'host|h=s' => \&host, # Host that MPD is running on.
47 'port|p=s' => \&port, # Port that MPD is listening on.
48 'menu|m=s' => \$menu, # Name of the menu to create.
49 'album=s' => \$album, # Album to get tracks from
50 'artist=s' => \$artist, # Artist to limit results to
51 'title=s' => \$title, # Title to create menu for
54 $album = decode_utf8($album) if defined($album);
55 $artist = decode_utf8($artist) if defined($artist);
56 $title = decode_utf8($title) if defined($title);;
59 my $sock = new IO::Socket::INET6(
64 ) or die("could not open socket: $!.\n");
65 binmode($sock, ":utf8");
67 die("could not connect to MPD: $!.\n")
68 if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/));
70 die("MPD version $1.$2.$3 insufficient.\n")
71 if ( ($1 < MPD_MJR_MIN)
72 || ($1 == MPD_MJR_MIN && $2 < MPD_MNR_MIN)
73 || ($1 == MPD_MJR_MIN && $2 == MPD_MNR_MIN && $3 < MPD_REV_MIN));
76 # Create an album menu.
80 $menu = "MenuMPDAlbum" unless defined $menu;
83 print $sock "playlistfind album \"$album\"\n";
88 if (/^(\w+): (.*)$/) {
90 if (keys(%$entry) > 0) {
91 addalbumentry(\@playlist, $entry)
100 addalbumentry(\@playlist, $entry) if (keys(%$entry) > 0);
102 die("No tracks found.\n") if (!@playlist);
103 foreach (sort albumsort @playlist) {
104 my ($t_file, $t_trackno, $t_artist, $t_title, $t_id) = (
112 next if (defined $artist && !$accept{albumdir($t_file)});
114 $t_artist = sanitise($t_artist, 0);
115 $t_title = sanitise($t_title, 0);
117 my $cmd = sprintf "AddToMenu $menu \"%d\t%s - %s\""
118 ." Exec exec $FVWM/scripts/mpdexec.pl"
120 $t_trackno, $t_artist, $t_title, $t_id;
124 } elsif (defined $artist) {
125 # Create an artist menu.
128 my $quoteartist = $artist;
130 $menu = "MenuMPDArtist" unless defined $menu;
132 $quoteartist =~ s/"/\\"/g;
133 print $sock "playlistfind artist \"$quoteartist\"\n";
138 if (/^(\w+): (.*)$/) {
139 $file = $2 if ($1 eq "file");
140 $albums{$2} = $file if ($1 eq "Album");
144 die("No albums found.\n") if (!keys(%albums));
146 { # work around 'use locale' breaking s///i
149 foreach (sort keys(%albums)) {
151 my $a_album = sanitise($key, 1);
153 open THUMB, "-|", "$FVWM/scripts/thumbnail.zsh",
154 "--small", "--music", $albums{$key};
157 die("Incompetent use of thumbnail.zsh") if ($?);
160 $thumb = "%$thumb%" if (-f $thumb);
162 cmd("AddToMenu $menu \"$thumb$a_album\" Popup MenuMPDArt_$i");
164 cmd("AddToMenu MenuMPDArt_$i DynamicPopUpAction MakeMenuMPDArt_$i");
166 cmd("DestroyFunc MakeMenuMPDArt_$i");
167 cmd("AddToFunc MakeMenuMPDArt_$i
168 + I DestroyMenu MenuMPDArt_$i
169 + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
170 ."--menu MenuMPDArt_$i "
171 ."--album ".shellify($key, 1)." "
172 ."--artist ".shellify($artist, 1)."\"");
174 cmd("AddToFunc KillMenuMPD I DestroyMenu MenuMPDArt_$i");
175 cmd("AddToFunc KillMenuMPD I DestroyFunc MakeMenuMPDArt_$i");
179 } # end use locale workaround
180 } elsif (defined $title) {
181 # Create a title menu.
185 $menu = "MenuMPDTitle" unless defined $menu;
187 # Open and close brackets.
188 my ($ob, $cb) = ("[\[~〜<〈(ー−-]", "[\]~〜>〉)ー−-]");
192 # Deal with specific cases.
193 s/ちいさな(?=ヘミソフィア)//; # ヘミソフィア
194 s/ "mix on air flavor" dear EIKO SHIMAMIYA//; # Spiral wind
195 s/ "So,you need me" Style//; # I need you
196 s/ ::Symphony Second movement:://; # Disintegration
197 s/-\[instrumental\]//; # 青い果実
198 s/ -Practice Track-//; # Fair Heaven
199 s/〜世界で一番アナタが好き〜//; # Pure Heart
201 s/ sora no uta ver.//; # 美しい星
203 s/\s*-remix-$//; # Otherwise "D-THREAD -remix-" doesn't work right.
205 # Deal with titles like "blah (ABC version)".
206 s/\s*$ob.*(style|mix|edit|edition|ver\.?|version|melody|カラオケ)$cb?$//i;
208 # Deal with titles like "blah (without XYZ)".
209 s/\s*$ob\s*((e\.)?piano|english|japanese|inst|tv|without|w\/o|off|back|short|karaoke|game).*//i;
211 # Deal with titles like "blah instrumental".
212 s/\s+(instrumental|off vocal|short|tv)([\s-]+(mix|size|version))?$//i;
213 s/\s+without\s+\w+$//i;
215 # Deal with separate movements in classical pieces.
219 my $_basetitle = $basetitle;
221 $_basetitle =~ s/"/\\"/g;
222 print $sock "playlistsearch title \"$_basetitle\"\n";
227 if (/^(\w+): (.*)$/) {
229 push @titles, $entry if (keys(%$entry) > 0);
236 push @titles, $entry if (keys(%$entry) > 0);
238 { # work around 'use locale' breaking s///i
240 foreach (sort titlesort @titles) {
241 my ($t_file, $t_artist, $t_title, $t_id) = (
248 # MPD searches are case-insensitive.
249 next if (!($t_title =~ m/(\P{Latin}|^)\Q$basetitle\E(\P{Latin}|$)/ || $t_title =~ m/\Q$basetitle\E/i));
251 $t_artist = sanitise($t_artist, 1);
252 $t_title = sanitise($t_title, 1);
254 open THUMB, "-|", "$FVWM/scripts/thumbnail.zsh",
255 "--small", "--music", $t_file;
258 die("Incompetent use of thumbnail.zsh") if ($?);
261 $thumb = "%$thumb%" if (-f $thumb);
263 cmd("AddToMenu $menu \"$thumb$t_artist - $t_title\""
264 ." Exec exec $FVWM/scripts/mpdexec.pl"
267 } # end use locale workaround
270 my ($state, $songid) = (undef, undef);
273 $menu = "MenuMPD" unless defined $menu;
275 print $sock "status\n";
280 if (/^(\w+): (.*)$/) {
281 $state = $2 if ($1 eq "state");
282 $songid = $2 if ($1 eq "songid");
285 die("Failed status query\n") unless (defined $state);
287 cmd("AddToMenu $menu Playing Title") if ($state eq "play");
288 cmd("AddToMenu $menu Paused Title") if ($state eq "pause");
289 cmd("AddToMenu $menu Stopped Title") if ($state eq "stop");
291 if (defined $songid) {
292 print $sock "playlistid $songid\n";
297 if (/^(\w+): (.*)$/) {
301 die("Failed data query\n") unless (keys(%entry) > 0);
303 open THUMB, "-|", "$FVWM/scripts/thumbnail.zsh",
304 "--image", "--music", $entry{file};
308 die("Incompetent use of thumbnail.sh") if ($?);
314 cmd("AddToMenu $menu \"*$thumb*\" "
315 ."Exec exec geeqie ".shellify($scan, 0));
317 cmd("AddToMenu $menu \"Title: ".sanitise($entry{Title}, 0)
318 ."\" Popup MenuMPDTitle");
319 cmd("AddToMenu $menu \"Artist: ".sanitise($entry{Artist}, 0)
320 ."\" Popup MenuMPDArtist");
321 cmd("AddToMenu $menu \"Album: ".sanitise($entry{Album}, 0)
322 ."\" Popup MenuMPDAlbum");
323 cmd("AddToMenu $menu \"\" Nop");
325 cmd("AddToMenu $menu \"<Song info unavailable>\"");
326 cmd("AddToMenu $menu \"\" Nop");
329 if ($state eq "play" || $state eq "pause") {
330 cmd("AddToMenu $menu \"\t\tNext%$icons/next.svg:16x16%\" "
331 ."Exec exec $FVWM/scripts/mpdexec.pl next");
332 cmd("AddToMenu $menu \"\t\tPause%$icons/pause.svg:16x16%\" "
333 ."Exec exec $FVWM/scripts/mpdexec.pl pause");
334 cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" "
335 ."Exec exec $FVWM/scripts/mpdexec.pl play");
336 cmd("AddToMenu $menu \"\t\tStop%$icons/stop.svg:16x16%\" "
337 ."Exec exec $FVWM/scripts/mpdexec.pl stop");
338 cmd("AddToMenu $menu \"\t\tPrev%$icons/prev.svg:16x16%\" "
339 ."Exec exec $FVWM/scripts/mpdexec.pl previous");
340 } elsif ($state eq "stop") {
341 cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" "
342 ."Exec exec $FVWM/scripts/mpdexec.pl play");
344 die("Unknown MPD state!\n");
347 cmd("AddToMenu $menu \"\" Nop");
348 cmd("AddToMenu $menu \"\t\tShuffle%$icons/shuffle.svg:16x16%\" "
349 ."Exec exec $FVWM/scripts/mpdexec.pl shuffle");
351 cmd("DestroyMenu MenuMPDTitle");
352 cmd("AddToMenu MenuMPDTitle DynamicPopUpAction MakeMenuMPDTitle");
353 cmd("DestroyMenu MenuMPDArtist");
354 cmd("AddToMenu MenuMPDArtist DynamicPopUpAction MakeMenuMPDArtist");
355 cmd("DestroyMenu MenuMPDAlbum");
356 cmd("AddToMenu MenuMPDAlbum DynamicPopUpAction MakeMenuMPDAlbum");
358 cmd("DestroyFunc MakeMenuMPDTitle");
359 cmd("AddToFunc MakeMenuMPDTitle
360 + I DestroyMenu MenuMPDTitle
361 + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
362 ."--menu MenuMPDTitle "
363 ."--title ".shellify($entry{Title}, 1)."\"");
365 cmd("DestroyFunc MakeMenuMPDAlbum");
366 cmd("AddToFunc MakeMenuMPDAlbum
367 + I DestroyMenu MenuMPDAlbum
368 + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
369 ."--menu MenuMPDAlbum "
370 ."--album ".shellify($entry{Album}, 1)." "
371 ."--artist ".shellify($entry{Artist}, 1)."\"");
373 cmd("DestroyFunc MakeMenuMPDArtist");
374 cmd("AddToFunc MakeMenuMPDArtist
375 + I DestroyMenu MenuMPDArtist
376 + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
377 ."--menu MenuMPDArtist "
378 ."--artist ".shellify($entry{Artist}, 1)."\"");
380 cmd("DestroyFunc KillMenuMPD");
381 cmd("AddToFunc KillMenuMPD I Nop");
385 print $sock "close\n";
398 my ($playlist, $entry) = @_;
400 push(@$playlist, $entry);
402 if (defined $artist && $artist eq $entry->{Artist}) {
403 my $albumdir = albumdir($entry->{file});
404 $accept{$albumdir} = 1;
412 $file =~ s:(/Disk [0-9]+[^/]*)?/[^/]*$::;
418 return ($a->{Disc} <=> $b->{Disc}) if ($a->{Disc} != $b->{Disc});
419 return ($a->{Track} <=> $b->{Track});
424 return ($a->{Album} cmp $b->{Album}) if($a->{Album} ne $b->{Album});
425 return ($a->{Artist} cmp $b->{Artist}) if($a->{Artist} ne $b->{Artist});
426 return ($a->{Title} cmp $b->{Title});
431 my ($str, $quoted) = @_;