#!/usr/bin/perl -w
# snstatus_json.pl: get status of players connected to SN through JSON-RPC
# Caveat: Slapped over the previous version using the XML interface.
# Version 1.1: uses mysqueezebox.com. 
# Use release 1.0 for squeezenetwork.com

# Copyright (C) 2009 epoch1970 - v. 1.1 29-sep-2009 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# version 2, which should be included with this software, but isn't.


my ($rcs) = (
 ' $Id: snstatus_json.pl,v 1.4 2009/02/24 22:15:28 prax Exp prax $ '
	     =~ /(\d+(\.\d+)+)/ );	

use strict;
use HTTP::Cookies;
use JSON::RPC::Client;
#use Data::Dumper; # use it when setting $run{debug} to 1.

#
# DEFINE YOUR OWN mySqueezeBox.com ACCOUNT EMAIL AND PASSWORD HERE !! 
#
my $mySBlogin = 'your_email_address';
my $mySBpass = 'your_password';


my %run = (# run every x secs
	   loopfreq => 25,
	   # useless loop counter
	   loop => 0,
	   # next run at... (filled @ runtime)
	   at => 0,
	   # more traces - use Data::Dumper above as well
	   debug => 0,
	   # Set @ runtime when a valid session is detected
	   session => 0,
	   # refs to share the objects, set @ runtime
	   ua => 'ref to JSON::RPC::Client object',
	   content => 'ref to JSON::RPC::ReturnObject',
	  );
my %SN = ( # URLs and params for login and JSON RPC 
	  login => { url => "http://www.mysqueezebox.com/user/login",
		     params => {  email =>  "$mySBlogin",
				  password => "$mySBpass",
				  login => 'Sign+in',
				  nextPage => '/userHome',
				  state => '' },
		   },
	  jsonrpc => { url => "http://www.mysqueezebox.com/jsonrpc.js",
		       params => { cookie_jar => { autosave => 0},
				   agent => "snstatus_json_v_$rcs",
				   from => "$mySBlogin",
				   timeout => 60 }
		     }
	 );
# Start with an empty definition for squeezeplayers statuses.
my %SB = ( );


sub main {
  END { 
    print("Exiting after $run{loop} runs.\n");
  }

  die "Edit this script and enter your login/password before running it!\n" if ($SN{login}->{params}->{email} eq "your_email_address" || $SN{login}->{params}->{password} eq "your_password");

  my @query;
  print "[$0] Starting. Status updates every $run{loopfreq} secs at most. Press Ctrl-c to break.\n";
  while (1) {
    local $SIG{INT} = sub { print "\n"; exit };

    # Need a session to get status info. Stored in a cookie.
    logintoSN() unless validSession();
    # Request players status via JSON, update our players list
    @query = ['',['serverstatus',0,999]]; # Server status
    updateServerstatus() if jsonQuery(@query);

    # Loop through the connected players
    foreach my $playerid (sort keys %SB) {
      if ($SB{$playerid}->{onSN}  &&
	  $SB{$playerid}->{onSN} eq 1) {
	# Request alarms list
	@query = [$playerid,['alarms','0','999']]; # Alarms list - Breaks on mySB ?
	updateAlarms($playerid) if jsonQuery(@query);
	sleep(5);
	# Request playing status
	@query = [$playerid,['status','-',1,'tags:gABbehldiqtyrSuoKLN']]; # Now playing
	updatePlayMode($playerid) if jsonQuery(@query);
	sleep(5);
      }
    }

    printStatus();
    sleepwait();
    $run{loop}++;
  }
}

###
sub logintoSN {
  # Reset reference to the JSON RPC client (an LWP user agent)
  undef $run{ua};
  # Get a new client with some LWP::UserAgent characteristics
  my $client = JSON::RPC::Client->new;
  while (my ($method, $uaparam) = each %{ $SN{jsonrpc}->{params} }) {
    $client->ua->$method($uaparam);
  }

  # Open an SN session - See URL source & wiki fields
  print "Opening session with www.mysqueezebox.com\n";
  # Using UA for a simple HTTP post
  my $response = $client->ua->post($SN{login}->{url}, \%{ $SN{login}->{params} });

  if ($response->is_error) {
    my $status = $response->status_line;
    chomp($status);
    print "Login failed. Error [$status] \n";
  }
  else {
    # Set shared ref to JSON CLient object. Used in session checking
    $run{ua} = \$client;
    my $cookies = $client->ua->cookie_jar->as_string;
    chomp($cookies); # :-)
    print "Login success. Got cookie [$cookies] \n";
  }
}

###
sub validSession {
  # Need a JSON::RPC::Client object
  return unless ( defined $run{ua} && 
		  ref($run{ua} )   &&
		  ref(${ $run{ua} }) eq 'JSON::RPC::Client'); 
  my $client = ${ $run{ua} };

  # Reset session flag
  $run{session} = 0;

  my $now = time();
  print "[$now] Checking session cookie:" if ($run{debug});
  # Check every cookie for a valid session. Overkill: there's only one cookie in the jar ?
  $client->ua->cookie_jar->scan( sub { $run{session} = 1 if ( $_[1] eq 'sdi_squeezenetwork_session' &&
							      $_[4] eq 'www.mysqueezebox.com'       &&
							      $_[3] eq "/"                          &&
							      $_[8] > $now ); } );
  if ($run{session} == 0) {
    my $cookies = $client->ua->cookie_jar->as_string;
    chomp($cookies); 
    print " No valid session in cookie jar [$cookies]\n" if ($run{debug});
  }
  else { 
    print "ok.\n" if ($run{debug}); 
  }
  return $run{session};
}

###
sub jsonQuery {
  my $queryparams = shift;
  # require a query and a session
  return unless ( ref($queryparams) eq 'ARRAY' && 
		  $run{session} );
  my $client = ${ $run{ua} };
  # Return 1 if no error - undef response just in case
  my $val = 0;
  undef $run{content};

  my $now = time();
  # Build the query for JSON::RPC::Client - FIXME: id => 1 is wrong.
  my $query = {
	       id => '1',
	       method => 'slim.request',
	       params => $queryparams
	      };
  my $response = $client->call($SN{jsonrpc}->{url}, $query);

  if ($response->is_error) {
    print "[$now] Query Error: ".  $response->error_message ."\n";
  }
  else {
    $val = 1;
    # Shared ref to JSON::RPC::ReturnObject content hash
    $run{content} = \%{ $response->result };
    print "JSON response -- BEGIN\n" . Dumper($response->content) . "JSON response -- END\n" if ($run{debug});
  }
  return $val;
}

### Populate the %SB hash
sub updateServerstatus {
  foreach my $player (@{ $run{content}->{players_loop} }) {
    my $uuid = $player->{playerid};
    my $d = "[0-9A-Fa-f]";
    my $dd = "$d$d";
    if ($uuid =~ /($dd(:)$dd(\2$dd){4})/o) {
      $SB{$uuid}->{onSN} = $player->{connected};
      # SN: power does not seem to be reliable here ? In player mode it is. - mySB: have another look
      # $SB{$uuid}->{power} = $player->{power};
      $SB{$uuid}->{name} = $player->{name};
      # Seen this player 
      $SB{$uuid}->{uptodate} = 1;
    }
    else {
      print "Skipping player [$player->{name}]: strange uuid [$player->{playerid}]\n" if ($run{debug});
    }
  } 
}

sub updateAlarms {
  my $uuid = shift;
  return unless $uuid;
  # Zap alarms in %SB if we've seen the player in order to refresh
  undef $SB{$uuid}->{alarms} if ($SB{$uuid}->{uptodate} eq 1);
  foreach my $alarm (@{ $run{content}->{alarms_loop} }) {
    my $alarmid =  $alarm->{id};
    $SB{$uuid}->{alarms}->{$alarmid}->{time} = $alarm->{time};
    $SB{$uuid}->{alarms}->{$alarmid}->{dows} = $alarm->{dow};
    $SB{$uuid}->{alarms}->{$alarmid}->{repeat} = $alarm->{repeat};
    $SB{$uuid}->{alarms}->{$alarmid}->{url} = $alarm->{url};
    $SB{$uuid}->{alarms}->{$alarmid}->{enabled} = $alarm->{enabled};
  }
}

sub updatePlayMode {
  my $uuid = shift;
  return unless $uuid;
  # SN: This is the reliable power status info (??)
  $SB{$uuid}->{power} = $run{content}->{power};
  $SB{$uuid}->{mode} = $run{content}->{mode};
  $SB{$uuid}->{volume} = $run{content}->{'mixer volume'};
  $SB{$uuid}->{title} =  $run{content}->{playlist_loop}[0]->{title};
  $SB{$uuid}->{artist} = $run{content}->{playlist_loop}[0]->{artist};
  $SB{$uuid}->{url} = $run{content}->{playlist_loop}[0]->{url};
}

###
sub printStatus {
  my $now = time();
  print "Players hash -- BEGIN\n" . Dumper(%SB) . "Players hash -- END\n" if ($run{debug});

  foreach my $player (sort keys %SB) {
    my $sbref = \%{ $SB{$player} };

    # On mySB ?
    my $status = ($sbref->{onSN}) ? "on mySB" : "not on mySB";
    $status .= ($sbref->{power}) ? ", power on" : ", power off" if($sbref->{onSN}); # cf. power status quirk
    printf("[$now] Player %s %s status: $status\n", 
	   "[$sbref->{name}]", 
	   ($sbref->{uptodate}) ? "current" : "last known");
    
    if ($sbref->{onSN}) {
      # Play/pause/stop
      my $mode = "Mode $sbref->{mode}";
      if ($sbref->{mode} eq "play") {
	$mode .= ($sbref->{title}) ? ", Title [$sbref->{title}]" : ();
	$mode .= ($sbref->{artist}) ? " by [$sbref->{artist}]" : ();
	$mode .= ($sbref->{url}) ? " from [$sbref->{url}]" : ();
	$mode .= "(Volume: $sbref->{volume})";
      }
      print("\t $mode \n") if ($mode ne "");
      # Alarms list
      my @alarms;
      foreach my $alarm (sort keys %{ $sbref->{alarms} }) {
	my $alarmref = \%{ $sbref->{alarms}->{$alarm} };
	if ($alarmref->{enabled} eq 1) {
	  my $url = substr($alarmref->{url},0,25);
	  push (@alarms, "$alarmref->{time}\@$alarmref->{dows}: $url");
	}
      }
      my $alarmlist;
      $alarmlist = $#alarms + 1 ." Alarms [" . join(" | ", @alarms) . "]" if (@alarms);
      print("\t $alarmlist \n") if ($alarmlist && $alarmlist ne "");
    }
    # Reset uptodate flag - FIXME Only serves when a player is deleted from SN ?
    undef $sbref->{uptodate};
  }
}

###
sub sleepwait {
  my $now = time();
  my $wait = $run{at} - $now;
  if ($wait >= 1) {
    print "[$now] Sleeping $wait more seconds\n";
    sleep($wait); 
  }
  $run{at} = time() + $run{loopfreq};
}

###
main();
1;
__END__
