#!/usr/bin/env perl
#
# Version:
my $version="0.27";

my $blerb="
  Squeezy:
      A command-line utility for controlling squeezebox network audio players via their squeezeserver.

  Copyright:
     Stephen Blott (smblott@gmail.com)
     Phillip Camp (phillip<dot>camp@gmail.com)

  License:
     MIT license
     http://opensource.org/licenses/mit-license.php

  Project home:
     - https://github.com/pssc/squeezy

  See also:
     squeezy.README.txt
     squeezy.conf
     squeezy --help

";

use strict;

#
# imports
#
use IO::Socket;
use IO::Select;
use Cwd 'realpath';
use Encode;

#
# find a configuration file
#
my $conf_base = 'squeezy.conf';
my $conf_locs = qq( ./$conf_base
		    $ENV{HOME}/.$conf_base
		    $ENV{HOME}/.config/squeezy/$conf_base
		    $ENV{HOME}/.config/$conf_base
		    /usr/local/etc/$conf_base
		    /etc/$conf_base );

my @conf_file = map { $_ && -r $_ ? $_ : () } split /\s+/, $conf_locs;
my $conf_file = $conf_file[0];

# die "squeezy error: could not find configuration file:\n", map { "\t$_\n" } @conf_file
#    unless $conf_file;

#
# some globals
#
my $json_module     = "JSON::RPC::Client";
my $my_name    =  "/$0";
   $my_name    =~ s:^.*/::;
my $server     = 'localhost';
my $port       =  9090;
my $json       =  9000;
my $username   =  undef;
my $password   =  undef;
my $group      =  undef;
my @group;
my %shortcut;
my $default    =  undef;
my $silent     =  undef;
my $little     = '3';
my $lot        = '10';
my $prefixpre  = '';
my $prefixpos  = '';
my $tick       = undef;
my $player     = undef;
my $cpo         = undef;
my $def_player = undef;
my $autodiscover = 1;
my $playerdiscover = 1;
my %command;
my @player;
my @servers = ();
my @udp_ports = (3483);
my $DISCOVERY_PERIOD = 1;   # discovery period
my $showtime = 7;
my $timeout = $showtime +1;
my $json_api_uri_post = "/jsonrpc.js";
my $json_api_uri_pre = "http://";
my $socket = undef;
my $action_def = sub{ };
my $action = $action_def;
my $not = 0;
my $serverss ='';
my $playerss ='';
my $verbose = 0;
my $skip = {};

#
# utilities
#
sub json_support {
    my $soft = shift;
     
    # check whether module is already loaded
    return 1 if (defined $INC{'JSON/RPC/Client.pm'} or defined $INC{'JSON/RPC/Legacy/Client.pm'});

    eval {
        require JSON::RPC::Client;
        JSON::RPC::Client->import();
    };
    if ($@) {
        eval {
           $json_module = 'JSON::RPC::Legacy::Client';
           require JSON::RPC::Legacy::Client;
           JSON::RPC::Legacy::Client->import();
        };
    }

    if ($@) {
       if ( -r "/etc/debian_version" ) {
          warn "Install JSON support: sudo apt-get install libjson-rpc-perl" if ( -r "/etc/debian_version" );
       } else {
          warn "Install JSON support: cpan App::cpanminus; cpanm JSON::RPC::Legacy::Client";
       }
       die "JSON features not supported: JSON::RPC::(Legacy::)Client not available" if not $soft;
       warn "JSON features not supported: JSON::RPC::(Legacy::)Client not available... Trying to cope." if $soft > 1;
       return 0;
    }
    return 1;
}

sub urlencode {
  my $str = shift;
  $str =~ s/([^A-Za-z0-9])/sprintf('%%%02X', ord($1))/seg;
  return $str;
}

sub urldecode {
  my $str = shift;
  $str =~ s/%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg;
  return $str;
}

sub pick_field
{
   return urldecode((split /\s+/, shift)[shift]);
}

sub notted {
   if ($not) {
      return not shift
   } else {
      return shift
   }
}

sub report
{
   my $mess = shift;
   my $value = shift;
   print join(' ', $cpo->{name}|| $player, $mess, $value), "\n";
   return $value;
}

sub jsonr {
   my ($key , $r) = @_;
    if (ref($r) eq "HASH") {
      print $$r{$key}."\n";
    } else {
      print STERR $r;
    }
}

sub jsonp {
   my ($key , $r) = @_;
    if (ref($r) eq "HASH" and defined $$r{$key}) {
      return $$r{$key};
    } else {
      print STERR $r;
      return undef;
    }
}

sub terminal_cols
{
   -t STDOUT || return undef;
   return (map { chomp; $_ } `tput cols`)[0];
}

#
# server socket commands
#
sub send_command
{
   # this a syncronous command/response interaction, one line is sent to the
   # server and one line is received in response (and returned)
   if ( ! $socket )
   {
      $socket  = IO::Socket::INET -> new ( PeerAddr => $server,
		                           PeerPort => $port,
		                           Timeout  => $timeout,
		                           Proto    => 'tcp' )
	         or
	         (warn "squeezy error: could not connect to '$server' port '$port'\n" and return -1);

      if ( $username )
      {
	 if ( ! $password )
	    { die "squeezy error: username provided, but no password provided (username='$username')\n"; }
	 print $socket "login $username $password\n";
	 # ignore the following response; even with an incorrect
	 # username/password the server happily echos it back
	 my $response = <$socket>;
      }
      # force some unnecessary client/server interaction in order to detect
      # an authentication failure
      print $socket "player count ?\n";
      if ( ! $socket->connected() || ! <$socket> )
	 { die "squeezy error: authentication failed (username='$username', password='$password')\n"; }
   }

   if ( ! $socket->connected() )
      { die "squeezy error: server dropped socket connection\n"; }

   print $socket join(" ", @_), "\n";

   ## FIXME some commands produce no responce... so timoeut needed
   my $response ="";
   my $wait = IO::Select->new($socket);
   if (my ($found) = $wait->can_read($timeout)) {
         $response = <$socket>;
   }

   if ( ! $socket->connected() )
      { die "squeezy error: server dropped socket connection\n"; }

   chomp $response;
   return $response;
}

=head2 silmrequest ( $server, $player, @command_and_ags )

query using json slim request interface.

=cut

sub slimrequest {
   my ($server, $player, $command_and_ags) = @_;
   json_support();

   my $client = $server->{JSONC} ? $server->{JSONC} : new $json_module;
   my $uri    = $json_api_uri_pre.($server->{IP}||$server->{HOST}).":".($server->{JSON}||$json).$json_api_uri_post;

   my $callobj = {
      method  => 'slim.request',
   };

   $client->version($version);
   $client->id("squeezy");

   $callobj->{params} = [ ($player->{name} || "") ,$command_and_ags ];

   my $res = $client->call($uri, $callobj);
   if($res) {
      if ($res->is_error) {
          return "Error : ". $res->error_message;
      }
      else {
          return $res->result;
      }
   }
   else {
      return $client->status_line."\n";
   }
}

#
# read configuration file
#

if ( -r $conf_file )
{
   open CONFIG, "<$conf_file"
      or die "squeezy: open of configuration file failed ($conf_file)\n";

   foreach ( <CONFIG> )
   {
      s/#.*//;       # strip comments
      s/^\s*//;      # strip leading whitespace
      s/\s*$//;      # strip trailing whitespace
      $_ || do next; # skip empty lines

      my @arg = split /\s+/;

      $arg[0] eq 'server'   && $#arg == 1 && do { $server   = $arg[1]; push @servers, { HOST => $arg[1] }; next; };
      $arg[0] eq 'server'   && $#arg == 2 && do { $server   = $arg[1]; push @servers, { HOST => $arg[1], PORT => $arg[2]}; next; };
      $arg[0] eq 'server'   && $#arg == 3 && do { $server   = $arg[1]; push @servers, { HOST => $arg[1], PORT => $arg[2], JSON => $arg[3]}; next; };
      $arg[0] eq 'port'     && $#arg == 1 && do { $port     = $arg[1]; next; };
      $arg[0] eq 'json'     && $#arg == 1 && do { $json     = $arg[1]; next; };
      $arg[0] eq 'username' && $#arg == 1 && do { $username = $arg[1]; next; };
      $arg[0] eq 'password' && $#arg == 1 && do { $password = $arg[1]; next; };

      $arg[0] eq 'player'
	 && do
	 {  if ( $#arg == 1 ) { push @{$group[0]},       { name=>$arg[1]}; next; }
	    if ( $#arg == 2 ) { push @{$group[$arg[1]]}, { name=>$arg[2]}; next; } 
	    if ( $#arg == 3 ) { push @{$group[$arg[1]]}, { name=>$arg[2], server=>{HOST=>$arg[3]}}; next; } };

      $arg[0] eq 'shortcut' && $#arg == 2
	 && do
	 {  $default = $arg[2] unless $default;
	    $shortcut{$arg[1]} = $arg[2];
	    next; };

      $arg[0] eq 'small_volume' && $#arg == 1 && $arg[1] =~ m/^[0-9]+$/ && do { $little = $arg[1]; next; };
      $arg[0] eq 'large_volume' && $#arg == 1 && $arg[1] =~ m/^[0-9]+$/ && do { $lot    = $arg[1]; next; };

      $arg[0] eq 'prefixpre'
	 && do
	 {  if ( $#arg == 0 ) { next; }
	    if ( $#arg == 1 ) { $prefixpre = $arg[1]; next; } };

      $arg[0] eq 'prefixpos'
	 && do
	 {  if ( $#arg == 0 ) { next; }
	    if ( $#arg == 1 ) { $prefixpos = $arg[1]; next; } };

      $arg[0] eq 'autodiscover'
         && do
         { if ( $#arg == 0 ) { next; }
           if ( $#arg == 1 ) { $autodiscover = (lc($arg[1]) eq 'yes') ? 1 : 0; next; } };

      $arg[0] eq 'playerdiscover'
         && do
         { if ( $#arg == 0 ) { next; }
           if ( $#arg == 1 ) { $playerdiscover = (lc($arg[1]) eq 'yes') ? 1 : 0; next; } };

      $arg[0] eq 'discoverperiod'
         && do
         { if ( $#arg == 0 ) { next; }
           if ( $#arg == 1 ) { $DISCOVERY_PERIOD = $arg[1]; next; } };

      die "error in $conf_file: $_\n"
   }

   close CONFIG;
} 



if ( $autodiscover ) {
   foreach my $port (@udp_ports) {
      socket(my $socket, AF_INET, SOCK_DGRAM, getprotobyname('udp'));
      setsockopt($socket, SOL_SOCKET, SO_BROADCAST, 1);
      my $destpaddr = sockaddr_in($port, INADDR_BROADCAST);
      send($socket, "eIPAD\0NAME\0JSON\0VERS\0UUID\0", 0, $destpaddr);
      my $wait = IO::Select->new($socket);

      while (my ($found) = $wait->can_read($DISCOVERY_PERIOD)) {
         my $clientpaddr = recv($socket, my $msg, 1600, 0);
         # chop of leading character
         $msg = substr($msg, 1);

         my $len = length($msg);
         my ($tag, $len2, $val);

         my $bag = {};

         while ($len > 0) {
                $tag  = substr($msg, 0, 4);
                $len2 = unpack("xxxxC", $msg);
                $val  = $len2 ? substr($msg, 5, $len2) : undef;

                # print(" TLV: $tag len: $len2, $val");

                $bag->{$tag} = $val;

                $msg = substr($msg, $len2 + 5);
                $len = $len - $len2 - 5;
         }

         # get server's IP address
         if ($clientpaddr) {
            my ($portno, $ipaddr) = sockaddr_in($clientpaddr);
            $bag->{IP} = inet_ntoa($ipaddr);
            $bag->{HOST} = gethostbyaddr($ipaddr, AF_INET);
            if (json_support(2)) {
                my $cliport = jsonp('_p2',slimrequest($bag,undef,["pref" ,"plugin.cli:cliport","?" ]));
   	        $bag->{PORT} = $cliport if $cliport; 
	    }
	    push @servers, $bag;
         }
      }
      close $socket;
   }
} 

if (scalar(@servers) == 0 ) {
     push @servers, { HOST=>$server, PORT=>$port} 
} elsif (scalar(@servers) > 1 ) {
      $serverss = 'servers: ', join(', ', map { $_->{NAME}.":".($_->{HOST} || "uknown").":".($_->{IP} || "unkown")  } @servers), "\n";
}

if ($playerdiscover) {
    # ask squeezeservers on 9090 
    # for the names of players
   foreach my $host (@servers) {
       $server = $host->{IP} || $host->{HOST};
       my $count = pick_field 2, send_command 'player count ?';
       if ( $count )
       {
           for (my $i=0; $i<$count; $i+=1)
           {
	       my $pn = pick_field 3, send_command "player name $i ?";
               my $pid = pick_field 3, send_command "player id $i ?"; 
	       if ( $pn && $pn ne '?' ) { 
                      push @{$group[0]}, { name=>$pn, pid=>$pid , server=>$host } 
               } else {  
	              if ( $pid && $pid ne '?' )
	                  { push @{$group[0]}, { name=>$pid, server=>$host} } 
                      warn "Player discovery playname issue $i:$pn:$pid"
               }
           }
           if ( $group[0][0] )
           {
	       my $name = $host->{NAME} || $host->{HOST} || $host->{IP};
               if (scalar(@servers) eq 1 ) {
		  $name = '';
	       } else {
                  $name = "(".$name.")";
	       }
	       $playerss.= 'players auto discovered: '. join(', ', map { $_->{name}.$name } @{$group[0]}). "\n";
           }
       }
       $host->{SOCKET} = $socket;
       $socket = undef;
   }
}



if ( $username && ! $password )
   { die "squeezy configuration error: username is set ($username) but password is unset\n"; }


# find a default player
while ( ! $def_player )
{
   @player = map { @$_ } @group;
   $def_player = $player[0];
   if ( !$def_player )
   {
      print STDERR "squeezy warning: no players specified or detected\n";
      print STDERR "                 you likely won't be able to do much without them\n";
      #$group[0][0] = 'NO_PLAYERS_THIS_WILL_NOT_WORK';
      last;
   }
}

# push a group containing all players
push @group, [ @player ];

#
# strip any trailing '/'s from $prefixpre and $prefixpos
#

if ( $prefixpre ) { $prefixpre =~ s:/+$::; }
if ( $prefixpos ) { $prefixpos =~ s:/+$::; }

#
# listen output wrapper functions
#

sub listen_output
{
   # this is just an output hook, it's somewhere to put post-processing
   # triggers for the -listen output
   #
   # if we see a "new song", then request full status for that player
   #
   if ( $_[1] eq 'playlist' && $_[2] eq 'newsong' )
   {
      print $socket "$_[0] status - 1\n";
   }

   print STDOUT join(' ', @_), "\n";
}

sub listen
{
   my $count = pick_field 2, send_command 'player count ?';
   listen_output 'player', 'count', $count;

   my @ids;
   for (my $i=0; $i<$count; $i+=1)
   {
      my $id = pick_field 3, send_command "player id $i ?";
      push @ids, $id;
      listen_output 'player', 'id', $i, $id;
   }

   # the &send_command() calls above force $socket to be open by here
   my $select = IO::Select->new($socket);
   STDOUT->autoflush(1);

   sub socket_input
   {
      my $wait = shift;

      while ( $select->can_read($wait) )
      {
	 my $line = <$socket>;
	 chomp $line;
	 my @line = split(/\s+/, $line, 3);

	 if ( $line[1] eq 'status' )
	 {
	    # 'status' lines contain many fields, so parse them here to make
	    # them easier to deal with down stream
	    my @prefix = (urldecode($line[0]), 'status');
	    listen_output @prefix, 'start', 'status';
	    foreach ( map { urldecode($_) } split(/\s+/, $line) )
	       { listen_output @prefix, split(/:/, $_, 2); }
	    listen_output @prefix, 'end', 'status';
	    next;
	 }

	 listen_output map { urldecode($_) } split(/\s+/, $line);
      }
   };

   # from here on we print() directly to the socket and sprinkle in
   # socket_input() calls to catch the ouput; syncronous send_command() calls
   # will no loger work

   for (my $i=0; $i<$count; $i+=1)
   {
      print $socket "player name $ids[$i] ?\n";
      socket_input(0); # 0 here means do not block
   }

   for (my $i=0; $i<$count; $i+=1)
   {
      print $socket "$ids[$i] status - 1\n";
      socket_input(0); # 0 here means do not block
   }

   print $socket "listen 1\n";
   while ( $socket->connected() )
   {
      socket_input(1); # 1 here means block for 1 second
      if ( $tick )
      {
	 for (my $i=0; $i<$count; $i+=1)
	 {
	    print $socket "$ids[$i] time ?\n";
	    socket_input(0); # 0 here means do not block
	 }
      }
   }

   print STDERR "squeezy error: lost socket connection\n";
   $socket = undef;
}

sub idFromName {
   my $name = shift;

   foreach my $p (@{$group[ $#group ] }) {
      if ( ($p->{name} eq $name && $p->{pid} ) || $name eq $p->{pid} ) {
           return $p->{pid};
      }
   }
   return undef;
}

# ######################################################################
# commands
#

sub command
{
   my $command = shift;
   if ( $command{$command}->{deprecated} ) {
        warn "Commmand ",$command," deprecated.\n";
   }
   if ( $command{$command}->{json} ) {
       $command{$command}->{command}(shift,shift,@_);
   } else {
       $command{$command}->{command}(@_);
   }
}

## Warning CLI API supports Using the name insead of the ID but only!!
# if its the first arg IE 0
%command =
(

   '-add' =>
      {
	 help     => 'add the indicated file, directory (contents), playlist or url to the playlist',
	 do_shift => 1,
	 command  => sub { command -play, shift, 'add'; }
      },

   '-alarms' =>
	{
	   help => 'enabled alarms count (can only sound if not generally disabled)',
	   command => sub { report 'enabled alarms', pick_field 6, send_command urlencode($player)." alarms 0 99 filter:enabled"; }
	},

   '-alarms_enabled' =>
        {
	   help => 'alarms generally enabled?',
	   command => sub { report 'alarms enabled', pick_field 3, send_command urlencode($player)." playerpref alarmsEnabled ?"; }
	},

   '-all' =>
      {
	 help     => 'apply subsequent commands to all players (eg. squeezy -all -off)',
	 command  => sub { $group = $#group; }
      },

   '-button' =>
      {
	 help     => 'send a button key command to the player (see the server\'s "Default.map" file)',
	 do_shift => 1,
	 command  => sub { send_command urlencode($player), 'button', $_[0]; }
      },

   '-cliport' =>
      {
        help  => "query cli port VIA JSON",
	json => 1,
	server => 1,
        command  => sub { jsonr("_p2",slimrequest(shift,undef,["pref" ,"plugin.cli:cliport","?" ])); },
      },

   '-connected' =>
	{
	   help => 'is player connected?',
           command => sub { report 'connected', pick_field( 2, send_command urlencode($player)." connected ?"); }
	},

   '-default' =>
      {
	 help    => "play the default shortcut (the first one in the configuration file, currently $default)",
	 command =>
	    sub
	    {
	       if ( ! $default )
		  { die 'squeezy -default: no shortcuts in configuration file, so no default available\n'; }
	       command -play, $default;
	    }
      },

    '-if_alarm' =>
	{
	   help => 'if alarms are enabled generally and an alarm is enabled',
           command =>
	   sub { if (notted( my $r = (command('-alarms_enabled') eq "1" && command('-alarms') ne 'count:0')))
	   { print $cpo->{name}," has ",$r ? "" : "no ","active alarm \n"; &$action; } }
	},

   '-die_if_playing' =>
      {
	 help    => 'if this player is on and playing, then die immediately; exit code 1',
         deprecated => 1,
	 command =>
	    sub { if ( command('-power') && command('-mode') eq 'play' )
		     { print $cpo->{name}," is on and playing, exiting immediately\n"; exit(1); } }
      },

   '-elapsed' =>
      {
	 help    => 'show the elapsed time in currently-playing track/url',
	 command => sub { report 'time', pick_field 2, send_command urlencode($player)." time ?"; }
      },

    '-exit' =>
        {
          help    => 'set up action for -if_XXXX to exit with specified code',
          do_shift => 1,
          command => sub { my $ecode = shift ;$action = sub { print "Exiting $ecode\n" if $verbose; exit($ecode); }},
        },

    '-skip'=>
        {
          help    => 'set up action for -if_XXXX to skip to next player',
          command => sub { $action = sub { print "Skipping ",$cpo->{name},"\n" if $verbose; $skip->{$player}=1; $action = $action_def; &$action; }},
        },

    '-not' =>
	{
	  help   => '-if_XXXX can be prefixed with not',
          command => sub { $not = 1 },
	},

    '-if_connected' =>
	{
	   help => 'if this player is not connected',
	   command => sub { if (notted( my $r = command('-connected') eq 1) )
	   { print $cpo->{name}," is ",$r ? "" : "not " ,"connected\n"; &$action; } }
	},

   '-if_playing' =>
        {
	   help => 'if this player is on and playing',
	   command => sub { if (notted( my $r = (command('-power') eq "1" && command('-mode') eq 'play')) )
	   { print $cpo->{name}," is ",$r ? "on and" : "not" ," playing,\n"; &$action; } }
        },

   '-if_sleeping' =>
	{
	 help    => 'if this player is on and is timing down to sleep',
	 command =>
	    sub { if ( notted( my $r = (command('-power') eq "1" && command('-sleeping') ) ) )
		{ print $cpo->{name}," is ",$r ? "on and" : "not" ," timing down to sleep\n"; &$action; } }
	},

   '-exit_if_playing' =>
      {
	 help    => 'if this player is on and playing, then exit immediately; exit code 0',
         deprecated => 1,
	 command =>
	    sub { if ( command('-power') && command('-mode') eq 'play')
		     { print $cpo->{name}," is on and playing, exiting immediately\n"; exit(0); } }
      },

   '-exit_if_sleeping' =>
      {
	 help    => 'if this player is on and is timing down to sleep, then exit immediately; exit code 0',
         deprecated => 1,
	 command =>
	    sub {
	       if ( command('-power') && command('-sleeping') )
		     { print $cpo->{name}," is on and timing down to sleep, exiting immediately\n"; exit(0); } }
      },

   '-group' =>
      {
	 help     => 'apply subsequent command to all players in this group (eg. squeezy -group 1)',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       if ( ! ($_[0] =~ m/^[0-9]+$/ ) )
		  { die "squeezy -group: not a number ($_[0])\n"; }
	       if ( $#group < $_[0] )
		  { die "squeezy -group: invalid group number ($_[0], max is $#group)\n"; }
	       $group = shift;
	    }
      },

   '-groups' =>
      {
	 help     => 'show the configured player groups',
	 command  =>
	    sub
	    {
	       for (my $i=0; $i<=$#group; $i+=1)
	       {
		  print "group $i:\n";
		  foreach my $g ( @{$group[$i]} )
		     { print "   ".$g->{name}."\n"; }
	       }
	    }
      },

   '-help' =>
      {
	 help    => 'show this help message',
	 server => 1,
	 command =>
	    sub
	    {
           print "squeezy $version\n",$blerb;
	       print "configuration file:\n   $conf_file\n";
	       print "\nconfiguration file search locations:\n";
	       foreach ( map { $_ ? $_ : () } split /\s+/, $conf_locs )
		  { print "   $_\n"; }
	       print "\ncommand-line options:\n";
	       foreach my $command ( sort keys %command )
	       {
		  if ( $command{$command}->{help} and not $command{$command}->{deprecated})
		  {
		     my $argument = $command{$command}->{do_shift} ? ' <arg>' : '';
		     print sprintf "%-18s: %s\n", "$command$argument", $command{$command}->{help};
		  }
	       }
	    }
      },

   '-json' =>
      {
        help  => "send command via JSON interface quoted as one argument",
	json => 1,
	server => 1,
	do_shift => 1,
        command  => sub {
                            my $result = slimrequest(shift,shift,[split(/\s+/,shift,-1)]); 
                            print Dumper($result);
                            #if (ref $result eq 'HASH') {
                            #   foreach (keys $result) {
                            #      print $_,": ",$$result{$_},"\n";;
                            #   }
			    #} else { print "undef\n";}
        }
      },

   '-jump' =>
      {
	 help     => 'jump to a track on the current playlist (relative or absolute, eg. 3, +2, -4)',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $target = shift;
	       if ( ! ( $target =~ m/^[+-]?[0-9]{1,}$/ ) )
		  { die "squeezy -jump: invalid jump specifier ($target)\n"; }
	       my $count = command -playlist_length;
	       my $index = command -playlist_index;
	       report 'new track', pick_field 3, send_command urlencode($player)." playlist index $target";
	       command -playing;
	    }
      },

   '-listen' =>
      {
	 help     => 'listen to squeezeserver activity (on standard output, see also \'-tick\')',
	 command  => sub { &listen(); }
      },

   '-louder' =>
      {
	 help    => "increase the volume slightly (by $little%)",
	 command => sub { command -volume, "+$little"; }
      },

   '-Louder' =>
      {
	 help    => "increase the volume significantly (by $lot%)",
	 command => sub { command -volume, "+$lot" }
      },

   '-mode' =>
      {
	 help    => 'show the player\'s mode ("play", "stop" or "pause")',
	 command =>
	    sub
	    {
	       command -power; # FIXME needed?
	       report 'mode', pick_field 2, send_command urlencode($player)." mode ?";
	    }
      },

   '-mute' =>
      {
	 # this is not real "muting";  it should be possible subsequently to
	 # unmute, returning to the original volume;  this doesn't work on
	 # my players (perhps because they're old), so I took the brute
	 # force approach here --> just set the volume to 0
	 help     => 'set volume to 0',
	 command  => sub { command -volume, '0'; }
      },

   '-next' =>
      {
	 help    => 'jump to the next track on the playlist',
	 command =>
	    sub
	    {
	       send_command urlencode($player)." playlist index +1";
	       command -playing;
	    }
      },

   '-on' =>
      {
	 help    => 'turn player on and start playing the current playlist/track',
	 command =>
	    sub
	    {
	       send_command urlencode($player)." power 1";
	       send_command urlencode($player)." pause 0";
	       command -power;
	       command -mode;
	       command -playing;
	    }
      },

   '-off' =>
      {
	 help    => 'turn player off',
	 command =>
	    sub
	    {
	       send_command urlencode($player)." power 0";
	       command -power;
	    }
      },

   '-options' =>
      {
	 help    => 'list all options to standard output (useful for configuring shell completion)',
	 server   => 1,
	 command => sub {
	       foreach my $command ( sort keys %command )
	       {
		  if ( not $command{$command}->{deprecated})
		  {
		     print $command," ";
		  }
	       }
               print "\n";
	    }
      },

   '-pause' =>
      {
	 help    => 'toggle pause',
	 command =>
	    sub
	    {
	       send_command urlencode($player)." pause";
	       command -mode;
	    }
      },

   '-play' =>
      {
	 help     => 'play the indicated file, directory (contents), playlist or url',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $play = shift;
	       my $oper = shift || 'play'; # can also be 'add'
	       if ( -r $play )
	       {
		  $play = realpath($play);
		  $play =~ s:^$prefixpre::; # $play should still have a leading '/' here
		  $play = "$prefixpos$play";
	       }
	       $play = urlencode $play;
	       send_command urlencode($player)." playlist $oper $play";
	       command -playing;
	    }
      },

   '-player_count' =>
      {
	 help    => 'show the number of connected players',
	 command  => sub { report 'player count', pick_field 2, send_command 'player count ?'; }
      },

   '-player_name' =>
      {
	 help     => 'show the name of the player',
         do_shift => 1,
	 command  => sub { 
                           print 'player name ',  pick_field 3,send_command "player name ".urlencode(shift)." ?";
			   print "\n"
                     }
      },

   '-player_id' =>
      {
         ## not the real player id command as this would need a index or ID we get the name back on failure ie no id.
	 help     => 'show the id of the player',
	 command  => sub { 
                         my $pid =  $cpo->{pid} || pick_field 0,send_command urlencode($player);
                         if ($pid eq $cpo->{name}) {
                            report 'player id',  pick_field 0, "None($cpo->{pid})";
                         } else {
                            report 'player id',  pick_field 0, $pid;
                         }
                     }
      },

   '-player_ip' =>
      {
         ## Needs to be id or index not name if not vaild id we just get the fist players IP back... so check vaild id?
	 help     => 'show the ip address of the player',
	 command  => sub { 
                             my $pid = $cpo->{pid} || pick_field 0,send_command urlencode($player);
                             if ($pid eq $cpo->{name} ) {
                                report 'player IP', pick_field 0, "None";
                             } else {
                                report 'player IP',  pick_field 3, send_command "player ip ".urlencode($pid)." ?"; 
                             }
                     }
      },

   '-playlist' =>
      {
	 help     => 'show the current playlist',
	 command  =>
	    sub
	    {
	       my $count = command -playlist_length;
	       my $index = command -playlist_index;
	       my $cols  = terminal_cols();
	       for (my $i=0; $i<$count; $i+= 1)
	       {
		  my $album  = pick_field 4, send_command urlencode($player)." playlist album $i ?";
		  my $artist = pick_field 4, send_command urlencode($player)." playlist artist $i ?";
		  my $title  = pick_field 4, send_command urlencode($player)." playlist title $i ?";
		  if ( $cols )
		  {
		     my $left  = sprintf "%s %-3d %s", $i == $index ? '>' : ' ', $i, $title;
		     my $right = sprintf "%${cols}s", "$album - $artist";
		     my $line  = sprintf "%s %s", $left, substr($right, length($left)+1);
		     print substr($line,0,$cols), "\n";
		  }
		  else
		  {
		     print $i == $index ? '> ' : '  ';
		     print sprintf "%-3d ", $i;
		     print "$title; $album - $artist\n";
		  }
	       }
	    }
      },

   '-playlist_delete' =>
      {
	 help     => 'delete (or remove) an item from the current playlist (eg. \'-playlist_delete 3\')',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $target = shift;
	       if ( ! ( $target =~ m/^[0-9]{1,}$/ ) )
		  { die "squeezy -playlist_delete: invalid playlist specifier ($target)\n"; }
	       my $count = command -playlist_length;
	       if ( $count <= $target )
		  { die "squeezy -playlist_delete: invalid playlist specifier, too big ($target)\n"; }
	       report 'playlist delete', pick_field 3, send_command  urlencode($player)." playlist delete $target";
	       command -playing;
	    }
      },

   '-playlist_index' =>
      {
	 help     => 'show the index of the current song in the playlist',
	 command  => sub { report 'playlist index', pick_field 3, send_command  urlencode($player)." playlist index ?"; }
      },

   '-playlist_length' =>
      {
	 help     => 'show the length of the current playlist',
	 command  => sub { report 'playlist length', pick_field 3, send_command  urlencode($player)." playlist tracks ?"; }
      },

   '-playlist_repeat' =>
      {
	 help     => 'playlist repeat <0|1|2|?|none|song|all|query|toggle>',
         do_shift => 1 ,
	 command  => sub { my $arg = shift;
                         if ( not($arg eq '?' or $arg eq 0 or $arg eq 1 or $arg eq 2 ) ) {
                            if ( $arg eq 'none' ) {
				$arg = 0
                            } elsif ( $arg eq 'query' ) {
                               $arg = '?'; 
                            } elsif ( $arg eq 'song' ) {
                            	$arg = 1
                            } elsif ( $arg eq 'all' ) {
                              	$arg = 2
                            } else {
                            	$arg = '';
                            }
                         }
                         report 'playlist repeat', pick_field 3, send_command  urlencode($player)." playlist repeat $arg";
                         if ($arg eq '') {
                            command -playlist_repeat, "?";
                         }
                     }
      },

   '-playlist_shuffle' =>
      {
	 help     => 'playlist shuffle <0|1|2|?|none|song|album|query|toggle>',
         do_shift => 1,
	 command  => sub { my $arg = shift;
                         if ( not($arg eq '?' or $arg eq 0 or $arg eq 1 or $arg eq 2 ) ) {
                            if ( $arg eq 'none' ) {
                               $arg = 0
                            } elsif ( $arg eq 'query' ) {
                               $arg = '?'; 
                            } elsif ( $arg eq 'song' ) {
                               $arg = 1
                            } elsif ( $arg eq 'album' ) {
                               $arg = 2
                            } else {
                              $arg = '';
                            }
                         }
                         report 'playlist shuffle', pick_field 3, send_command  urlencode($player)." playlist shuffle $arg";
                         if ($arg eq '') {
                            command -playlist_shuffle, "?";
                         }
                     }
      },

   '-player_model' =>
      {
         ## Needs to be id or index not name 
	 help     => 'show the model of the player',
	 command  => sub { report 'player model',  pick_field 3, send_command "player model ".urlencode($cpo->{pid})." ?"; }
      },

   '-players' =>
      {
	 help    => 'show all configured players',
	 command => sub { print map { $_->{name}."\n" } @player; }
      },

   '-playing' =>
      {
	 help    => 'show the currently-playing track',
	 command => sub { 
                     report 'genre', pick_field 2, send_command  urlencode($player)." genre ?"; 
                     report 'artist', pick_field 2, send_command  urlencode($player)." artist ?"; 
                     report 'album', pick_field 2, send_command  urlencode($player)." album ?"; 
                     command -title;
                     report 'duration', pick_field 2, send_command  urlencode($player)." duration ?"; 
                     command '-time', '?';
                     report 'remote', pick_field 2, send_command  urlencode($player)." remote ?"; 
                     command -current_title;
                     report 'path', pick_field 2, send_command  urlencode($player)." path ?"; 
         }
      },

   '-title' =>
      {
	 help    => 'show the track title ',
	 command => sub { report 'title', pick_field 2, send_command  urlencode($player)." title ?"; }
      },

   '-current_title' =>
      {
	 help    => 'show the current title',
	 command => sub { report 'current_title', pick_field 2, send_command  urlencode($player)." current_title ?"; }
      },

   '-power' =>
      {
	 help    => 'show whether power is on or off',
	 command => sub { report 'power', pick_field 2, send_command  urlencode($player)." power ?"; }
      },

   '-previous' =>
      {
	 help    => 'jump to the previous track on the playlist',
	 command =>
	    sub
	    {
	       send_command urlencode($player)." playlist index -1";
	       command -playing;
	    }
      },

   '-print_links' =>
      {
	 help    => 'show a list of commands suitable for creating player pseudonyms for the squeezy command',
	 command =>
	    sub
	    {
	       my $squeezy = $0; # not $my_name
	       my $dir     = $squeezy;
		  $dir     =~ s:/[^/]+$::;
	       foreach ( map { lc $_->{name} } @player )
		  { print "ln -vf \"$squeezy\" \"$dir/$_\"\n"; }
	    }
      },

   '-quieter' =>
      {
	 help    => "decrease the volume slightly (by $little%)",
	 command => sub { command -volume, "-$little"; }
      },

   '-Quieter' =>
      {
	 help    => "reduce the volume significantly (by $lot%)",
	 command => sub { command -volume, "-$lot"; }
      },

   '-rescan' =>
      {
         help     => "rescan changes|playlists|clean",
	 server   => 1,
	 do_shift => 1,
         command  => sub { my $st = urlencode shift;
                 if    ($st eq "changes"   ) { $st ="rescan"; }
                 elsif ($st eq "playlists" ) { $st ="rescan playlists"; }
                 elsif ($st eq "clean"     ) { $st ="wipecache"; }
                 elsif ($st eq "wipe"      ) { $st ="wipecache"; }
                 elsif ($st eq "all"       ) { $st ="wipecache"; }
                 else                      { $st= "rescan ".urlencode($st); }
                 send_command $st;
         }
      },

   '-shortcuts' =>
      {
	 help     => 'show the configured shortcuts',
	 command  => sub { foreach ( keys %shortcut )
		              { print sprintf "%-8s %s\n", $_, $shortcut{$_}; }
	    }
      },

   '-show' =>
      {
	 help    => "show a message on the player's display (for $showtime seconds)",
	 do_shift => 1,
	 command  => sub { my $show = urlencode shift;
	                   send_command urlencode($player)." show font:huge duration:".$showtime." centered:1 line2:$show"; }
      },

   '-silent' =>
      {
	 help     => 'redirect standard output to \'/dev/null\'',
	 command  => sub { open STDOUT, '>', '/dev/null'; }
      },

   '-sleep' =>
      {
	 help     => 'make the player sleep in <arg> minutes (use \'0\' or \'-sleep_clear\' to cancel sleep)',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $time = shift;
	       if ( ! ( $time =~ m/^[0-9]{1,}$/ ) )
		  { die "squeezy -sleep: invalid argument ($time)\n"; }
	       $time = 60 * $time;
	       send_command  urlencode($player)." sleep $time";
	       command -sleeping;
	    }
      },

   '-sleep_clear' =>
      {
	 help     => 'cancel sleep',
	 command  =>
	    sub
	    {
	       send_command  urlencode($player)." sleep 0";
	       command -sleeping;
	    }
      },

   '-sleeping' =>
      {
	 help     => 'report on a player\'s sleep status (indicated in seconds)',
	 command  =>
	    sub
	    {
	       my $time = pick_field 2, send_command  urlencode($player)." sleep ?";
	       my @time = split('\.', $time);
	       report 'sleep', $time[0];
	    }
      },

   '-start' =>
      {
 	help     => 'start playing from the current playlist',
        command  => sub {
	       send_command  urlencode($player)." play";
               command -playing;
        }
      },

   '-sync' =>
      {
	 help     => 'synchronise player with <arg> (another player id )',
	 do_shift => 1,
	 command  =>
	    sub
	    {
               # arg needs to be a real player id not name.
               my $arg = idFromName(shift);
	       send_command  urlencode($player)." sync ".urlencode($arg);
	       report 'sync', pick_field 2, send_command  urlencode($player)." sync ?";
	    }
      },

   '-syncgroups' =>
      {
	 help     => 'show synchronisation groups',
	 server   => 1,
	 command  => sub { 
		     my @r = split /\s+/, send_command 'syncgroups ?';
                     if (shift @r eq 'syncgroups') {
                        while ($#r > 0) {
                              print urldecode(shift @r)," ",urldecode(shift @r),"\n";
                        }
                     } 
                  }
      },

   '-tick' =>
      {
	 help    => 'make -listen request all players\' playback time every second',
	 command => sub { $tick = 1; }
      },

   '-time' =>
      {
	 help     => 'jump to an absolute or relative position in the current track (measured in seconds; eg. 10, +30, -30)',
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $time = shift;
	       if ( ! ( $time =~ m/^[+-]?[?0-9]{1,}$/ ) )
		  { die "squeezy -time: invalid time specifier ($time)\n"; }
	       report 'time', pick_field 2, send_command urlencode($player)." time $time";
	    }
      },

   '-unsync' =>
      {
	 help    => 'unsynchronise player',
	 command =>
	    sub
	    {
	       send_command urlencode($player)." sync -";
	       command -syncgroups;
	       report 'sync', pick_field 2, send_command urlencode($player)." sync ?";
	    }
      },

   '-volume' =>
      {
	 help    => "set the player's volume (absolute or relative, use \'?\' to query the current volume)",
	 do_shift => 1,
	 command  =>
	    sub
	    {
	       my $vol = shift;
	       if ( $vol ne '?' )
	       {
		  if ( ! ( $vol =~ m/^[+-]?[0-9]{1,}$/ ) )
		     { die "squeezy -volume: invalid volume specifier ($vol)\n"; }
		  if ( 100 < $vol || $vol < -100)
		     { die "squeezy -volume: invalid volume specifier ($vol, should be min/max 100)\n"; }
		  send_command urlencode($player)." mixer volume $vol";
	       }
	       report 'volume', pick_field 3, send_command urlencode($player)." mixer volume ?";
	    }
      },

   '-verbose' =>
      {
	 help     => 'show serves and players amongst other info',
	 server   => 1,
	 command  => sub {
                        $verbose = 1;
			print $serverss if ($serverss);
			print $playerss if ($playerss);
                  }
      },
);

#
# add options to select each player; for example, for a player 'Kitchen' this
# will add options:

#   -Kitchen
#   -kitchen
#   -k
#
# (options are not added if they would overwrite existing options; so, if you
#  have a player named 'Help', then an option '-help' will not be added)
#
foreach ( @player )
{
   my $po  = $_;              
   my $p  = $_->{name};               # player name
   my $s  = $_->{server};
   my $ns = $p;
   $ns =~ s/ /_/g; 	      # remove spaces
   my $lc = lc $ns;           # same, but lower case
   # Deal with c hars rather than bytes as all lms output is utf8 encoded...
   my $fc = substr( Encode::decode_utf8($lc),0,1);  # just the first character
   $fc = Encode::encode_utf8($fc);
   foreach ( $ns, $lc, $fc )
   {
      if ( ! $command{$_} )
      {
	 $command{"-$_"} = { is_player => 1,
			     help      => "select player $p (automatically added option)",
			     command   => sub { $group  = undef; $def_player = $po; $player = $p; $cpo = $po; $server = $s->{IP} || $s->{HOST} || $server; $socket = $s->{SOCKET} || undef; $port = $s->{PORT} || $port; print "$p selected\n"; }, };
      }
   }
}

#
# add a command to directly play each shortcut; again, only if it doesn't
# overwrite an existing command
#

foreach ( keys %shortcut )
{
   if ( ! $command{-$_} )
   {
      my $sc = $_;
      $command{-$sc} =
	 {
	    help    => "play shortcut $sc ($shortcut{$sc}, automatically added option)",
	    command => sub { command -play, $shortcut{$sc}; }
	 };
   }
}

#
# some abbreviated/alternative option names
#

$command{'-'}              = $command{-quieter}          if ! $command{'-'};
$command{'--'}             = $command{-Quieter}          if ! $command{'--'};
$command{'+'}              = $command{-louder}           if ! $command{'+'};
$command{'++'}             = $command{-Louder}           if ! $command{'++'};
$command{-currenttitle}    = $command{-current_title}    if ! $command{-currenttitle};
$command{-playlist_remove} = $command{-playlist_delete}  if ! $command{-playlist_remove};
$command{-prev}            = $command{-previous}         if ! $command{-prev};
$command{-goto}            = $command{-jump}             if ! $command{-goto};
$command{'--help'}         = $command{-help}             if ! $command{'--help'};
$command{'--verbose'}      = $command{-verbose}          if ! $command{'--verbose'};

#
# if the name of this "executable" is also the name of a player, then select
# that player as the default player
#
# with a player named 'Kitchen' (say) and an appropriate hard link:
#
#   ln -v /usr/local/bin/squeezy /usr/local/bin/kitchen
#
# this allows you to say things like "kitchen -quieter"; see also the
# -print_links option
#

if ( $command{-$my_name} && $command{-$my_name}{is_player} )
   { command -$my_name; }

#
# argument processing (the main loop)
#

while ( 0 <= $#ARGV )
{
   my $arg  = shift @ARGV;
   my @whom = defined $group ? @{$group[$group]} : ( $def_player );
   ## FIXME some commands operate on server only...

   if ( $command{$arg} )
   {
      #
      # argument checking (to the extent that that's possible here)
      #

      if ( $command{$arg}->{do_shift} )
      {
	 my $argument = $ARGV[0];
	 if ( ! defined $argument )
	    { die "squeezy $arg: no argument where argument required\n"; }
	 if ( $command{$argument} )
	    { die "squeezy $arg: invalid argument ($argument, which rather looks like an option)\n"; }
      }

      #
      # command processing
      #
      foreach my $p ( @whom )
      {
	 if ( exists $p->{name} || $command{$arg}->{server} ) {
 		$socket = $p->{SOCKET} || undef;
 		$player = $p->{pid} || $p->{name} || undef;
                $cpo =  $p; # Needed for groups as def player not set correctly.
                $server = $p->{server} ? $p->{server}{IP} || $p->{server}{HOST} : $server;
		if ($skip->{$player}) {
			$skip->{$player} = 0;
			next;
		}
                $port = $p->{PORT} || $port;
                if ( $command{$arg}->{json} ) {
                    ## FIXME some commands operate on server only...
	 	    command $arg, ($p->{server} ||$servers[0]), $p, $ARGV[0];
                } else {
	 	    command $arg, $ARGV[0];
                }

	 } else {
		 die "squeezy invalid player object $p\n";
	 }
      }
      
      if ( $command{$arg}->{do_shift} )
	 { shift @ARGV; }

      if ( $arg =~ /-if_.*/ )
	{ $not = 0; }

      next;
   }

   die "squeezy error: unknown argument ($arg)\n";
}

exit(0);
