#!/usr/bin/perl use strict; use Audio::Beep; use Data::Dumper; use Getopt::Long; use IO::Socket; use IO::Select; use Pod::Usage; use Net::Telnet; use Time::HiRes qw(time); use URI::Escape; use Net::Google::Spreadsheets; my ($sc_addr, $playlist_name, $player_id); my ($google_username, $google_password); GetOptions('server=s' => \$sc_addr, 'playlist=s' => \$playlist_name, 'player=s' => \$player_id, 'username=s' => \$google_username, 'password=s' => \$google_password ) or pod2usage(); # squeezecenter connection print("SqueezeCenter:\t$sc_addr\n"); my $cli = cli_open($sc_addr); my $r; my $unknown = ''; # check player is connected $r = cli_request($cli, 'players', 0, 50); my $players = cli_args($r, 'playerindex'); my $player; foreach my $p (@$players) { if ($p->{playerid} eq $player_id && $p->{connected} == 1) { $player = $p; } } die "No player $player_id" unless $player; print("Player:\t\t" . $player->{name} . " ($player_id)\n"); # show briefly updates cli_subscribe($cli, undef, $player_id, 'displaystatus', 'subscribe:showbriefly'); # check player status $r = cli_request($cli, $player_id, 'status', '0', '2', 'tags:galdorTl'); my $status = cli_args($r, 'player_name')->[0]; # load worksheet my $sheet_name = ""; #$sheet_name .= "test_"; $sheet_name .= $player->{model}; if ($status->{signalstrength} == 0) { $sheet_name .= "_eth"; } else { $sheet_name .= "_wlan"; } print "Sheet: " . $sheet_name . "\n"; my $worksheet = player_worksheet("Radio QA", $sheet_name, [ 'State', 'Title', 'Time', 'Rebuf', 'Type', 'Bitrate', 'URL', 'TestDate', 'TestBy', ]); my $played_urls = {}; # picks/radiotime menu $r = cli_request($cli, 'picks', 'items', 0, 1000); load_items(cli_args($r, 'id')); print "Total stations: " . scalar(keys %$played_urls) . "\n"; print "Unknown:\n" . $unknown; sub load_items { my ($tracks) = @_; foreach my $track (@$tracks) { if ($track->{hasitems}) { print "Name: " . $track->{name} . "\n"; my $r = cli_request($cli, 'picks', 'items', 0, 1000, 'item_id:' . $track->{id}, 'want_url:1'); load_items(cli_args($r, 'id')); } elsif ($track->{type} eq 'audio') { play_item($track); } else { print "UNKNOWN: ".Dumper($track)."\n"; } } } sub play_item { my ($track) = @_; $played_urls->{$track->{url}}++; # has this item already been tested? my $row = fetch_row($worksheet, 'url', $track->{url}); if (($row and $row->{state}) # retry streams... #and not ($row and $row->{state} eq '-' and $row->{time} == 0) ) { print "Skip: " . $track->{name} . ": " . $track->{url} . "\n"; return; } print "Test: " . $track->{name} . ": " . $track->{url} . "\n"; # test start upload_row($worksheet, 'url', { 'state' => 'Testing', 'title' => $track->{name}, 'url' => $track->{url}, }); # play item my $r = cli_request($cli, $player_id, 'picks', 'playlist', 'play', 'item_id:' . $track->{id}); # wait 20 seconds sleep(20); # check stream started $r = cli_request($cli, $player_id, 'status', '0', '2', 'tags:galdorTl'); my $status = cli_args($r, 'player_name')->[0]; if ($status->{time} > 0) { # wait for 2 mins total sleep((2 * 60) - 20); # test results $r = cli_request($cli, $player_id, 'status', '0', '2', 'tags:galdorTl'); $status = cli_args($r, 'player_name')->[0]; } my $ok = undef; my $rebuf = 0; while (1) { $r = cli_listen($cli); last if (!$r); my $display = cli_args($r, 'type')->[0]; my $line0 = $display->{line0}; # Connection status if ($line0 =~ /^(\d\d\d.*)/) { $ok = $1; next; } if ($line0 =~ /^HTTP\/\d\.\d\s+(\d\d\d.*)/) { $ok = $1; next; } if ($line0 =~ /^Error: (.*)/) { $ok = $1; next; } if ($line0 =~ /^Problem: (.*)/) { $ok = $1; next; } if ($line0 =~ /^(Cannot request.*)/) { $ok = $1; next; } if ($line0 =~ /^(Connection reset by local host)/) { $ok = $1; next; } if ($line0 =~ /^(Connect timed out)/) { $ok = $1; next; } if ($line0 =~ /^(Timed out waiting for data)/) { $ok = $1; next; } # Count rebuffering if ($line0 =~ /Rebuffering/) { $rebuf++; next; } # Mostly harmless if (($line0 =~ /^Now playing/i) or ($line0 =~ /^Stopped/i) or ($line0 =~ /^Fetching track/i) or ($line0 =~ /^Getting stream info/i) ) { next; } # Unknown $unknown .= $line0; print Dumper($display); } # player crashed? if ($status->{player_connected} == 0) { print "Player is not connected\n"; $ok = 'crash'; # audible alarm while ($status->{player_connected} == 0) { beep(); sleep(1); $r = cli_request($cli, $player_id, 'status', '0', '2', 'tags:galdorTl'); $status = cli_args($r, 'player_name')->[0]; } } if (not $ok and $status->{time} == 0) { $ok = "Did not play"; } upload_row($worksheet, 'url', { 'state' => $ok, 'rebuf' => $rebuf, 'title' => $track->{name}, 'time' => $status->{time}, 'type' => $status->{type}, 'bitrate' => $status->{bitrate}, 'url' => $track->{url}, }); } ### XXXX utils, to be moved # Open SqueezeCenter cli connection sub cli_open { my ($host) = @_; my $cli = { host => $host, }; # request connection $cli->{rcon} = new Net::Telnet( Timeout => 10, Prompt => '/^\s+$/', Host => $host, Port => 9090 ); $cli->{rcon}->open() || die "Can't open CLI"; # subscription connection $cli->{scon} = new Net::Telnet( Timeout => 10, Prompt => '/^\s+$/', Host => $host, Port => 9090 ); $cli->{scon}->open() || die "Can't open CLI"; return $cli; } # Send cli request sub _cli_request { my $telnet = shift; my $timeout = shift; if (scalar(@_)) { my $command = join(' ', map(uri_escape($_), @_)); $telnet->print($command); } my $line = $telnet->getline(Timeout => $timeout); chomp($line); my @elements = map(uri_unescape($_), split / /, $line); return \@elements; } sub cli_request { my $cli = shift; my $r; while (1) { eval { $r = _cli_request($cli->{rcon}, 30, @_); }; if ($@ eq "") { return $r; } print($@); print("cli request failed, trying again...\n"); sleep(1); $cli->{rcon}->open(); } } sub cli_subscribe { my $cli = shift; # TODO remember subscriptions return _cli_request($cli->{scon}, @_); } sub cli_listen { my $cli = shift; my $r; while (1) { eval { $r = _cli_request($cli->{scon}, 0, @_); }; if ($@ eq "") { return $r; } # timed out? if (!$cli->{scon}->eof()) { return; } print("cli notify failed, trying again...\n"); sleep(1); $cli->{scon}->open(); # TODO re-subscribe } } # Parse extended cli response. $key is the item seperator sub cli_args { my ($elements, $key) = @_; my @values; my $block; foreach my $line (@$elements) { my ($k, $v) = ($line =~ /([^:]+):(.+)/); next unless $k; if ($k eq $key) { $block = {}; push @values, $block; } if (defined $block) { $block->{$k} = $v; } } return \@values; } # Finds or creates a worksheet on google docs sub player_worksheet { my ($title, $sheet, $cols) = @_; # XXXX my $service = Net::Google::Spreadsheets->new( username => $google_username, password => $google_password ); # XXXX my $spreadsheet = $service->spreadsheet({ title => $title }); $spreadsheet || die "No spreadsheet '$title'"; my $worksheet = $spreadsheet->worksheet({ title => $sheet }); if (not $worksheet) { $worksheet = $spreadsheet->add_worksheet( { title => $sheet, row_count => 1000, col_count => 20, }); # XXXX my $i = 1; my @cells = (); foreach my $col (@$cols) { push @cells, { row => 1, col => $i++, input_value => $col }; } $worksheet->batchupdate_cell(@cells); } $worksheet->{cols} = $cols; return $worksheet; } # Fetch row from google docs sub _fetch_row { my ($worksheet, $key, $val) = @_; return if not $worksheet; # queries can't include non-alphanums $val =~ s/[^\w\d]/_/g; # queries can't start with \d if ($val =~ /^\d/) { $val = '_' . $val; } my $row = $worksheet->row({sq => lc($key) . "=" . $val}); return $row ? $row->content : 0; } sub fetch_row { my $r; while (1) { eval { $r = _fetch_row(@_); }; return $r if ($@ eq ""); print("fetch_row failed, trying again...\n"); sleep(1); } } # Updates the test results for this track at google docs sub _upload_row { my ($worksheet, $key, $cells) = @_; return if not $worksheet; $cells->{testdate} = localtime(); $cells->{testby} = $ENV{USERNAME}; foreach my $k (keys %$cells) { next if ($cells->{$k} =~ /^[\d\.]+$/); # queries can't include non-alphanums $cells->{$k} =~ s/[^\w\d]/_/g; # queries can't start with \d if ($cells->{$k} =~ /^\d/) { $cells->{$k} = '_' . $cells->{$k}; } } # add - for cells without data foreach my $col (@{$worksheet->{cols}}) { $col = lc($col); next if $col eq 'fail'; $cells->{$col} = '-' if not defined($cells->{$col}); } my $row = $worksheet->row({sq => lc($key) . "=" . $cells->{$key}}); if ($row) { $row->content($cells); } else { $worksheet->add_row($cells); } } sub upload_row { my $r; while (1) { eval { $r = _upload_row(@_); }; return $r if ($@ eq ""); print("upload_row failed, trying again...\n"); sleep(1); } } __END__ =head1 NAME radio_tuner =head1 SYNOPSIS radio_tuner [options] Options: --server squeezecenter address --player player mac address --username google docs username --password google docs password Example: radio_tuner.pl --server=192.168.1.199 --player=00:04:20:08:20:05 --username=user@slimdevices.com --password=passwd =cut