10 use Time
::HiRes
qw(usleep);
12 BEGIN { *HAVE_XCHAT
= Xchat
->can('register') ?
sub {1} : sub {0}; *HAVE_IRSSI
= Irssi
->can('command_bind') ?
sub{1} : sub{0}; }
14 Xchat
::register
("foo_spam","0.5.1", "Prints the current playing song from foobar2000.", \
&close_telnet
) if (HAVE_XCHAT
);
16 # 0.5.2 - Added discnumber and totaldiscs tags. Changed default format. Silences a warning when a function ends on ",)". Fixed two warnings in the $if family.
17 # 0.5.1 - Fixed $if, $if2, $and, $or and $xor behavior on certain strings.
18 # 0.5 - Support subfunctions and tags with underlines. Changed some other details.
19 # 0.4 - Fixed UTF-8 corruption issues. Allow the user to specify a comment when using /aud by giving it as an argument. Document build_output.
20 # 0.3.2 - Change the method used to read foobar2000's response. The previous method would hang once in a while.
21 # 0.3.1 - Change default settings to avoid breakage if a track has | on one of the tags. Update documentation.
22 # 0.3 - Allow customization of the format string. Changed method of desync handling.
23 # 0.2.2 - Fix desync issues if foobar takes "too long" to respond. Added codec and bitrate to the output.
24 # 0.2.1 - Forgot to handle one error case on the telnet connection.
25 # 0.2 - Changed the recommended string and output. Fixed my wrong XChat API usage. Changed the way the telnet connection is handled.
29 # Doesn't support tags that are equal to "?" (foo_controlserver limitation).
30 # The progress bar produced by $bar() is imprecise.
31 # Missing documentation for $functions().
34 # Replace the current format syntax by foobar2000's title format
38 our $default_format = 'player[ (version)]: [albumartist ]\[[date ][album$ifgreater(totaldiscs,1,[\' - Disc \'discnumber\/totaldiscs],)][ #track[/totaltracks]]\] [trackartist - ]title \[position[/length]\][ bitrate\'kbps\'][ codec[ codec_profile]][ <-- comment]';
39 our $format = $default_format;
41 our $setting_file = undef; # Only used by Xchat
44 $telnet = new Net
::Telnet
(Port
=> 3333, Timeout
=> 10, Errmode
=> 'return') if not defined($telnet);
45 $telnet_open = $telnet->open("localhost");
46 unless($telnet_open) {
47 irc_print
("Error connecting to foobar2000! Make sure fb2k is running.");
48 irc_print
("Also check if foo_controlserver is properly configured.");
56 $telnet->put("exit\n");
62 return undef unless open_telnet
();
66 unless (defined($telnet->print("trackinfo"))) {
68 return undef unless open_telnet
();
71 my @result = $telnet->waitfor(Match
=> '/11[123]\|+.+?\|+.+?\|+(?!0\.[0-5][0-9]).*/', Timeout
=> 5);
73 $line = $result[1] if @result;
78 irc_print
("Error retrieving status from foobar2000!");
82 unless (eval { $line = decode
("UTF-8", $line, Encode
::FB_CROAK
) }) {
83 irc_print
("Error: line is not valid UTF-8. Check foo_controlserver's settings.");
89 if($line =~ /^11.\|\|\|/ and $line =~ /\|\|\|(.*?)\|\|\|$/) { # proper setting
90 @fields = split(/\|\|\|/, $line);
91 } else { # the luser didn't configure it correctly
92 $line =~ s/\|\|\|/\|/g; # fix possible half-configuration
93 @fields = split(/\|/, $line);
97 my $info = {player
=> 'foobar2000',
99 playlistindex
=> $fields[1],
100 trackindex
=> $fields[2],
101 pos_sec
=> $fields[3],
103 bitrate
=> $fields[5],
104 albumartist
=> $fields[6],
108 track
=> $fields[10],
109 title
=> $fields[11]};
110 if ($fields[15]) { # Compatibility with 0.1
111 $info->{'artist'} = $fields[12];
112 $info->{'totaltracks'} = $fields[13];
113 $info->{'position'} = $fields[14];
114 $info->{'length'} = $fields[15];
117 $info->{'version'} = $fields[16];
118 $info->{'version'} =~ s/^foobar2000 //;
121 $info->{'codec_profile'} = $fields[17] if ($fields[17]);
124 # New fields in 0.5.2
125 $info->{'discnumber'} = $fields[18];
126 $info->{'totaldiscs'} = $fields[19];
131 $info->{'state'} = "playing" if ($info->{'state'} eq "111");
132 $info->{'state'} = "paused" if ($info->{'state'} eq "113");
133 $info->{'state'} = "stopped" if ($info->{'state'} eq "112");
136 delete $info->{$_} if (defined($info->{$_}) and $info->{$_} eq '?');
139 $info->{'albumartist'} = $info->{'artist'} unless defined($info->{'albumartist'});
140 $info->{'trackartist'} = $info->{'artist'} if (defined($info->{'artist'}) and $info->{'albumartist'} ne $info->{'artist'});
142 if (defined($info->{'length'})) {
143 $info->{'len'} = $info->{'length'}; # Compatibility with 0.4
145 my ($h, $m, $s) = split(/\:/, $info->{'length'});
147 $info->{'len_sec'} = $s + $m * 60 + $h * 3600;
149 $info->{'len_sec'} = $m + $h * 60;
153 if (not defined($info->{'position'})) {
154 my ($s, $m, $h) = ($info->{'pos_sec'}, 0, 0);
163 $info->{'position'} = sprintf("%d:%02d:%02d", $h, $m, $s);
165 $info->{'position'} = sprintf("%d:%02d", $m, $s);
173 my ($format, $info, $sublevel, $parsed) = @_;
174 $sublevel = 0 if not defined $sublevel;
178 my @chars = split(//,$format);
180 # Language Definition
182 # lowercasestring <== should be parsed as a tag name, makes the expression fail if such tag is not defined
183 # [] <== brackets allow the parsing inside them to fail
184 # $func(arg1,arg2,...) <== function call (see parse_subfunction for details)
185 # '' <== string literal (ignores all parsing)
186 # \(character) <== literal character
190 # A bracket returns a defined value only if it has at least one tag or at least one of its embedded brackets return true.
202 for(my $i = 0; $i < @chars; $i++ ) { # 1st Pass (Lexical analysis, push into @tokens)
203 if ($chars[$i] eq "\\") { # Escaped character
204 $str .= "\\$chars[++$i]"; # Copy the next character without parsing
205 ++$sub if ($chars[$i] eq "[" and $sub > 0); # However, we still need to count [ and ]
206 --$sub if ($chars[$i] eq "]" and $sub > 0); # or the subexpression parser will give wrong results
207 } elsif($literal) { # If on literal mode
208 $str .= $chars[$i]; # Simply copy everything as-is until an unescaped ' is found
209 if ($chars[$i] eq "'" and ($chars[$i-1] ne "\\" or (defined($chars[$i-2]) and $chars[$i-2] ne "\\"))) {
213 } elsif (not defined($chars[$i+1])) { # This means we ended the string with an uneven number of unescaped 's
214 return "Malformed: mismatched ': $str";
216 } elsif ($sub) { # If on subexpression mode
217 $str .= $chars[$i]; # Copy everything as-is until an unescaped ] is found
218 if ($chars[$i] eq "[") { # We must copy any sub-subexpressions inside this sub-expression for recursive evaluation
220 } elsif ($chars[$i] eq "]" and $chars[$i-1] ne "\\" and --$sub == 0) {
223 } elsif (not defined($chars[$i+1])) { # This means we ended the string without $sub being 0
224 return "Malformed: mismatched [: $str";
226 } elsif ($tagmode) { # If on tag mode
227 $str .= $chars[$i]; # Copy tags as-is until any non-lowercase-alpha character is found
228 if (not defined($chars[$i+1]) or $chars[$i+1] !~ /[a-z_]/) {
233 } elsif ($func) { # If on function mode
234 $str .= $chars[$i]; # Copy everything until an unescaped ) is found
235 if ($chars[$i] eq "(" and $chars[$i-1] ne "\\") {
237 } elsif ($chars[$i] eq ")" and $chars[$i-1] ne "\\" and --$func <= 1) {
241 } elsif (not defined($chars[$i+1])) {
242 return "Malformed: mismatched (: $str";
245 if ($chars[$i] eq "'" and (not defined($chars[$i-1]) or $chars[$i-1] ne "\\")) {
246 push @tokens, "'$str'" if $str ne ""; # Found an opening quote
248 $literal = 1; # Enter literal mode
249 } elsif ($chars[$i] eq "[" and (not defined($chars[$i-1]) or $chars[$i-1] ne "\\")) {
250 push @tokens, "'$str'" if $str ne ""; # Found a subexpression opener
252 $sub = 1; # Enter subexpression mode
253 } elsif ($chars[$i] eq "\$" and (not defined($chars[$i-1]) or $chars[$i-1] ne "\\")) {
254 push @tokens, "'$str'" if $str ne "";
256 $func = 1; # Enter subfunction mode
257 } elsif ($chars[$i] =~ /[a-z]/) {
258 push @tokens, "'$str'" if $str ne ""; # Found a tag name
260 $tagmode = 1; # Enter tag mode
262 $str .= $chars[$i]; # Copy as a literal
267 push @tokens, "'$str'" if $str ne ""; # Make sure whatever is left from parsing is added as a literal
273 foreach my $token (@tokens) { # 2nd Pass, execute tokens
274 if ($token =~ /^'/) { # If the token is a literal, then
275 $token =~ s/^'//; # Strip the opening quote
276 $token =~ s/'$//; # And the closing one
277 $token =~ s/\\(.)/$1/g; # Remove the escape from all escaped characters
278 $output .= $token; # Copy to output
279 } elsif ($token =~ /^\[/) { # If the token is a subexpression
280 $token =~ s/^\[//; # Strip the opening [
281 $token =~ s/\]$//; # And the closing ]
282 my $recurse = parse_format
($token, $info, $sublevel+1); # Recurse
283 if (defined($recurse) and $recurse ne "") { # If the subexpression is true
284 $output .= $recurse; # Copy result to output
285 $sub++; # Count this as a valid subexpression
287 } elsif ($token =~ /^\$/) { # If the token is a subfunction
288 my $res = parse_subfunction
($token, $info, $sublevel);
289 if (defined($res) and $res ne "") {
293 } else { # If this is a tag
294 if (!defined($info->{$token})) { # If this tag is not defined
295 return undef; # Fail immediatly
297 $tagcount++; # Count this as a valid tag
298 $output .= $info->{$token}; # Copy value to output
302 $$parsed = ($tagcount or $sub or $func) if defined($parsed);
308 my ($format, $info, $sublevel) = @_;
309 $sublevel = 0 if not defined $sublevel;
312 my $output = parse_format
($format, $info, $sublevel, \
$sub);
314 return undef unless ($sub); # Fail if there are no tags and all subexpressions are false
319 sub parse_subfunction
{
320 my ($function, $info, $sublevel) = @_;
322 $function =~ /^\$(.*?)\((.*)\)$/;
326 my @rawargs = split(//, $2);
331 for(my $i = 0; $i < @rawargs; $i++) {
332 if ($i > 0 and $rawargs[$i-1] eq "\\") {
333 # Ignore everything else
335 --$parens if ($rawargs[$i] eq ")");
336 } elsif ($rawargs[$i] eq "(") {
338 } elsif ($rawargs[$i] eq ",") {
343 $str .= $rawargs[$i] if defined($rawargs[$i]);
347 for (my $i = 0; $i < @args; $i++) {
348 $args[$i] = parse_format
($args[$i], $info, $sublevel+1);
351 if ($func eq "bar") {
352 my $length = $info->{len_sec
};
353 my $pos = $info->{pos_sec
};
355 my ($len, $fill, $space, $filled) = @args;
356 $filled = 0 unless defined($filled);
358 my $fillpos = int($pos * $len / $length + 0.5);
363 for (my $i = 0; $i < $len; $i++) {
371 for (my $i = 0; $i < $len; $i++) {
372 if ($i == $fillpos) {
381 } elsif ($func eq "if" or $func eq "if2") {
382 my ($test, $iftrue, $iffalse);
384 ($test, $iftrue, $iffalse) = @args;
386 ($test, $iffalse) = @args;
395 } elsif ($func eq "ifgreater" or $func eq "ifequal" or $func eq "iflonger") {
396 my ($arg1, $arg2, $iftrue, $iffalse) = @args;
398 if (defined($arg1)) {
399 # Remove possible literal markers
404 if (defined($arg2)) {
408 return $iftrue if (defined($arg1));
411 return $iffalse unless (defined($arg1));
413 if ($func eq "iflonger") {
414 return $iftrue if (length($arg1) > length($arg2));
415 } elsif ($func eq "ifequal") {
416 return $iftrue if $arg1 == $arg2;
418 return $iftrue if $arg1 > $arg2;
421 } elsif ($func eq "abbr") {
422 my ($arg1, $arg2) = (0,0);
424 $arg2 = $args[1] if ($args[1]);
426 if (length($arg1) > $arg2) {
428 my @tokens = split(/\s+/, $arg1);
429 foreach my $token (@tokens) {
430 my @chars = split(//, $token);
436 } elsif ($func eq "num") {
437 my ($arg1, $arg2) = @args;
438 return undef unless $arg1;
439 return sprintf("%0${arg2}d", $arg1);
440 } elsif ($func =~ /^(add|sub|mul|div|mod|max|min)$/) {
441 my ($arg1, $arg2) = @args;
442 return undef unless (defined($arg1) and defined($arg2));
443 return $arg1 + $arg2 if ($func eq "add");
444 return $arg1 - $arg2 if ($func eq "sub");
445 return $arg1 * $arg2 if ($func eq "mul");
446 return $arg1 / $arg2 if ($func eq "div");
447 return $arg1 % $arg2 if ($func eq "mod");
448 return ($arg1 >= $arg2 ?
$arg1 : $arg2) if ($func eq "max");
449 return ($arg1 < $arg2 ?
$arg1 : $arg2) if ($func eq "min");
450 } elsif ($func =~ /^(and|or|xor|not)$/) {
451 my ($arg1, $arg2) = @args;
452 $arg1 = 0 unless defined $arg1;
453 $arg2 = 0 unless defined $arg2;
455 # Need to give explicit returns to avoid eating on parse_format
457 return ($arg1 ?
0 : 1) if ($func eq "not");
458 return (($arg1 && $arg2) ?
1 : 0) if ($func eq "and");
459 return (($arg1 || $arg2) ?
1 : 0) if ($func eq "or");
460 return (($arg1 && !$arg2) ?
1 : ((!$arg1 && $arg2) ?
1 : 0)) if ($func eq "xor");
461 } elsif ($func eq "strcmp") {
462 my ($arg1, $arg2) = @args;
463 return undef unless (defined($arg1) and defined($arg2));
464 return (($arg1 eq $arg2) ?
1 : 0);
471 my $info = get_track_info
();
472 $info->{comment
} = $_[0] if $_[0];
473 if (defined($info)) {
474 return build_output
($format, $info);
479 sub get_help_string
{
482 $fields = '%%codec%%|||%%bitrate%%|||%%album artist%%|||%%album%%|||%%date%%|||%%genre%%|||%%tracknumber%%|||%%title%%|||%%artist%%|||%%totaltracks%%|||%%playback_time%%|||%%length%%|||%%_foobar2000_version%%|||%%codec_profile%%|||%%discnumber%%|||%%totaldiscs%%';
484 $fields = '%codec%|||%bitrate%|||%album artist%|||%album%|||%date%|||%genre%|||%tracknumber%|||%title%|||%artist%|||%totaltracks%|||%playback_time%|||%length%|||%_foobar2000_version%|||%codec_profile%|||%discnumber%|||%totaldiscs%';
487 Required Plugin: foo_controlserver
488 URL: http://www.hydrogenaudio.org/forums/index.php?showtopic=38114
489 Required settings: Control Server tab:
491 * UTF-8 output/input: checked
492 * Base delimiter: |||
494 * Number of Clients: Some big number like 700
497 NOTE: the script only works with either the default or this custom Fields line.
499 This script can also work via SSH tunneling, by using -R 3333:localhost:3333.
506 sub get_intro_string
{
508 \002-----------------------------------------------------------------
509 \002foo_spam - prints the currently playing track from foobar2000
510 \002Created by Kovensky \(irc.rizon.net #shameimaru\)
511 This script requires a properly configured foobar2000.
512 Run /foo_help for help setting foobar2000 up.
513 \002-----------------------------------------------------------------
515 /aud - prints the playing song as an ACTION
517 /foo_help - explains how to set up foobar2000
518 /foo_format - explains how to set up the output format
519 \002-----------------------------------------------------------------
526 sub get_foo_format_help_string
{
529 Example: artist - [album - ]title
531 * lowercasestring <= Is parsed as a tag name. To see a list of tags, use /foo_tags.
532 * [expression] <= Evaluate expression as a regular format.
533 If there are no missing tags or at least one subexpression is true,
534 then the result of the expression will be included on the output.
535 * 'literal' <= Everything inside the quotes is copied as-is.
536 * \\' <= Inserts a literal '.
538 To change the format, you can use:
539 * Irssi: /set foo_format <new format>\n * X-Chat: /set_foo_format <default or new format>
541 Default: $default_format
548 sub get_taglist_string
{
550 List of available tags:
551 * player <= contains the player name (in this case, foobar2000).
552 * state <= one of \"paused\", \"playing\" or \"paused\".
553 * playlistindex <= the index of the current playlist, 0-based.
554 * trackindex <= the index of the current track on the playlist, 1-based.
555 * pos_sec <= the current position on the track, in seconds.
556 * codec <= the codec used on the currently playing track.
557 * bitrate <= the current bitrate in kbps, varies with VBR.
558 * albumartist <= the artist of the album. May or may not be the same as artist.
559 * album <= the current album title.
560 * date <= the album's / track's release date.
561 * genre <= the song's genre.
562 * track <= the track number.
563 * title <= the track's title.
564 * position <= the current position on the track, in m:ss format.
565 The following tags are only available if using the recommended settings:
566 * artist <= the artist of the current track.
567 * totaltracks <= the number of tracks in the album.
568 * len <= the track's duration, in m:ss format.
569 * version <= the player's version (e.g. \"v0.9.6.8\").
570 The following tag is set by foo_spam itself:
571 * comment <= all arguments that the user gives to /aud in a single string.
579 *print_now_playing
= sub {
580 my ($data, $server, $witem) = @_;
581 my $str = get_np_string
($data);
583 if ($witem && ($witem->{type
} eq "CHANNEL" ||
584 $witem->{type
} eq "QUERY")) {
585 $witem->command(encode_utf8
("me $str"));
590 *print_foo_help
= sub{
591 Irssi
::print(get_help_string
());
594 *print_foo_format_help
= sub {
595 Irssi
::print(get_foo_format_help_string
());
602 *print_foo_tags
= sub {
603 Irssi
::print(get_taglist_string
());
606 Irssi
::settings_add_str
("foo_spam", "foo_format", $format);
607 $format = Irssi
::settings_get_str
("foo_format");
609 Irssi
::command_bind
('aud', 'print_now_playing');
610 Irssi
::command_bind
('np', 'print_now_playing');
611 Irssi
::command_bind
('foo_help', 'print_foo_help');
612 Irssi
::command_bind
('foo_format','print_foo_format_help');
613 Irssi
::command_bind
('foo_tags','print_foo_tags');
615 } elsif (HAVE_XCHAT
) {
616 *print_now_playing
= sub {
617 my $str = get_np_string
($_[0][1] ?
$_[1][1] : undef);
619 Xchat
::command
(encode_utf8
("me $str"));
621 return Xchat
::EAT_ALL
();
624 *print_foo_help
= sub {
625 Xchat
::print(get_help_string
());
626 return Xchat
::EAT_ALL
();
633 *set_foo_format
= sub {
634 if (defined($_[0][1])) {
635 open($setting_file, ">", Xchat
::get_info
('xchatdir') . "/foo_spam.conf");
636 if ($_[0][1] eq "default") {
637 $format = $default_format;
641 Xchat
::print("Changed format to $format\n");
642 if (defined($setting_file)) {
643 print $setting_file $format;
644 close($setting_file);
646 Xchat
::print("Failed to save settings! Error: $!");
649 Xchat
::print("Current format: $format\n");
651 return Xchat
::EAT_ALL
();
653 if (defined(*set_foo_format
)) {} # Silence a warning
655 *print_foo_format_help
= sub {
656 Xchat
::print(get_foo_format_help_string
());
657 return Xchat
::EAT_ALL
();
660 *print_foo_tags
= sub {
661 Xchat
::print(get_taglist_string
());
662 return Xchat
::EAT_ALL
();
665 if (open($setting_file, "<", Xchat
::get_info
('xchatdir') . "/foo_spam.conf")) {
666 my $line = <$setting_file>;
668 $format = $line if (defined($line) and $line ne "");
669 close($setting_file);
672 Xchat
::hook_command
("np","print_now_playing", {help
=> "alias to /aud"});
673 Xchat
::hook_command
("aud","print_now_playing", {help
=> "prints your current playing song on foobar2000 on an ACTION"});
674 Xchat
::hook_command
("foo_help","print_foo_help", {help
=> "explains how to set up foobar2000"});
675 Xchat
::hook_command
("set_foo_format","set_foo_format", {help
=> "displays or changes the current format string"});
676 Xchat
::hook_command
("foo_format","print_foo_format_help", {help
=> "explains how to configure the format string"});
677 Xchat
::hook_command
("foo_tags","print_foo_tags", {help
=> "lists all available tags"});
680 binmode (STDERR
, ":encoding(utf-8)");
681 binmode (STDOUT
, ":encoding(utf-8)");
683 print (STDERR
"@_\n") if @_;
685 $format = join(" ", @ARGV) if $ARGV[0];
686 my $np = get_np_string
();
687 print "$np\n" if $np;
690 if (HAVE_XCHAT
or HAVE_IRSSI
) {
691 irc_print
(get_intro_string
());