diff --git a/shairport.pl b/shairport.pl index e54280ce..1a752b00 100755 --- a/shairport.pl +++ b/shairport.pl @@ -23,10 +23,12 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. -use 5.10.0; - use strict; +use 5.10.0; +# For given() { when() { } ... } +use feature ":5.10"; + use constant PORT => 5000; use Getopt::Long; @@ -38,6 +40,7 @@ use IO::Socket; use MIME::Base64; use HTTP::Request; use HTTP::Response; +use URI::Escape; use IPC::Open2; use Crypt::OpenSSL::RSA; use Digest::MD5 qw/md5_hex/; @@ -60,6 +63,14 @@ my $libao_devicename; my $libao_deviceid; # suppose hairtunes is under same directory my $hairtunes_cli = $FindBin::Bin . '/hairtunes'; +# Integrate with Squeezebox Server +my $squeeze; +# SBS CLI port +my $cliport; +# SB target +my $mac; +# SB volume +my $volume; # output debugging information my $verbose; # where to write PID @@ -82,6 +93,10 @@ GetOptions("a|apname=s" => \$apname, "ao_deviceid=s" => \$libao_deviceid, "v|verbose" => \$verbose, "w|writepid=s" => \$writepid, + "s|squeezebox" => \$squeeze, + "c|cliport=s" => \$cliport, + "m|mac=s" => \$mac, + "l|volume=s" => \$volume, "h|help" => \$help); sub usage { @@ -96,6 +111,10 @@ sub usage { " --ao_driver=driver Sets the ao driver (optional)\n", " --ao_devicename=devicename Sets the ao device name (optional)\n", " --ao_deviceid=id Sets the ao device id (optional)\n", + " -s --squeezebox Enables local Squeezebox Server integration\n", + " -c --cliport=port Sets the SBS CLI port\n", + " -m --mac=address Sets the SB target device\n", + " -l --volume=level Sets the SB volume level (in %)\n", " -d Daemon mode\n", " -w --writepid=path Write PID to this location\n", " -v --verbose Print debugging messages\n", @@ -112,6 +131,81 @@ if (defined($verbose) && $verbose) { $verbose = 0; } +$cliport = ( (defined( $cliport ) && $cliport && $cliport =~ m/^[0-9]+/ && $cliport > 1023 && $cliport < 65536 ) ? $cliport : 9090 ); + +$volume = ( (defined( $volume ) && $volume && $volume =~ m/^[0-9]+/ && $volume >= 0 && $volume <= 101 ) ? $volume : undef ); + +if (defined($squeeze) && $squeeze) { + my $players; + my @details; + + my $response; + my $socket = IO::Socket::INET -> new ( + PeerAddr => "127.0.0.1" + , PeerPort => $cliport + , Proto => 'tcp' + , Timeout => 1 + ); + if( !( $socket ) ) { + print "ERROR: Could not create socket to interface with SqueezeBox Server on port $cliport: $!\n"; + print "WARN: Disabling Squeezebox Server integration\n"; + undef $squeeze; + } + if( $squeeze ) { + print $socket "player count ?\n"; + $response = <$socket>; + if( !( defined( $response ) ) ) { + print "ERROR: Could not communicate with SqueezeBox Server on port $cliport\n"; + print "WARN: Disabling Squeezebox Server integration\n"; + undef $squeeze; + } else { + $players = ( $response =~ m/^player count ([0-9]+)$/ ); + print $socket "players 0 $players\n"; + $response = <$socket>; + @details = split( /playerindex%3A/ , $response ); + close( $socket ); + shift( @details ); + for( my $n = 0; $n <= scalar( @details ); $n++ ) { + if( defined( $details[ $n ] ) ) { + my $address = $details[ $n ]; + $address =~ s/^.*playerid%3A([[:xdigit:]%]+)\s.*$/$1/; + $address =~ s/%3A/:/g; + chomp $address; + $details[ $n ] = $address; + } + } + print "Discovered players: $players\n" if $verbose; + print "Player MAC addresses:\n" if $verbose; + foreach my $address (@details) { + print "\t$address\n" if( ( defined( $address ) && $address ) && $verbose ); + } + + if( defined( $mac ) && $mac ) { + chomp $mac; + if( !( grep { lc( $_ ) eq lc( $mac ) } @details ) ) { + print "ERROR: Invalid or non-present MAC specified.\n\n"; + print "Please select a target MAC address from:\n"; + foreach my $address (@details) { + print "\t$address\n" if( defined( $address ) && $address ); + } + exit(1); + } + } else { + if( 1 == $players ) { + $mac = $details[ 0 ]; + print "WARN: No Squeezebox player specified, using $mac.\n"; + } else { + print "ERROR: No Squeezebox player specified, please select a target MAC address with the '--mac' option with a value from:\n"; + foreach my $address (@details) { + print "\t$address\n" if( defined( $address ) && $address ); + } + exit(1); + } + } + $mac = uri_escape( $mac ) if( defined( $mac ) && $mac ); + } + } +}; chomp $apname; my @hw_addr = (0, map { int rand 256 } 1..5); @@ -124,10 +218,17 @@ $SIG{PIPE} = \&POPE; our $avahi_publish; +our $squeezebox_setup; sub REAP { - if ($avahi_publish == waitpid(-1, WNOHANG)) { - die("avahi daemon terminated or 'avahi-publish-service' binary not found"); + my $pid = waitpid( -1, WNOHANG ); + given( $pid ) { + when( $avahi_publish ) { + die( "avahi daemon terminated or 'avahi-publish-service' binary not found" ); + } + when( $squeezebox_setup ) { + print( "Squeezebox configuration routine completed\n" ) if $verbose; + } } print("Child exited\n") if $verbose; $SIG{CHLD} = \&REAP; @@ -140,11 +241,21 @@ $SIG{TERM} = $SIG{INT} = sub { print basename($0) . " killed\n"; map { eval { kill $_->{decoder_pid} } } keys %conns; kill 9, $avahi_publish if $avahi_publish; + # Clean up any running squeezebox_setup processes... + my $child; + do { + $child = waitpid( -1, WNOHANG ); + } while $child > 0; exit 0; }; $SIG{__DIE__} = sub { map { eval { kill $_->{decoder_pid} } } keys %conns; kill 9, $avahi_publish if $avahi_publish; + # Clean up any running squeezebox_setup processes... + my $child; + do { + $child = waitpid( -1, WNOHANG ); + } while $child > 0; }; $avahi_publish = fork(); @@ -224,6 +335,147 @@ if (defined($writepid) && $writepid) { print "Listening...\n" if $verbose; +sub performSqueezeboxSetup { + $squeezebox_setup = fork(); + if( 0 == $squeezebox_setup ) { + my $items; + my @favourites; + my @ids; + my $index; + + my $response; + + my $findFavourites = sub { + my ( $socket ) = @_; + print $socket "favorites items\n"; + $response = <$socket>; + $response =~ s/^\s*favorites\s+items\s+count%3A([0-9]+)\s*$/$1/; + $items = $response; + print "Found $items favourites...\n" if $verbose; + + print $socket "favorites items 0 $items want_url%3A1\n"; + $response = <$socket>; + undef( @favourites ); + @favourites = split( /id%3A/ , $response ); + @ids = split( /id%3A/ , $response ); + shift( @favourites ); + shift( @ids ); + for( my $n = 0; $n <= scalar( @favourites ); $n++ ) { + if( defined( $favourites[ $n ] ) ) { + my $url = $favourites[ $n ]; + $url =~ s/^.*url%3A([^ ]+)\s.*$/$1/; + $url = uri_unescape( $url ); + chomp $url; + print "\tFavourite with URL '$url' " if $verbose; + $favourites[ $n ] = $url; + } + if( defined( $ids[ $n ] ) ) { + my $id = $ids[ $n ]; + $id =~ s/^([^ ]+)\s.*$/$1/; + chomp $id; + my @components = split( /\./, $id ); + shift ( @components ); + $id = join( '.', @components ); + print "and ID '$id'\n" if $verbose; + $ids[ $n ] = $id; + } + } + }; + + my $socket = IO::Socket::INET -> new ( + PeerAddr => "127.0.0.1" + , PeerPort => ( (defined( $cliport ) and $cliport ) ? $cliport : 9090 ) + , Proto => 'tcp' + , Timeout => 1 + ) or die "Could not create socket: $!"; + + &$findFavourites( $socket ); + + print "Favourites URLs:\n" if $verbose; + foreach my $url (@favourites) { + print "\t$url\n" if( ( defined( $url ) && $url ) && $verbose ); + } + my $okay = 1; + if( !( grep { $_ =~ m/^wavin:/ } @favourites ) ) { + $okay = 0; + print "INFO: AirPlay 'wavin' Favourite does not exist - creating... "; + print $socket "favorites add url%3Awavin%3Aairplay title%3AAirPlay\n"; + $response = <$socket>; + if( $response =~ m/\scount%3A1/ ) { + print "done\nINFO: AirPlay 'wavin' favourite successfully created.\n"; + $okay = 1; + } else { + print "failed\nWARN: Could not create AirPlay favourite\n"; + print " Server response was $response\n"; + } + if( $okay ) { + &$findFavourites( $socket ); + + print "Updated Favourites URLs:\n" if $verbose; + foreach my $url (@favourites) { + print "\t$url\n" if( ( defined( $url ) && $url ) && $verbose ); + } + if( !( grep { $_ =~ m/^wavin:/ } @favourites ) ) { + print "WARN: Cloud not identify AirPlay Favourite, even after creating it - disabling SqueezeBox integration\n"; + $squeeze = 0; + $okay = 0; + } + } + } + if( $okay ) { + for ( my $n = 0 ; !( defined( $index ) ) && $n <= scalar( @favourites ) ; $n++ ) { + if( $favourites[ $n ] =~ m/^wavin:/ ) { + $index = $n; + print "Found favourite '" . $favourites[ $index ] . "' with ID '" . $ids[ $index ] . "' at position $index.\n" if $verbose; + } + } + #print "Turning on player... " if $verbose; + # For some reason, this one dies... + #eval { + # local $SIG{ALRM} = sub { die "Socket Operation Timed Out" }; + # print $socket "$mac power 1\n"; + # alarm 1; + # $response = <$socket>; + # print "$response\n" if $verbose; + # alarm 0; + #}; + #... even within an eval{} loop?! + + print "Showing message... " if $verbose; + print $socket "$mac show line2%3AStarting%20AirPlay duration%3A5 brightness%3ApowerOn font%3Ahuge\n"; + $response = <$socket>; + print "$response\n" if $verbose; + + if( defined( $volume ) ) { + print "Setting player volume... " if $verbose; + print $socket "$mac mixer volume $volume\n"; + $response = <$socket>; + print "$response\n" if $verbose; + } + + print "Unmuting player... " if $verbose; + print $socket "$mac mixer muting 0\n"; + $response = <$socket>; + print "$response\n" if $verbose; + + if( defined( $index ) ) { + print "Playing favourite... " if $verbose; + my $id = uri_escape( $ids[ $index ] ); + print $socket "$mac favorites playlist play item_id%3A$id\n"; + $response = <$socket>; + } else { + print "Resuming play... " if $verbose; + print $socket "$mac play\n"; + $response = <$socket>; + } + print "$response\n" if $verbose; + } + close( $socket ); + + exit(0); + } +}; + while (1) { my @waiting = $sel->can_read; foreach my $fh (@waiting) { @@ -234,6 +486,10 @@ while (1) { $sel->add($new); $new->blocking(0); $conns{$new} = {fh => $fh}; + + if (defined($squeeze) && $squeeze) { + &performSqueezeboxSetup(); + } } else { if (eof($fh)) { print "Closed: $fh\n" if $verbose;