]> git.draconx.ca Git - fvwmconf.git/blob - scripts/mpdmenu.pl
MPD script updates.
[fvwmconf.git] / scripts / mpdmenu.pl
1 #!/usr/bin/perl
2 #
3 # Copyright © 2018,2010,2012,2019 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 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.
11
12 use strict;
13
14 use Getopt::Long;
15 use IO::Socket::INET6;
16
17 use constant {
18         MPD_MJR_MIN => 0,
19         MPD_MNR_MIN => 13,
20         MPD_REV_MIN => 0,
21 };
22
23 use utf8;
24 use open qw(:std :utf8);
25 binmode(STDOUT, ":utf8");
26 use Encode;
27
28 sub cmd
29 {
30         print "$_[0]\n";
31 }
32
33 # Global hash for tracking what is to be "accepted".
34 my %accept = ();
35
36 my $FVWM = (defined $ENV{FVWM_USERDIR}) ? $ENV{FVWM_USERDIR}
37                                         : $ENV{HOME}."/.fvwm";
38 my $icons = "$FVWM/icons";
39
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";
44
45 GetOptions(
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
52 );
53
54 $album  = decode_utf8($album)  if defined($album);
55 $artist = decode_utf8($artist) if defined($artist);
56 $title  = decode_utf8($title)  if defined($title);;
57
58 # Connect to MPD.
59 my $sock = new IO::Socket::INET6(
60         PeerAddr => $host,
61         PeerPort => $port,
62         Proto => 'tcp',
63         Timeout => 2
64 ) or die("could not open socket: $!.\n");
65 binmode($sock, ":utf8");
66
67 die("could not connect to MPD: $!.\n")
68         if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/));
69
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));
74
75 if (defined $album) {
76         # Create an album menu.
77         my @playlist = ();
78         my $entry;
79
80         $menu = "MenuMPDAlbum" unless defined $menu;
81
82         $album =~ s/"/\\"/g;
83         print $sock "playlistfind album \"$album\"\n";
84         while (<$sock>) {
85                 last if (/^OK/);
86                 die($_) if (/^ACK/);
87
88                 if (/^(\w+): (.*)$/) {
89                         if ($1 eq "file") {
90                                 if (keys(%$entry) > 0) {
91                                         addalbumentry(\@playlist, $entry)
92                                 }
93
94                                 $entry = {};
95                         }
96
97                         $entry->{$1} = $2;
98                 }
99         }
100         addalbumentry(\@playlist, $entry) if (keys(%$entry) > 0);
101
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) = (
105                         $_->{file},
106                         $_->{Track},
107                         $_->{Artist},
108                         $_->{Title},
109                         $_->{Id},
110                 );
111
112                 next if (defined $artist && !$accept{albumdir($t_file)});
113
114                 $t_artist = sanitise($t_artist, 0);
115                 $t_title  = sanitise($t_title, 0);
116
117                 my $cmd = sprintf "AddToMenu $menu \"%d\t%s - %s\""
118                                   ." Exec exec $FVWM/scripts/mpdexec.pl"
119                                   ." playid %d",
120                                   $t_trackno, $t_artist, $t_title, $t_id;
121
122                 cmd($cmd);
123         }
124 } elsif (defined $artist) {
125         # Create an artist menu.
126         my %albums = ();
127         my $file;
128         my $quoteartist = $artist;
129
130         $menu = "MenuMPDArtist" unless defined $menu;
131
132         $quoteartist =~ s/"/\\"/g;
133         print $sock "playlistfind artist \"$quoteartist\"\n";
134         while (<$sock>) {
135                 last if (/^OK/);
136                 die($_) if (/^ACK/);
137
138                 if (/^(\w+): (.*)$/) {
139                         $file       = $2    if ($1 eq "file");
140                         $albums{$2} = $file if ($1 eq "Album");
141                 }
142         }
143
144         die("No albums found.\n") if (!keys(%albums));
145
146 { # work around 'use locale' breaking s///i
147         my $i = 0;
148         use locale;
149         foreach (sort keys(%albums)) {
150                 my $key      = $_;
151                 my $a_album  = sanitise($key, 1);
152
153                 open THUMB, "-|", "$FVWM/scripts/thumbnail.zsh",
154                                          "--small", "--music", $albums{$key};
155                 my $thumb = <THUMB>;
156                 close THUMB;
157                 die("Incompetent use of thumbnail.zsh") if ($?);
158
159                 $thumb =~ s/\n//sg;
160                 $thumb = "%$thumb%" if (-f $thumb);
161
162                 cmd("AddToMenu $menu \"$thumb$a_album\" Popup MenuMPDArt_$i");
163
164                 cmd("AddToMenu MenuMPDArt_$i DynamicPopUpAction MakeMenuMPDArt_$i");
165
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)."\"");
173
174                 cmd("AddToFunc KillMenuMPD I DestroyMenu MenuMPDArt_$i");
175                 cmd("AddToFunc KillMenuMPD I DestroyFunc MakeMenuMPDArt_$i");
176
177                 $i++;
178         }
179 } # end use locale workaround
180 } elsif (defined $title) {
181         # Create a title menu.
182         my @titles;
183         my $entry;
184
185         $menu = "MenuMPDTitle" unless defined $menu;
186
187         # Open and close brackets.
188         my ($ob, $cb) = ("[\[~〜<〈(ー−-]", "[\]~〜>〉)ー−-]");
189
190         $_ = $title;
191
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
200         s/〜彼方への哀歌//;                           # 十二幻夢
201         s/ sora no uta ver.//;                       # 美しい星
202
203         s/\s*-remix-$//; # Otherwise "D-THREAD -remix-" doesn't work right.
204
205         # Deal with titles like "blah (ABC version)".
206         s/\s*$ob.*(style|mix|edit|edition|ver\.?|version|melody|カラオケ)$cb?$//i;
207
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;
210
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;
214
215         # Deal with separate movements in classical pieces.
216         s/: [IVX]+\..*//;
217
218         my $basetitle  = $_;
219         my $_basetitle = $basetitle;
220
221         $_basetitle =~ s/"/\\"/g;
222         print $sock "playlistsearch title \"$_basetitle\"\n";
223         while (<$sock>) {
224                 last if (/^OK/);
225                 die($_) if (/^ACK/);
226
227                 if (/^(\w+): (.*)$/) {
228                         if ($1 eq "file") {
229                                 push @titles, $entry if (keys(%$entry) > 0);
230                                 $entry = {};
231                         }
232
233                         $entry->{$1} = $2;
234                 }
235         }
236         push @titles, $entry if (keys(%$entry) > 0);
237
238 { # work around 'use locale' breaking s///i
239         use locale;
240         foreach (sort titlesort @titles) {
241                 my ($t_file, $t_artist, $t_title, $t_id) = (
242                         $_->{file},
243                         $_->{Artist},
244                         $_->{Title},
245                         $_->{Id},
246                 );
247
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));
250
251                 $t_artist = sanitise($t_artist, 1);
252                 $t_title  = sanitise($t_title, 1);
253
254                 open THUMB, "-|", "$FVWM/scripts/thumbnail.zsh",
255                                          "--small", "--music", $t_file;
256                 my $thumb = <THUMB>;
257                 close(THUMB);
258                 die("Incompetent use of thumbnail.zsh") if ($?);
259
260                 $thumb =~ s/\n//sg;
261                 $thumb = "%$thumb%" if (-f $thumb);
262
263                 cmd("AddToMenu $menu \"$thumb$t_artist - $t_title\""
264                     ." Exec exec $FVWM/scripts/mpdexec.pl"
265                     ." playid $t_id");
266         }
267 } # end use locale workaround
268 } else {
269         # Make MPD base menu
270         my ($state, $songid) = (undef, undef);
271         my %entry = ();
272
273         $menu = "MenuMPD" unless defined $menu;
274
275         print $sock "status\n";
276         while (<$sock>) {
277                 last if (/^OK/);
278                 die($_) if (/^ACK/);
279
280                 if (/^(\w+): (.*)$/) {
281                         $state  = $2 if ($1 eq "state");
282                         $songid = $2 if ($1 eq "songid");
283                 }
284         }
285         die("Failed status query\n") unless (defined $state);
286
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");
290
291         if (defined $songid) {
292                 print $sock "playlistid $songid\n";
293                 while (<$sock>) {
294                         last if (/^OK/);
295                         die($_) if (/^ACK/);
296
297                         if (/^(\w+): (.*)$/) {
298                                 $entry{$1} = $2;
299                         }
300                 }
301                 die("Failed data query\n") unless (keys(%entry) > 0);
302
303                 open THUMB, "-|", "$FVWM/scripts/thumbnail.zsh",
304                                          "--image", "--music",  $entry{file};
305                 my $thumb = <THUMB>;
306                 my $scan  = <THUMB>;
307                 close(THUMB);
308                 die("Incompetent use of thumbnail.sh") if ($?);
309
310                 $thumb =~ s/\n//sg;
311                 $scan  =~ s/\n//sg;
312
313                 if (-f $thumb) {
314                         cmd("AddToMenu $menu \"*$thumb*\" "
315                                 ."Exec exec geeqie ".shellify($scan, 0));
316                 }
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");
324         } else {
325                 cmd("AddToMenu $menu \"<Song info unavailable>\"");
326                 cmd("AddToMenu $menu \"\" Nop");
327         }
328
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");
343         } else {
344                 die("Unknown MPD state!\n");
345         }
346
347         cmd("AddToMenu $menu \"\" Nop");
348         cmd("AddToMenu $menu \"\t\tShuffle%$icons/shuffle.svg:16x16%\" "
349                 ."Exec exec $FVWM/scripts/mpdexec.pl shuffle");
350
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");
357
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)."\"");
364
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)."\"");
372
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)."\"");
379
380         cmd("DestroyFunc KillMenuMPD");
381         cmd("AddToFunc   KillMenuMPD I Nop");
382 }
383
384 # Finished.
385 print $sock "close\n";
386
387 sub sanitise
388 {
389         $_ = $_[0];
390         s/&/&&/g if ($_[1]);
391         s/([\$@%^*])/\1\1/g;
392         s/"/\\"/g;
393         return $_;
394 }
395
396 sub addalbumentry
397 {
398         my ($playlist, $entry) = @_;
399
400         push(@$playlist, $entry);
401
402         if (defined $artist && $artist eq $entry->{Artist}) {
403                 my $albumdir = albumdir($entry->{file});
404                 $accept{$albumdir} = 1;
405         }
406 }
407
408 sub albumdir
409 {
410         my $file = $_[0];
411
412         $file =~ s:(/Disk [0-9]+[^/]*)?/[^/]*$::;
413         return $file
414 }
415
416 sub albumsort
417 {
418         return ($a->{Disc} <=> $b->{Disc}) if ($a->{Disc} != $b->{Disc});
419         return ($a->{Track} <=> $b->{Track});
420 }
421
422 sub titlesort
423 {
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});
427 }
428
429 sub shellify
430 {
431         my ($str, $quoted) = @_;
432         $str =~ s/'/'\\''/g;
433         if ($quoted) {
434                 $str =~ s/\\/\\\\/g;
435                 $str =~ s/"/\\"/g;
436         }
437         return "'$str'";
438 }