#!/usr/bin/perl
+#
+# Copyright © 2008,2010,2012,2019 Nick Bowler
+#
+# Silly little script to generate an FVWM menu with various bits of MPD
+# status information and controls.
+#
+# License GPLv3+: GNU General Public License version 3 or any later version.
+# This is free software: you are free to change and redistribute it.
+# There is NO WARRANTY, to the extent permitted by law.
use strict;
use Getopt::Long;
use IO::Socket::INET6;
+use Scalar::Util qw(reftype);
+use FindBin;
use constant {
MPD_MJR_MIN => 0,
- MPD_MNR_MIN => 13,
+ MPD_MNR_MIN => 21,
MPD_REV_MIN => 0,
};
use utf8;
-use encoding 'utf8';
+use open qw(:std :utf8);
+binmode(STDOUT, ":utf8");
use Encode;
+my $SELF = "$FindBin::Bin/$FindBin::Script";
+my $MUSIC = $ENV{MUSIC} // "/srv/music";
+
+# get_item_thumbnails({ options }, file, ...)
+# get_item_thumbnails(file, ...)
+#
+# For each music file listed, obtain a thumbnail (if any) for the cover art.
+#
+# The first argument is a hash reference to control the mode of operation;
+# it may be omitted for default options.
+#
+# get_item_thumbnails({ small => 1 }, ...) - smaller thumbnails
+#
+# The returned list consists of strings (in the same order as the filename
+# arguments) suitable for use directly in FVWM menus; by default the filename
+# is bracketed by asterisks (e.g., "*thumbnail.png*"); in small mode it is
+# surrounded by % (e.g., "%thumbnail.png%"). If no cover art was found, the
+# empty string is returned for that file.
+sub get_item_thumbnails {
+ my @results = ();
+ my $flags = {};
+ my @opts = ();
+
+ $flags = shift if (reftype($_[0]) eq "HASH");
+ return @results unless @_;
+
+ my $c = "*";
+ if ($flags->{small}) {
+ push @opts, "--small";
+ $c = "%";
+ }
+
+ open THUMB, "-|", "$FindBin::Bin/mpdthumb.sh", @opts, "--", @_;
+ foreach (@_) {
+ my $thumb = <THUMB>;
+ chomp $thumb;
+
+ $thumb = "$c$thumb$c" if (-f $thumb);
+ push @results, $thumb;
+ }
+ close THUMB;
+ die("mpdthumb failed") if ($?);
+
+ return @results;
+}
+
+# Given a music filename, search for the cover art in the same directory.
+sub mpd_cover_filename {
+ my ($dir) = @_;
+ my $file;
+
+ $dir =~ s/\/[^\/]*$//;
+ foreach ("cover.png", "cover.jpg", "cover.tiff", "cover.bmp") {
+ if (-f "$dir/$_") {
+ $file = "$dir/$_";
+ last;
+ }
+ }
+ return unless defined $file;
+
+ # Follow one level of symbolic link to get to the scans directory.
+ $file = readlink($file) // $file;
+ $file = "$dir/$file" unless ($file =~ /^\//);
+ return $file;
+}
+
sub cmd
{
print "$_[0]\n";
my $sock = new IO::Socket::INET6(
PeerAddr => $host,
PeerPort => $port,
- Proto => 'tcp'
+ Proto => 'tcp',
+ Timeout => 2
) or die("could not open socket: $!.\n");
binmode($sock, ":utf8");
my $entry;
$menu = "MenuMPDAlbum" unless defined $menu;
-
+
$album =~ s/"/\\"/g;
print $sock "playlistfind album \"$album\"\n";
while (<$sock>) {
last if (/^OK/);
die($_) if (/^ACK/);
-
+
if (/^(\w+): (.*)$/) {
if ($1 eq "file") {
if (keys(%$entry) > 0) {
addalbumentry(\@playlist, $entry)
}
-
+
$entry = {};
}
-
+
$entry->{$1} = $2;
}
}
addalbumentry(\@playlist, $entry) if (keys(%$entry) > 0);
-
+
die("No tracks found.\n") if (!@playlist);
foreach (sort albumsort @playlist) {
my ($t_file, $t_trackno, $t_artist, $t_title, $t_id) = (
$_->{Title},
$_->{Id},
);
-
+
next if (defined $artist && !$accept{albumdir($t_file)});
-
- $t_artist = sanitise($t_artist);
- $t_title = sanitise($t_title);
-
+
+ $t_artist = sanitise($t_artist, 0);
+ $t_title = sanitise($t_title, 0);
+
my $cmd = sprintf "AddToMenu $menu \"%d\t%s - %s\""
- ." Exec mpc playid %d",
+ ." Exec exec $FindBin::Bin/mpdexec.pl"
+ ." playid %d",
$t_trackno, $t_artist, $t_title, $t_id;
-
+
cmd($cmd);
}
} elsif (defined $artist) {
while (<$sock>) {
last if (/^OK/);
die($_) if (/^ACK/);
-
+
if (/^(\w+): (.*)$/) {
$file = $2 if ($1 eq "file");
$albums{$2} = $file if ($1 eq "Album");
{ # work around 'use locale' breaking s///i
my $i = 0;
use locale;
- foreach (sort keys(%albums)) {
- my $key = $_;
- my $a_album = sanitise($key);
- open THUMB, "-|", "$FVWM/scripts/thumbnail.sh",
- "--small", "--music", $albums{$key};
- my $thumb = <THUMB>;
- close THUMB;
- die("Incompetent use of thumbnail.sh") if ($?);
+ my @keys = sort keys %albums;
+ my @thumbs = get_item_thumbnails({ small => 1 },
+ map { $albums{$_} } @keys);
- $thumb =~ s/\n//sg;
- $thumb = "%$thumb%" if (-f $thumb);
+ foreach my $key (@keys) {
+ my $a_album = sanitise($key, 1);
+ my $thumb = shift @thumbs;
cmd("AddToMenu $menu \"$thumb$a_album\" Popup MenuMPDArt_$i");
cmd("DestroyFunc MakeMenuMPDArt_$i");
cmd("AddToFunc MakeMenuMPDArt_$i
+ I DestroyMenu MenuMPDArt_$i
- + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
+ + I -PipeRead \"exec $SELF "
."--menu MenuMPDArt_$i "
."--album ".shellify($key, 1)." "
."--artist ".shellify($artist, 1)."\"");
$menu = "MenuMPDTitle" unless defined $menu;
# Open and close brackets.
- my ($ob, $cb) = ("[\[~〜<(ー−-]", "[\]~〜>)ー−-]");
+ my ($ob, $cb) = ("[\[~〜<〈(ー−-]", "[\]~〜>〉)ー−-]");
$_ = $title;
s/ -Practice Track-//; # Fair Heaven
s/〜世界で一番アナタが好き〜//; # Pure Heart
s/〜彼方への哀歌//; # 十二幻夢
+ s/ sora no uta ver.//; # 美しい星
s/\s*-remix-$//; # Otherwise "D-THREAD -remix-" doesn't work right.
# Deal with titles like "blah (ABC version)".
- s/\s*$ob.*(style|mix|edit|edition|ver\.?|version|カラオケ)$cb?$//i;
+ s/\s*$ob.*(style|mix|edit|edition|ver\.?|version|melody|カラオケ)$cb?$//i;
# Deal with titles like "blah (without XYZ)".
- s/\s*$ob((e\.)?piano|english|japanese|inst|tv|without|w\/o|off|back|short|karaoke|game).*//i;
+ s/\s*$ob\s*((e\.)?piano|english|japanese|inst|tv|without|w\/o|off|back|short|karaoke|game).*//i;
# Deal with titles like "blah instrumental".
- s/\s+(instrumental|off vocal|short)(\s+(size|version|s))?$//i;
+ s/\s+(instrumental|off vocal|short|tv)([\s-]+(mix|size|version))?$//i;
s/\s+without\s+\w+$//i;
+ # Deal with separate movements in classical pieces.
+ s/: [IVX]+\..*//;
+
my $basetitle = $_;
my $_basetitle = $basetitle;
while (<$sock>) {
last if (/^OK/);
die($_) if (/^ACK/);
-
+
if (/^(\w+): (.*)$/) {
if ($1 eq "file") {
push @titles, $entry if (keys(%$entry) > 0);
$entry = {};
}
-
+
$entry->{$1} = $2;
}
}
{ # work around 'use locale' breaking s///i
use locale;
+
+ my @thumbs = get_item_thumbnails({ small => 1 },
+ map { $_->{file} } @titles);
+ for (my $i = 0; $i < @titles; $i++) {
+ $titles[$i]->{thumb} = $thumbs[$i];
+ }
+
foreach (sort titlesort @titles) {
- my ($t_file, $t_artist, $t_title, $t_id) = (
+ my ($t_file, $t_artist, $t_title, $t_id, $thumb) = (
$_->{file},
$_->{Artist},
$_->{Title},
$_->{Id},
+ $_->{thumb}
);
# MPD searches are case-insensitive.
- next if (!($t_title =~ m/(\P{Latin}|^)\Q$basetitle\E(\P{Latin}|$)/));
- $t_artist = sanitise($t_artist);
- $t_title = sanitise($t_title);
+ next if (!($t_title =~ m/(\P{Latin}|^)\Q$basetitle\E(\P{Latin}|$)/ || $t_title =~ m/\Q$basetitle\E/i));
- open THUMB, "-|", "$FVWM/scripts/thumbnail.sh",
- "--small", "--music", $t_file;
- my $thumb = <THUMB>;
- close(THUMB);
- die("Incompetent use of thumbnail.sh") if ($?);
+ $t_artist = sanitise($t_artist, 1);
+ $t_title = sanitise($t_title, 1);
- $thumb =~ s/\n//sg;
- $thumb = "%$thumb%" if (-f $thumb);
-
- cmd("AddToMenu $menu \"$thumb$t_artist - $t_title\" Exec mpc playid $t_id");
+ cmd("AddToMenu $menu \"$thumb$t_artist - $t_title\""
+ ." Exec exec $FindBin::Bin/mpdexec.pl"
+ ." playid $t_id");
}
} # end use locale workaround
} else {
$songid = $2 if ($1 eq "songid");
}
}
- die("Failed status query\n") unless (defined $state && defined $songid);
+ die("Failed status query\n") unless (defined $state);
- print $sock "playlistid $songid\n";
- while (<$sock>) {
- last if (/^OK/);
- die($_) if (/^ACK/);
+ cmd("AddToMenu $menu Playing Title") if ($state eq "play");
+ cmd("AddToMenu $menu Paused Title") if ($state eq "pause");
+ cmd("AddToMenu $menu Stopped Title") if ($state eq "stop");
- if (/^(\w+): (.*)$/) {
- $entry{$1} = $2;
+ if (defined $songid) {
+ print $sock "playlistid $songid\n";
+ while (<$sock>) {
+ last if (/^OK/);
+ die($_) if (/^ACK/);
+
+ if (/^(\w+): (.*)$/) {
+ $entry{$1} = $2;
+ }
}
- }
- die("Failed data query\n") unless (keys(%entry) > 0);
+ die("Failed data query\n") unless (keys(%entry) > 0);
- open THUMB, "-|", "$FVWM/scripts/thumbnail.sh",
- "--image", "--music", $entry{file};
- my $thumb = <THUMB>;
- my $scan = <THUMB>;
- close(THUMB);
- die("Incompetent use of thumbnail.sh") if ($?);
+ my ($thumb) = get_item_thumbnails($entry{file});
+ if ($thumb) {
+ my $cover = mpd_cover_filename("$MUSIC/$entry{file}");
- $thumb =~ s/\n//sg;
- $scan =~ s/\n//sg;
+ cmd("AddToMenu $menu \"$thumb\" "
+ ."Exec exec geeqie ".shellify($cover, 0));
+ }
- cmd("AddToMenu $menu Playing Title") if ($state eq "play");
- cmd("AddToMenu $menu Paused Title") if ($state eq "pause");
- cmd("AddToMenu $menu Stopped Title") if ($state eq "stop");
- if (-f $thumb) {
- cmd("AddToMenu $menu \"*$thumb*\" "
- ."Exec exec gqview ".shellify($scan, 0));
+ cmd("AddToMenu $menu \"Title: ".sanitise($entry{Title}, 0)
+ ."\" Popup MenuMPDTitle");
+ cmd("AddToMenu $menu \"Artist: ".sanitise($entry{Artist}, 0)
+ ."\" Popup MenuMPDArtist");
+ cmd("AddToMenu $menu \"Album: ".sanitise($entry{Album}, 0)
+ ."\" Popup MenuMPDAlbum");
+ cmd("AddToMenu $menu \"\" Nop");
+ } else {
+ cmd("AddToMenu $menu \"<Song info unavailable>\"");
+ cmd("AddToMenu $menu \"\" Nop");
}
- cmd("AddToMenu $menu \"Title: ".sanitise($entry{Title})."\" "
- ."Popup MenuMPDTitle");
- cmd("AddToMenu $menu \"Artist: ".sanitise($entry{Artist})."\" "
- ."Popup MenuMPDArtist");
- cmd("AddToMenu $menu \"Album: ".sanitise($entry{Album})."\" "
- ."Popup MenuMPDAlbum");
- cmd("AddToMenu $menu \"\" Nop");
if ($state eq "play" || $state eq "pause") {
cmd("AddToMenu $menu \"\t\tNext%$icons/next.svg:16x16%\" "
- ."Exec exec mpc next");
+ ."Exec exec $FindBin::Bin/mpdexec.pl next");
cmd("AddToMenu $menu \"\t\tPause%$icons/pause.svg:16x16%\" "
- ."Exec exec mpc pause") if ($state eq "play");
+ ."Exec exec $FindBin::Bin/mpdexec.pl pause");
cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" "
- ."Exec exec mpc play") if ($state eq "pause");
+ ."Exec exec $FindBin::Bin/mpdexec.pl play");
cmd("AddToMenu $menu \"\t\tStop%$icons/stop.svg:16x16%\" "
- ."Exec exec mpc stop");
+ ."Exec exec $FindBin::Bin/mpdexec.pl stop");
cmd("AddToMenu $menu \"\t\tPrev%$icons/prev.svg:16x16%\" "
- ."Exec exec mpc prev");
+ ."Exec exec $FindBin::Bin/mpdexec.pl previous");
} elsif ($state eq "stop") {
cmd("AddToMenu $menu \"\t\tPlay%$icons/play.svg:16x16%\" "
- ."Exec exec mpc play");
+ ."Exec exec $FindBin::Bin/mpdexec.pl play");
} else {
die("Unknown MPD state!\n");
}
cmd("AddToMenu $menu \"\" Nop");
cmd("AddToMenu $menu \"\t\tShuffle%$icons/shuffle.svg:16x16%\" "
- ."Exec exec mpc shuffle");
+ ."Exec exec $FindBin::Bin/mpdexec.pl shuffle");
cmd("DestroyMenu MenuMPDTitle");
cmd("AddToMenu MenuMPDTitle DynamicPopUpAction MakeMenuMPDTitle");
cmd("DestroyFunc MakeMenuMPDTitle");
cmd("AddToFunc MakeMenuMPDTitle
+ I DestroyMenu MenuMPDTitle
- + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
+ + I -PipeRead \"exec $SELF "
."--menu MenuMPDTitle "
."--title ".shellify($entry{Title}, 1)."\"");
cmd("DestroyFunc MakeMenuMPDAlbum");
cmd("AddToFunc MakeMenuMPDAlbum
+ I DestroyMenu MenuMPDAlbum
- + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
+ + I -PipeRead \"exec $SELF "
."--menu MenuMPDAlbum "
."--album ".shellify($entry{Album}, 1)." "
."--artist ".shellify($entry{Artist}, 1)."\"");
-
+
cmd("DestroyFunc MakeMenuMPDArtist");
cmd("AddToFunc MakeMenuMPDArtist
+ I DestroyMenu MenuMPDArtist
- + I -PipeRead \"exec $FVWM/scripts/mpdmenu.pl "
+ + I -PipeRead \"exec $SELF "
."--menu MenuMPDArtist "
."--artist ".shellify($entry{Artist}, 1)."\"");
sub sanitise
{
$_ = $_[0];
- s/([\$&@%^*])/\1\1/g;
+ s/&/&&/g if ($_[1]);
+ s/([\$@%^*])/\1\1/g;
s/"/\\"/g;
return $_;
}