#!/usr/bin/perl
# mymusicd (version 1.0.6)
#
# email: caffiend@dioxin.com
# www: http://dioxin.com/~caffiend/mymusic
# aim: ilikecaffeine
#
# see CHANGES file for details on this version. 
# see the UPGRADE file for information on upgrading from a previous version.

# this program is distributed under the terms of the GNU Public License.
#
# if you're just wondering how this thing works, you'll need to be familiar
# with mysql, php, and perl (but not necessarily a guru in any of these. I'm 
# certainly not. ; )  the meat of this program is in the play_song subroutine.

my $binary	= "/usr/bin/mpg123";       #must be a command line mp3 player
my @args	= ('-q','-b','256'); #args for $binary (can be empty)

#default values to use (can be overridden on command line)
my $dfl_delay = 0; #delay before picking a new song
my $dfl_hist_size=100; #size of history table
my $dfl_host = "localhost"; #database host
my $dfl_user = "mymusic"; # database user
my $dfl_passwd = "mymusic"; # database password
my $dfl_db = "mymusic"; #database name

# no user-configurable variables below this line...
# ==============================

$version = '1.0.6';

use DBI;
use Getopt::Long;
use POSIX;
use POSIX ":sys_wait_h";
use Tie::RefHash;

GetOptions(
	"daemon" => \$daemonize,
        "help" => \$help,
	"host=s" => \$host,
	"user=s" => \$user,
	"passwd=s" => \$passwd,
	"db=s" => \$db,
        "hist-size=i" => \$hist_size);


if ($help) { &usage; }

if ($daemonize) { fork && exit; }

$SIG{INT} = 'set_kill';
$SIG{CHLD} = \&REAPER;

$host = $dfl_host unless $host;
$db = $dfl_db unless $db;
$user = $dfl_user unless $user;
$passwd = $dfl_passwd unless $passwd;

$hist_size = $dfl_hist_size unless $hist_size;

$delay =  $dfl_delay unless $delay;
die "delay must be non-negative!\n" unless $delay > -1;

# initialize variables...

my $query = 0;
my $sth=0;
my $entries=0;
my $position=0;
my $id=0;
my $song_id=0;
my $play=0;

my $title="";
my $artist="";
my $album="";
my $filename="";
my $randtime=0;
my $randflag=0;
my $playtime=0;

my %history = ();
tie %history, "Tie::RefHash";

##  get a db handle... (we'll be using this a lot)
$dbh = DBI->connect("DBI:mysql:$db:$host",$user,$passwd);

$query = "DELETE FROM history";
$dbh->do($query);

while ()    # main program loop
{

  if ($kill == 1) { &shutdown; }
  
  # check control table
  $query = "SELECT state FROM control LIMIT 1";
  $sth = $dbh->prepare($query);
  $sth->execute();
  $state = $sth->fetchrow_array;
  $sth->finish;

  if ($state eq "playing" && $playing == 0)  # should be playing a song, but we aren't
  {
    play_song();  # start one
  }
  elsif ($state eq 'playing' && $playing == 1)  # currently playing a song.
  {
    sleep 4;  # wait four seconds before trying again.
  }
  elsif ($state eq 'stopped')
  {
     if ($playing == 1)  # setting status to stopped should stop songs that are playing.
     {
       $rv = kill 'TERM', $pid;
     }
     sleep 4;  # what did you expect? We're stopped. : )
  }
  elsif ($state eq 'next')  # the ui has asked us to skip to the next song.
  {
     if ($playing == 1) #otherwise, there's really no song to kill. : )
     {
       $rv = 0;
       $rv = kill 'TERM', $pid;				#kill the player!
       sleep 1;
       $query = "UPDATE control SET state='playing'";  # change the state back, so we don't keep
       $dbh->do($query);											# skipping.
     }
     else # next chosen, but we aren't playing a song... change state to stopped
     {
       $query = "UPDATE control SET state='stopped'";
       $dbh->do($query);
     } 
  }    
  else
  {
  print "Error: Uknown value in control table: $state\n";
  print "Fixing...\n";
  print "If you see this message more than once, email caffiend\@dioxin.com\n";
  $query = "DELETE FROM control";
  $dbh->do($query);
  $query = "INSERT INTO control VALUES('playing')";
  $dbh->do($query);
  sleep 5;
  }

}

# only subroutines below this line...
#------------------------------------------------------------
#

sub play_song()   # this sub should really be broken down into digestable
		  # chunks. : ) 
{
  $query = "SELECT queue.time,queue.song_id FROM queue ORDER BY time";
   
  $sth = $dbh->prepare($query);
  $nqueued = $sth->execute();
  if ($nqueued == 0) 
  {
    ##  no songs in queue...
     $randflag = 1;
     $sth->finish();
     $query = "SELECT COUNT(*) FROM songs";
     $sth=$dbh->prepare($query);
     $sth->execute();
     $nsongs = $sth->fetchrow_array();

     $query = "SELECT COUNT(*) FROM history";
     $sth=$dbh->prepare($query);
     $sth->execute();
     $nhistory = $sth->fetchrow_array();  
     $sth->finish;
 
     %history = ();
     $query = "SELECT song FROM history ORDER BY played";
     $sth=$dbh->prepare($query);
     $sth->execute();
     while ($song = $sth->fetchrow_array() )
      {
       $history{$song} = 1;
      }

     $query = "SELECT id,votes FROM songs";
	  
     $sth=$dbh->prepare($query);
     $sth->execute();
     %map = ();
     while ($row=$sth->fetchrow_hashref() )
     {
       unless (exists $history{$row->{id}} ) # this takes out already played songs
	{
         $map{$row->{id}} = $row->{votes}+1; # maybe some songs have no votes
        }
     }
     $sth->finish;

     #pick a song
     $song_id = weighted_rand(weight_to_dist(%map));
     %map = ();
     $entries=1;
     } 
     else
     {
	## songs in queue...
	($date, $song_id) = $sth->fetchrow_array();
	$sth->finish();
	$randflag = 0;
	
	## now we remove that song from the queue...
	$query = "DELETE FROM queue WHERE song_id=$song_id";
	$sth=$dbh->prepare($query);
	$sth->execute();
	$sth->finish();
     }

     if ($entries != 0) {
     ## grab song info from db...
     $query = "SELECT title, artist, album, filename FROM songs WHERE id = $song_id";
     $sth = $dbh->prepare($query);
     if (($sth->execute()) == 0) {
	    print "Song not found in database!!!";
     }
     else
     {
       ($title,$artist,$album,$filename) = $sth->fetchrow_array();
       $sth->finish();
	    
       $query= "UPDATE nowplaying SET song_id='$song_id'";
       $dbh->do($query);
      
    ## update votes

	    if ($randflag != 1) {
		$query="UPDATE songs SET votes=votes+1 WHERE id='$song_id';";
		$sth=$dbh->prepare($query);
		$sth->execute();
		$sth->finish();
	    }
            
            #check the history. if it's full, replace the oldest one,
	    #otherwise add a new one.
            $query = "SELECT COUNT(*) AS num FROM history";
            $sth = $dbh->prepare($query);
	    $sth->execute();
	    $current_hist_size = $sth->fetchrow_hashref();
            $playdate = POSIX::strftime("%Y-%m-%d %H:%M:%S",localtime());

            #we only want the last $hist_size songs
	    if ($current_hist_size->{num} >= $hist_size) {

	        # we have to delete the oldest song.
		$query = "SELECT MIN(played) FROM history";
		$sth=$dbh->prepare($query);
		$sth->execute();
		$oldest = $sth->fetchrow_array();
		$sth->finish;
               $query = "DELETE FROM history WHERE played=\'$oldest\'";
	       $sth=$dbh->do($query);
            }

            $query = "INSERT INTO history VALUES('$playdate',$song_id)";
            $sth=$dbh->prepare($query);
            $sth->execute();
            $sth->finish();

	@execargs = (@args,$filename);  # construct player command

        $playing = 1;
        next if $pid = fork;	# parent

        die "fork: $!" unless defined $pid; #failure
        exec($binary,@execargs);  # finally, play the song... 
	exit;
	}
     }
  }

sub weight_to_dist {
    my %weights = @_;
    my %dist = ();
    my $total = 0;
    my ($key,$weight);
    local $_;
    
    foreach (values %weights) {
	$total+=$_;
    }
    while ( ($key, $weight) = each %weights ) {
	$dist{$key} = $weight/$total;
    }
    %weights = ();
    $total = 0;
    return %dist;
}

sub weighted_rand {
    my %dist = @_;
    my ($key, $weight);
    
    while (1) {
	my $rand = rand;
	while ( ($key, $weight) = each %dist ) {
	    return $key if ($rand -= $weight) <0;
	}
    }
}

sub REAPER
{
  my $stiff;
  while (($stiff = waitpid(-1,&WNOHANG)) > 0)
  {
  }
  $SIG{CHLD} = \&REAPER;

  $playing = 0;
}


sub set_kill
{
  $kill = 1;
}  

sub shutdown
{
   #  shut 'er down...
   printf "Shutting down...\n";
   $query = "UPDATE  nowplaying SET song_id = 0";
   $dbh->do($query) or warn "Couldn't do $query!\n";
   printf "Clearing history table...";
   $query = "DELETE FROM history";
   $dbh->do($query) or warn "Couldn't do $query!\n";
   printf "done.\n";
   printf "Setting status table...";
   $query = "UPDATE control SET state='off'";
   $dbh->do($query) or warn "Couldn't do $query!\n";
   printf "done.\n";
   printf "closing database connection...";
   $sth->finish;
   $dbh->disconnect;
   printf "done.\n";
   printf "killing %s...",$binary;
   printf "done\n";
   printf "stopping daemon...";
   printf "done.\n";
   printf "exiting...\n";

   exit;
}

sub usage
{
  print "mymusic version $version\n";
  print "http://dioxin.com/~caffiend/mymusic/\n";
  print "caffiend\@dioxin.com\n";
  print "--help\t\tthis message\n";
  print "--daemon\t\tgo into background\n";
  print "--host=[host]\t\tdatabase host\n";
  print "--user=[user]\t\tdatabase user\n";
  print "--passwd=[password]\t\tdatabase passwd\n";
  print "--db=[name]\t\tdatabase name\n";
  print "--hist-size=n\t\tsize of history to keep\n";
  print "=-" x 40 . "\n";
  print "to quit a running mymusicd, use <ctrl>+c";
  print "or";
  print "'killall -INT mymusicd' in daemon mode.\n";
  print;
  exit;
}
