]> git.draconx.ca Git - fvwmconf.git/blob - scripts/mpdmenu.pl
mpdmenu: Use a specialized script to send playid commands to MPD.
[fvwmconf.git] / scripts / mpdmenu.pl
1 #!/usr/bin/perl
2
3 use strict;
4
5 use Getopt::Long;
6 use IO::Socket::INET6;
7
8 use constant {
9         MPD_MJR_MIN => 0,
10         MPD_MNR_MIN => 13,
11         MPD_REV_MIN => 0,
12 };
13
14 use utf8;
15 use encoding 'utf8';
16 use Encode;
17
18 sub cmd
19 {
20         print "$_[0]\n";
21 }
22
23 # Global hash for tracking what is to be "accepted".
24 my %accept = ();
25
26 my $FVWM = (defined $ENV{FVWM_USERDIR}) ? $ENV{FVWM_USERDIR}
27                                         : $ENV{HOME}."/.fvwm";
28 my $icons = "$FVWM/icons";
29
30 # Default values for stuff.
31 my ($album, $artist, $title, $menu) = (undef, undef, undef, undef);
32 my $host = (defined $ENV{MPD_HOST}) ? $ENV{MPD_HOST} : "localhost";
33 my $port = (defined $ENV{MPD_PORT}) ? $ENV{MPD_PORT} : "6600";
34
35 GetOptions(
36         'host|h=s'   => \&host,   # Host that MPD is running on.
37         'port|p=s'   => \&port,   # Port that MPD is listening on.
38         'menu|m=s'   => \$menu,   # Name of the menu to create.
39         'album=s'    => \$album,  # Album to get tracks from
40         'artist=s'   => \$artist, # Artist to limit results to
41         'title=s'    => \$title,  # Title to create menu for
42 );
43
44 $album  = decode_utf8($album)  if defined($album);
45 $artist = decode_utf8($artist) if defined($artist);
46 $title  = decode_utf8($title)  if defined($title);;
47
48 # Connect to MPD.
49 my $sock = new IO::Socket::INET6(
50         PeerAddr => $host,
51         PeerPort => $port,
52         Proto => 'tcp'
53 ) or die("could not open socket: $!.\n");
54 binmode($sock, ":utf8");
55
56 die("could not connect to MPD: $!.\n")
57         if (!(<$sock> =~ /^OK MPD ([0-9]+)\.([0-9]+)\.([0-9]+)$/));
58
59 die("MPD version $1.$2.$3 insufficient.\n")
60         if (  ($1 <  MPD_MJR_MIN)
61            || ($1 == MPD_MJR_MIN && $2 <  MPD_MNR_MIN)
62            || ($1 == MPD_MJR_MIN && $2 == MPD_MNR_MIN && $3 < MPD_REV_MIN));
63
64 if (defined $album) {
65         # Create an album menu.
66         my @playlist = ();
67         my $entry;
68
69         $menu = "MenuMPDAlbum" unless defined $menu;
70
71         $album =~ s/"/\\"/g;
72         print $sock "playlistfind album \"$album\"\n";
73         while (<$sock>) {
74                 last if (/^OK/);
75                 die($_) if (/^ACK/);
76
77                 if (/^(\w+): (.*)$/) {
78                         if ($1 eq "file") {
79                                 if (keys(%$entry) > 0) {
80                                         addalbumentry(\@playlist, $entry)
81                                 }
82
83                                 $entry = {};
84                         }
85
86                         $entry->{$1} = $2;
87                 }
88         }
89         addalbumentry(\@playlist, $entry) if (keys(%$entry) > 0);
90
91         die("No tracks found.\n") if (!@playlist);
92         foreach (sort albumsort @playlist) {
93                 my ($t_file, $t_trackno, $t_artist, $t_title, $t_id) = (
94                         $_->{file},
95                         $_->{Track},
96                         $_->{Artist},
97                         $_->{Title},
98                         $_->{Id},
99                 );
100
101                 next if (defined $artist && !$accept{albumdir($t_file)});
102
103                 $t_artist = sanitise($t_artist, 0);
104                 $t_title  = sanitise($t_title, 0);
105
106                 my $cmd = sprintf "AddToMenu $menu \"%d\t%s - %s\""
107                                   ." Exec exec $FVWM/scripts/mpdexec.pl"
108                                   ." playid %d",
109                                   $t_trackno, $t_artist, $t_title, $t_id;
110
111                 cmd($cmd);
112         }
113 } elsif (defined $artist) {
114         # Create an artist menu.
115         my %albums = ();
116         my $file;
117         my $quoteartist = $artist;
118
119         $menu = "MenuMPDArtist" unless defined $menu;
120
121         $quoteartist =~ s/"/\\"/g;
122         print $sock "playlistfind artist \"$quoteartist\"\n";
123         while (<$sock>) {
124                 last if (/^OK/);
125                 die($_) if (/^ACK/);
126
127                 if (/^(\w+): (.*)$/) {
128                         $file       = $2    if ($1 eq "file");
129                         $albums{$2} = $file if ($1 eq "Album");
130                 }
131         }
132
133         die("No albums found.\n") if (!keys(%albums));
134
135 { # work around 'use locale' breaking s///i
136         my $i = 0;
137         use locale;
138         foreach (sort keys(%albums)) {
139                 my $key      = $_;
140                 my $a_album  = sanitise($key, 1);
141
142                 open THUMB, "-|", "$FVWM/scripts/thumbnail.zsh",
143                                          "--small", "--music", $albums{$key};
144                 my $thumb = <THUMB>;
145                 close THUMB;
146                 die("Incompetent use of thumbnail.zsh") if ($?);
147
148                 $thumb =~ s/\n//sg;
149                 $thumb = "%$thumb%" if (-f $thumb);
150
151                 cmd("AddToMenu $menu \"$thumb$a_album\" Popup MenuMPDArt_$i");
152
153                 cmd("AddToMenu MenuMPDArt_$i DynamicPopUpAction MakeMenuMPDArt_$i");
154
155                 cmd("DestroyFunc MakeMenuMPDArt_$i");
156                 cmd("AddToFunc   MakeMenuMPDArt_$i
157                      + I DestroyMenu MenuMPDArt_$i
158                      + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
159                            ."--menu MenuMPDArt_$i "
160                            ."--album  ".shellify($key, 1)." "
161                            ."--artist ".shellify($artist, 1)."\"");
162
163                 cmd("AddToFunc KillMenuMPD I DestroyMenu MenuMPDArt_$i");
164                 cmd("AddToFunc KillMenuMPD I DestroyFunc MakeMenuMPDArt_$i");
165
166                 $i++;
167         }
168 } # end use locale workaround
169 } elsif (defined $title) {
170         # Create a title menu.
171         my @titles;
172         my $entry;
173
174         $menu = "MenuMPDTitle" unless defined $menu;
175
176         # Open and close brackets.
177         my ($ob, $cb) = ("[\[~〜<〈(ー−-]", "[\]~〜>〉)ー−-]");
178
179         $_ = $title;
180
181         # Deal with specific cases.
182         s/ちいさな(?=ヘミソフィア)//;                 # ヘミソフィア
183         s/ "mix on air flavor" dear EIKO SHIMAMIYA//; # Spiral wind
184         s/ "So,you need me" Style//;                  # I need you
185         s/ ::Symphony Second movement:://;            # Disintegration
186         s/-\[instrumental\]//;                        # 青い果実
187         s/ -Practice Track-//;                        # Fair Heaven
188         s/〜世界で一番アナタが好き〜//;               # Pure Heart
189         s/〜彼方への哀歌//;                           # 十二幻夢
190         s/ sora no uta ver.//;                       # 美しい星
191
192         s/\s*-remix-$//; # Otherwise "D-THREAD -remix-" doesn't work right.
193
194         # Deal with titles like "blah (ABC version)".
195         s/\s*$ob.*(style|mix|edit|edition|ver\.?|version|melody|カラオケ)$cb?$//i;
196
197         # Deal with titles like "blah (without XYZ)".
198         s/\s*$ob\s*((e\.)?piano|english|japanese|inst|tv|without|w\/o|off|back|short|karaoke|game).*//i;
199
200         # Deal with titles like "blah instrumental".
201         s/\s+(instrumental|off vocal|short|tv)([\s-]+(mix|size|version))?$//i;
202         s/\s+without\s+\w+$//i;
203
204         # Deal with separate movements in classical pieces.
205         s/: [IVX]+\..*//;
206
207         my $basetitle  = $_;
208         my $_basetitle = $basetitle;
209
210         $_basetitle =~ s/"/\\"/g;
211         print $sock "playlistsearch title \"$_basetitle\"\n";
212         while (<$sock>) {
213                 last if (/^OK/);
214                 die($_) if (/^ACK/);
215
216                 if (/^(\w+): (.*)$/) {
217                         if ($1 eq "file") {
218                                 push @titles, $entry if (keys(%$entry) > 0);
219                                 $entry = {};
220                         }
221
222                         $entry->{$1} = $2;
223                 }
224         }
225         push @titles, $entry if (keys(%$entry) > 0);
226
227 { # work around 'use locale' breaking s///i
228         use locale;
229         foreach (sort titlesort @titles) {
230                 my ($t_file, $t_artist, $t_title, $t_id) = (
231                         $_->{file},
232                         $_->{Artist},
233                         $_->{Title},
234                         $_->{Id},
235                 );
236
237                 # MPD searches are case-insensitive.
238                 next if (!($t_title =~ m/(\P{Latin}|^)\Q$basetitle\E(\P{Latin}|$)/ || $t_title =~ m/\Q$basetitle\E/i));
239
240                 $t_artist = sanitise($t_artist, 1);
241                 $t_title  = sanitise($t_title, 1);
242
243                 open THUMB, "-|", "$FVWM/scripts/thumbnail.zsh",
244                                          "--small", "--music", $t_file;
245                 my $thumb = <THUMB>;
246                 close(THUMB);
247                 die("Incompetent use of thumbnail.zsh") if ($?);
248
249                 $thumb =~ s/\n//sg;
250                 $thumb = "%$thumb%" if (-f $thumb);
251
252                 cmd("AddToMenu $menu \"$thumb$t_artist - $t_title\""
253                     ." Exec exec $FVWM/scripts/mpdexec.pl"
254                     ." playid $t_id");
255         }
256 } # end use locale workaround
257 } else {
258         # Make MPD base menu
259         my ($state, $songid) = (undef, undef);
260         my %entry = ();
261
262         $menu = "MenuMPD" unless defined $menu;
263
264         print $sock "status\n";
265         while (<$sock>) {
266                 last if (/^OK/);
267                 die($_) if (/^ACK/);
268
269                 if (/^(\w+): (.*)$/) {
270                         $state  = $2 if ($1 eq "state");
271                         $songid = $2 if ($1 eq "songid");
272                 }
273         }
274         die("Failed status query\n") unless (defined $state);
275
276         cmd("AddToMenu $menu Playing Title") if ($state eq "play");
277         cmd("AddToMenu $menu Paused Title")  if ($state eq "pause");
278         cmd("AddToMenu $menu Stopped Title") if ($state eq "stop");
279
280         if (defined $songid) {
281                 print $sock "playlistid $songid\n";
282                 while (<$sock>) {
283                         last if (/^OK/);
284                         die($_) if (/^ACK/);
285
286                         if (/^(\w+): (.*)$/) {
287                                 $entry{$1} = $2;
288                         }
289                 }
290                 die("Failed data query\n") unless (keys(%entry) > 0);
291
292                 open THUMB, "-|", "$FVWM/scripts/thumbnail.zsh",
293                                          "--image", "--music",  $entry{file};
294                 my $thumb = <THUMB>;
295                 my $scan  = <THUMB>;
296                 close(THUMB);
297                 die("Incompetent use of thumbnail.sh") if ($?);
298
299                 $thumb =~ s/\n//sg;
300                 $scan  =~ s/\n//sg;
301
302                 if (-f $thumb) {
303                         cmd("AddToMenu $menu \"*$thumb*\" "
304                                 ."Exec exec geeqie ".shellify($scan, 0));
305                 }
306                 cmd("AddToMenu $menu \"Title:   ".sanitise($entry{Title}, 0)
307                         ."\" Popup MenuMPDTitle");
308                 cmd("AddToMenu $menu \"Artist:  ".sanitise($entry{Artist}, 0)
309                         ."\" Popup MenuMPDArtist");
310                 cmd("AddToMenu $menu \"Album:   ".sanitise($entry{Album}, 0)
311                         ."\" Popup MenuMPDAlbum");
312                 cmd("AddToMenu $menu \"\" Nop");
313         } else {
314                 cmd("AddToMenu $menu \"<Song info unavailable>\"");
315                 cmd("AddToMenu $menu \"\" Nop");
316         }
317
318         if ($state eq "play" || $state eq "pause") {
319                 cmd("AddToMenu $menu \"\t\tNext%$icons/next.svg:16x16%\" "
320                         ."Exec exec mpc next");
321                 cmd("AddToMenu $menu \"\t\tPause%$icons/pause.svg:16x16%\" "
322                         ."Exec exec mpc pause") if ($state eq "play");
323                 cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" "
324                         ."Exec exec mpc play") if ($state eq "pause");
325                 cmd("AddToMenu $menu \"\t\tStop%$icons/stop.svg:16x16%\" "
326                         ."Exec exec mpc stop");
327                 cmd("AddToMenu $menu \"\t\tPrev%$icons/prev.svg:16x16%\" "
328                         ."Exec exec mpc prev");
329         } elsif ($state eq "stop") {
330                 cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" "
331                         ."Exec exec mpc play");
332         } else {
333                 die("Unknown MPD state!\n");
334         }
335
336         cmd("AddToMenu $menu \"\" Nop");
337         cmd("AddToMenu $menu \"\t\tShuffle%$icons/shuffle.svg:16x16%\" "
338                 ."Exec exec mpc shuffle");
339
340         cmd("DestroyMenu MenuMPDTitle");
341         cmd("AddToMenu   MenuMPDTitle  DynamicPopUpAction MakeMenuMPDTitle");
342         cmd("DestroyMenu MenuMPDArtist");
343         cmd("AddToMenu   MenuMPDArtist DynamicPopUpAction MakeMenuMPDArtist");
344         cmd("DestroyMenu MenuMPDAlbum");
345         cmd("AddToMenu   MenuMPDAlbum  DynamicPopUpAction MakeMenuMPDAlbum");
346
347         cmd("DestroyFunc MakeMenuMPDTitle");
348         cmd("AddToFunc   MakeMenuMPDTitle
349              + I DestroyMenu MenuMPDTitle
350              + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
351                            ."--menu MenuMPDTitle "
352                            ."--title ".shellify($entry{Title}, 1)."\"");
353
354         cmd("DestroyFunc MakeMenuMPDAlbum");
355         cmd("AddToFunc   MakeMenuMPDAlbum
356              + I DestroyMenu MenuMPDAlbum
357              + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
358                            ."--menu MenuMPDAlbum "
359                            ."--album  ".shellify($entry{Album}, 1)." "
360                            ."--artist ".shellify($entry{Artist}, 1)."\"");
361
362         cmd("DestroyFunc MakeMenuMPDArtist");
363         cmd("AddToFunc   MakeMenuMPDArtist
364              + I DestroyMenu MenuMPDArtist
365              + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
366                            ."--menu MenuMPDArtist "
367                            ."--artist ".shellify($entry{Artist}, 1)."\"");
368
369         cmd("DestroyFunc KillMenuMPD");
370         cmd("AddToFunc   KillMenuMPD I Nop");
371 }
372
373 # Finished.
374 print $sock "close\n";
375
376 sub sanitise
377 {
378         $_ = $_[0];
379         s/&/&&/g if ($_[1]);
380         s/([\$@%^*])/\1\1/g;
381         s/"/\\"/g;
382         return $_;
383 }
384
385 sub addalbumentry
386 {
387         my ($playlist, $entry) = @_;
388
389         push(@$playlist, $entry);
390
391         if (defined $artist && $artist eq $entry->{Artist}) {
392                 my $albumdir = albumdir($entry->{file});
393                 $accept{$albumdir} = 1;
394         }
395 }
396
397 sub albumdir
398 {
399         my $file = $_[0];
400
401         $file =~ s:(/Disk [0-9]+[^/]*)?/[^/]*$::;
402         return $file
403 }
404
405 sub albumsort
406 {
407         return ($a->{Disc} <=> $b->{Disc}) if ($a->{Disc} != $b->{Disc});
408         return ($a->{Track} <=> $b->{Track});
409 }
410
411 sub titlesort
412 {
413         return ($a->{Album}  cmp $b->{Album})  if($a->{Album}  ne $b->{Album});
414         return ($a->{Artist} cmp $b->{Artist}) if($a->{Artist} ne $b->{Artist});
415         return ($a->{Title}  cmp $b->{Title});
416 }
417
418 sub shellify
419 {
420         my ($str, $quoted) = @_;
421         $str =~ s/'/'\\''/g;
422         if ($quoted) {
423                 $str =~ s/\\/\\\\/g;
424                 $str =~ s/"/\\"/g;
425         }
426         return "'$str'";
427 }