#!/usr/bin/perl -w
# pclink.pl version 0.5a
# Written by Nathan Peterson

use IO::Socket;
use IO::Select;

# Note these global variables should be edited to your configuration

$global_myname = "Nathan's MP3s";
$global_mp3dir = "/home/nathan/public_html/music";
$global_mp3url = "http://<me>:80/~nathan/music";

# $global_myname
# -  this is the pclink server name that will show up on your streamium
# $global_mp3dir
# -  this is the base dir were the mp3s are on your harddrive
# -  make sure to configure this and the mp3url variable if you
# -  are using the dir() option in your nodesfile
# $global_mp3url
# -  this is the url pointing to the same dir as $global_mp3dir.
# -  note that if you use the string "<me>" in the url it will be replaced
# -  later by your actual IP as seen by the streamium


# Read in nodesfile
open NODES, "<nodesfile.txt" or die "can't open nodesfile!";
$maxnode = 0;
while(<NODES>){
  if(/^\#/ || $_ eq "\n"){ next; } # ignore commented or blank lines
  ($node,$name,$links) = split /;/;
  $nodes[$node] = "$name;$links";
  chomp($nodes[$node]);

  $maxnode = ($node > $maxnode ? $node : $maxnode);
}
close NODES;

# put mp3 file urls in nodes array
$n = $maxnode;
for($i=0;$i<=$maxnode;$i++){
  next if (!defined($nodes[$i]) || $nodes[$i] !~ /\;dir\(/);
  $nodes[$i] =~ /^(.*);dir\((.*)\)/;
  $name = $1;
  $dir = $2;
  $nodes[$i] = "$name;";
  opendir DIR, $global_mp3dir."/".$dir;
  @files = sort (grep !/^\.\.?\z/, readdir DIR);
  closedir DIR;
  foreach $name (@files){
    $n++;

    $url = $global_mp3url."/$dir/$name";
    $url =~ s/ /%20/g; # streamium doesn't like spaces
    $nodes[$n] = "$name;$url";
    $nodes[$i] = $nodes[$i].$n.",";
  }
  if($nodes[$i] =~ /,$/){ chop($nodes[$i]); }
}

# Will need for later
$sock_sel = new IO::Select();

# Open UDP sock for listening
$udpsock = new IO::Socket::INET (LocalPort => 42591,
                                 Proto     => 'udp'
                                );
die "Could not connect: $!" unless $udpsock;

RESTART: # jump back to here if we need to restart the server

# Wait for UDP broadcast
$udpsock->recv($datagram, 4096);
$clientIP = $udpsock->peerhost();

print "$datagram\n\n";
# start over if not pclink client
if($datagram !~ /^<PCLinkClient>/){ goto(RESTART); }

# Open tcpsock connection
$hellosock = new IO::Socket::INET (PeerAddr => $clientIP,
                                   PeerPort => 42951,
                                   Proto    => 'tcp'
                                  );
die "Socket could not be created.  Reason: $!\n" unless $hellosock;

# record my IP address for later
$myIP = $hellosock->sockhost();

# Send Hello, close connection
&hello_resp($hellosock,$global_myname);
close ($hellosock);

# Open tcpsock for listening
$pclinksock = new IO::Socket::INET (LocalPort => 42951,
                                    Proto     => 'tcp',
                                    Listen    => 1,
                                    Reuse     => 1
                                   );
die "Could not connect: $!" unless $pclinksock;


$sock_sel->add($udpsock);
$sock_sel->add($pclinksock);

while(1){
  # get a set of readable handles (blocks until at least one handle is ready)
  # take all readable handles in turn
  @ready = $sock_sel->can_read();
  foreach $rsock (@ready) {
    # if it is pclinksock then we should accept(), read, and respond
    if($rsock == $pclinksock){
      $connection = $pclinksock->accept();
      ($node,$elem,$index) = &get_node($connection);

      $data = &make_xml($node,$elem,$index);
      &pclink_send($connection, $data);
      close($connection);
    }
    # if it is udpsock then client has reset so we must close tcp sock and restart server
    # note that it highly unlikely that some non-pclink client is broadcasting on this port, so we will take our chances.
    elsif($rsock == $udpsock){
      $sock_sel->remove($udpsock);
      $sock_sel->remove($pclinksock);
      close($pclinksock);
      goto(RESTART);
    }
    # otherwise wtf?!?
    else {
      die "unknown handle: $rsock";
    }
  }
}


################
## Subroutines
################

sub hello_resp {
  my ($sock,$name) = @_;
  my ($IP) = $sock->sockhost();
  my (@IP) = split /\./,$IP;

  # convert IP address to little endian
  $IP = $IP[0] + $IP[1]*0x100 + $IP[2]*0x10000 + $IP[3]*0x1000000;

  my ($hello) = "<PCLinkServer><Version>1.0</Version><VendorID>MUSICMATCH</VendorID><name>$name</name><ShortName>$name</ShortName><IP>$IP</IP><Port>51111</Port></PCLinkServer>\n";

  print $sock $hello;
  print $hello;
  $sock->flush(); # is this necessary?
}

sub pclink_send {
  my ($sock,$data) = @_;
  my ($datalen) = length $data;
  my ($header) = "HTTP/1.0 200 OK\r\nAccept-Ranges: bytes\r\nContent-Length:$datalen\r\nContent-Type: text/xml\r\n\r\n";

  print $sock $header.$data;
  print $header.$data;
  $sock->flush(); # is this necessary?
}

sub get_node {
  my ($sock) = @_;
  my ($datagram,$nodeid,$numelem,$fromindex);

  $sock->recv($datagram, 4096);
  print "\n\n$datagram\n";
  $nodeid = ($datagram =~ /<nodeid>(.*)<\/nodeid>/ ? $1 : 0);
  $numelem = ($datagram =~ /<numelem>(.*)<\/numelem>/ ? $1 : 0);
  $fromindex = ($datagram =~ /<fromindex>(.*)<\/fromindex>/ ? $1 : 0);
  return ($nodeid,$numelem,$fromindex);
}

sub make_xml {
  my ($node,$elem,$index) = @_;
  my ($nodeline) = $nodes[$node];
  my ($links) = (split /;/, $nodeline)[1];
  my (@links) = split /,/, $links;
  my ($totnumelem) = scalar @links;
  my ($xml,$i,$name,$url,$len,@files);

  $xml = "<contentdataset>";

  $numelem = $totnumelem;
  for($i=$index;$i<$numelem;$i++){
    $nodeline = $nodes[$links[$i]];
    ($name,$_) = split /;/, $nodeline;
    if(/^http:/){
      s/http:\/\/<me>/http:\/\/$myIP/;
      $url = (split /,/)[0];
      $url =~ s/ /%20/g; # streamium doesn't like spaces
      $len = /length\((.*)\)/ ? $1 : "";
      $xml = $xml."<contentdata><name>$name</name><nodeid>$links[$i]</nodeid><playable/><url>$url</url>";
      $xml = $xml."<title>$name</title><album></album><trackno></trackno><artist></artist>";
      $xml = $xml."<genre></genre><year></year><bitrate></bitrate><playlength>$len</playlength></contentdata>";
    }
    else{
      $xml = $xml."<contentdata><name>$name</name><nodeid>$links[$i]</nodeid><branch/></contentdata>";
    }
  }
  $xml = $xml."<totnumelem>$totnumelem</totnumelem><fromindex>0</fromindex><numelem>$numelem</numelem><alphanumeric/></contentdataset>\n";

  return $xml;
}

