Change communication method
[foo_spam.git] / foo_spam.pl
blob901dffeb4e0260faf70ffbee40f31610279ceee9
1 #! /usr/bin/env perl
3 use warnings;
4 use strict;
5 use utf8;
6 use Encode;
8 use Net::Telnet;
9 use File::Path;
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);
15 # ChangeLog:
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.
26 # 0.1 - First version
28 # Known Bugs:
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().
33 # TODO:
34 # Replace the current format syntax by foobar2000's title format
36 our $telnet_open = 0;
37 our $telnet = undef;
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
43 sub open_telnet {
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.");
50 return $telnet_open;
53 sub close_telnet {
54 if($telnet_open) {
55 $telnet_open = 0;
56 $telnet->put("exit\n");
57 $telnet->close;
61 sub get_track_info {
62 return undef unless open_telnet();
64 my $line = undef;
66 unless (defined($telnet->print("trackinfo"))) {
67 close_telnet();
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;
75 close_telnet();
77 unless($line) {
78 irc_print("Error retrieving status from foobar2000!");
79 return undef;
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.");
84 return undef;
87 my @fields;
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);
96 # Standard settings
97 my $info = {player => 'foobar2000',
98 state => $fields[0],
99 playlistindex => $fields[1],
100 trackindex => $fields[2],
101 pos_sec => $fields[3],
102 codec => $fields[4],
103 bitrate => $fields[5],
104 albumartist => $fields[6],
105 album => $fields[7],
106 date => $fields[8],
107 genre => $fields[9],
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];
116 if ($fields[16]) {
117 $info->{'version'} = $fields[16];
118 $info->{'version'} =~ s/^foobar2000 //;
120 # New field in 0.5
121 $info->{'codec_profile'} = $fields[17] if ($fields[17]);
123 if ($fields[19]) {
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");
135 for (keys %$info) {
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'});
146 if (defined $s) {
147 $info->{'len_sec'} = $s + $m * 60 + $h * 3600;
148 } else {
149 $info->{'len_sec'} = $m + $h * 60;
153 if (not defined($info->{'position'})) {
154 my ($s, $m, $h) = ($info->{'pos_sec'}, 0, 0);
156 $h = $s / 3600;
157 $s %= 3600;
159 $m = $s / 60;
160 $s %= 60;
162 if ($h > 0) {
163 $info->{'position'} = sprintf("%d:%02d:%02d", $h, $m, $s);
164 } else {
165 $info->{'position'} = sprintf("%d:%02d", $m, $s);
169 return $info;
172 sub parse_format {
173 my ($format, $info, $sublevel, $parsed) = @_;
174 $sublevel = 0 if not defined $sublevel;
176 my $output = "";
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
188 # Bracket Nesting
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.
192 my @tokens = ();
193 my $tagcount = 0;
194 my $fail = 0;
196 my $literal = 0;
197 my $sub = 0;
198 my $func = 0;
199 my $tagmode = 0;
200 my $str = "";
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 "\\"))) {
210 push @tokens, $str;
211 $str = "";
212 $literal = 0;
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
219 ++$sub;
220 } elsif ($chars[$i] eq "]" and $chars[$i-1] ne "\\" and --$sub == 0) {
221 push @tokens, $str;
222 $str = "";
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_]/) {
229 push @tokens, $str;
230 $str = "";
231 $tagmode = 0;
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 "\\") {
236 $func++;
237 } elsif ($chars[$i] eq ")" and $chars[$i-1] ne "\\" and --$func <= 1) {
238 push @tokens, $str;
239 $str = "";
240 $func = 0;
241 } elsif (not defined($chars[$i+1])) {
242 return "Malformed: mismatched (: $str";
244 } else {
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
247 $str = $chars[$i];
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
251 $str = $chars[$i];
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 "";
255 $str = $chars[$i];
256 $func = 1; # Enter subfunction mode
257 } elsif ($chars[$i] =~ /[a-z]/) {
258 push @tokens, "'$str'" if $str ne ""; # Found a tag name
259 $str = $chars[$i];
260 $tagmode = 1; # Enter tag mode
261 } else {
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
269 $tagcount = 0;
270 $sub = 0;
271 $func = 0;
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 "") {
290 $output .= $res;
291 $func++;
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);
304 return $output;
307 sub build_output {
308 my ($format, $info, $sublevel) = @_;
309 $sublevel = 0 if not defined $sublevel;
311 my $sub;
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
316 return $output;
319 sub parse_subfunction {
320 my ($function, $info, $sublevel) = @_;
322 $function =~ /^\$(.*?)\((.*)\)$/;
324 my $func = $1;
326 my @rawargs = split(//, $2);
327 my @args = ();
329 my $parens = 0;
330 my $str = "";
331 for(my $i = 0; $i < @rawargs; $i++) {
332 if ($i > 0 and $rawargs[$i-1] eq "\\") {
333 # Ignore everything else
334 } elsif($parens) {
335 --$parens if ($rawargs[$i] eq ")");
336 } elsif ($rawargs[$i] eq "(") {
337 ++$parens;
338 } elsif ($rawargs[$i] eq ",") {
339 push @args, $str;
340 $str = "";
341 ++$i;
343 $str .= $rawargs[$i] if defined($rawargs[$i]);
345 push @args, $str;
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);
360 my $bar = "";
362 if ($filled) {
363 for (my $i = 0; $i < $len; $i++) {
364 if ($i < $fillpos) {
365 $bar .= $fill;
366 } else {
367 $bar .= $space;
370 } else {
371 for (my $i = 0; $i < $len; $i++) {
372 if ($i == $fillpos) {
373 $bar .= $fill;
374 } else {
375 $bar .= $space;
380 return "$bar";
381 } elsif ($func eq "if" or $func eq "if2") {
382 my ($test, $iftrue, $iffalse);
383 if ($func eq "if") {
384 ($test, $iftrue, $iffalse) = @args;
385 } else {
386 ($test, $iffalse) = @args;
387 $iftrue = $test;
390 if ($test) {
391 return $iftrue;
392 } else {
393 return $iffalse;
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
400 $arg1 =~ s/\'//g;
401 # Ignore spacing
402 $arg1 =~ s/\s+//g;
404 if (defined($arg2)) {
405 $arg2 =~ s/\'//g;
406 $arg2 =~ s/\s+//g;
407 } else {
408 return $iftrue if (defined($arg1));
409 return $iffalse;
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;
417 } else { # ifgreater
418 return $iftrue if $arg1 > $arg2;
420 return $iffalse;
421 } elsif ($func eq "abbr") {
422 my ($arg1, $arg2) = (0,0);
423 $arg1 = $args[0];
424 $arg2 = $args[1] if ($args[1]);
426 if (length($arg1) > $arg2) {
427 my $abbr = "";
428 my @tokens = split(/\s+/, $arg1);
429 foreach my $token (@tokens) {
430 my @chars = split(//, $token);
431 $abbr .= $chars[0];
433 return $abbr;
435 return $arg1;
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);
465 } else {
466 return undef;
470 sub get_np_string {
471 my $info = get_track_info();
472 $info->{comment} = $_[0] if $_[0];
473 if (defined($info)) {
474 return build_output($format, $info);
476 return undef;
479 sub get_help_string {
480 my $fields;
481 if (HAVE_IRSSI) {
482 $fields = '%%codec%%|||%%bitrate%%|||%%album artist%%|||%%album%%|||%%date%%|||%%genre%%|||%%tracknumber%%|||%%title%%|||%%artist%%|||%%totaltracks%%|||%%playback_time%%|||%%length%%|||%%_foobar2000_version%%|||%%codec_profile%%|||%%discnumber%%|||%%totaldiscs%%';
483 } else {
484 $fields = '%codec%|||%bitrate%|||%album artist%|||%album%|||%date%|||%genre%|||%tracknumber%|||%title%|||%artist%|||%totaltracks%|||%playback_time%|||%length%|||%_foobar2000_version%|||%codec_profile%|||%discnumber%|||%totaldiscs%';
486 my $help = <<EOF
487 Required Plugin: foo_controlserver
488 URL: http://www.hydrogenaudio.org/forums/index.php?showtopic=38114
489 Required settings: Control Server tab:
490 * Server Port: 3333
491 * UTF-8 output/input: checked
492 * Base delimiter: |||
493 Recommended setting:
494 * Number of Clients: Some big number like 700
495 * Fields: $fields
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.
503 return $help;
506 sub get_intro_string {
507 my $intro = <<EOF
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-----------------------------------------------------------------
514 Usage:
515 /aud - prints the playing song as an ACTION
516 /np - alias to /aud
517 /foo_help - explains how to set up foobar2000
518 /foo_format - explains how to set up the output format
519 \002-----------------------------------------------------------------
523 return $intro;
526 sub get_foo_format_help_string {
527 my $help = <<EOF
528 Format Definition
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
545 return $help;
548 sub get_taglist_string {
549 my $list = <<EOF
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.
575 return $list;
578 if (HAVE_IRSSI) {
579 *print_now_playing = sub {
580 my ($data, $server, $witem) = @_;
581 my $str = get_np_string($data);
582 if (defined($str)) {
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());
598 *irc_print = sub {
599 Irssi::print($_[0]);
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);
618 if (defined($str)) {
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();
629 *irc_print = sub {
630 Xchat::print(@_);
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;
638 } else {
639 $format = $_[1][1];
641 Xchat::print("Changed format to $format\n");
642 if (defined($setting_file)) {
643 print $setting_file $format;
644 close($setting_file);
645 } else {
646 Xchat::print("Failed to save settings! Error: $!");
648 } else {
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>;
667 chomp $line;
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"});
678 } else {
679 $| = 1;
680 binmode (STDERR, ":encoding(utf-8)");
681 binmode (STDOUT, ":encoding(utf-8)");
682 *irc_print = sub {
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());