#!/opt/bin/perl

# icedj
# version: 0.9.14
#
# - Icecast playlist manager and MP3 File Streamer
#
# Copyright (c) 1999 Arne Claassen <icedj@remixradio.com>
#
# Streaming code based on iceplay
# Copyright (c) 1999 Jack Moffitt, Barath Raghavan
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

use strict;
use IO::Socket;
use Time::Local;
use POSIX qw(:signal_h :errno_h :sys_wait_h);



# ____ Define Globals _________________________________________________________
use vars qw($RESET $DJSCRIPT $SILENT $DEBUG $BACKGROUND $QUIET);
use vars qw($SHOW $LOCKFILE $RESTART $SCHEDULE $CURRENT $PLAYLIST);
use vars qw(@CRONTAB @SCHEDULE @TRACKLIST @REQUESTLIST @TOPTEN);
use vars qw(@HISTORY @SHOWPLAYLIST @SHOWHISTORY @SHOWQUEUE);
use vars qw(%CONF %SONG %SCHEDULE %SHOW);

# ____ Set up Signal Handlers _________________________________________________
$SIG{HUP}= \&RESET;
$SIG{TERM}= \&KILLER;
$SIG{INT}= \&KILLER;
$SIG{QUIT}= \&KILLER;

# ____ Get Config _____________________________________________________________
my($ARG,$CONF);
foreach $ARG (@ARGV) {
	if($ARG =~ /--silent/i) { $SILENT = 1; }
	elsif($ARG =~ /--debug/i) { $DEBUG = 1; }
	elsif($ARG =~ /--quiet/i) { $QUIET = 1; }
	elsif($ARG =~ /--background/i) { $BACKGROUND = 1; }
	elsif($ARG !~ m#^--#) { $CONF = $ARG; }
	}

open(CONF, $CONF);
writeLog( "reading config from $CONF");
while (<CONF>) {
	chomp;
	if(/^#/) { next; } #comment, ignore line
	/(\S+)\s+(.*)$/;
	$CONF{$1} = $2;
	}
close(CONF);

($CONF{'datadir'},$CONF{'conf'}) = $CONF =~ m#(.*/)([^/]*?)$#;
if($CONF{'mode'} == 1) { $SILENT = 1; }
elsif($CONF{'mode'} == 2) { $QUIET = 1; }
elsif($CONF{'mode'} == 3) { $BACKGROUND = 1; }
elsif($CONF{'mode'} == 4) { $DEBUG = 1; }
# ____ Various Set up _________________________________________________________
select((select(STDOUT), $| = 1)[0]);
$, = "\n";
srand();

print <<EOM  unless($SILENT);

-----------------------------
  i c e d j - Version 0.9.14
-----------------------------

IceDJ comes with NO WARRANTY, to the extent permitted by law.
You may redistribute copies of IceDJ under the terms of the
GNU General Public License.
For more information about these matters, see the file named COPYING.

-----------------------------

EOM

# ____ go to the background? __________________________________________________
if($BACKGROUND) {
	my($PID);
	if ($PID = fork()) { # Parent
		print "Going to the background at PID $PID\n\n";
		exit;
		}
	elsif (defined($PID)) { ## child
		#background session, carry on quietly
		$SILENT = 1;
		close(STDOUT);
		}
	else {
		dieLog("Couldn't spawn background session");
		}
	}

# ____ Check Config ___________________________________________________________
my($abort);
my(@required) = qw(server logname port public bitrate password name genre url playlist mp3root replaymin maxrequests);
foreach(@required) {
	$abort .= "$_\n" if($CONF{$_}eq"");
	}
if($abort) { 
	print "The following required fields were not defined in $CONF{'conf'}:\n$abort";
	exit;
	}


# ____ Set up Constants _______________________________________________________
my($SLEEPTIME) = 10;
my($UPDATETIME) = 60;
my($PIDFILE) = "$CONF{'datadir'}icedj-$CONF{'logname'}.pid";
my($HUPFILE) = "$CONF{'datadir'}icedj-$CONF{'logname'}.hup";
my($AVAILABLE) = "$CONF{'datadir'}avail-$CONF{'logname'}";
my($USED) = "$CONF{'datadir'}used-$CONF{'logname'}";
my($LOCKFILE) = "$CONF{'datadir'}icedj-$CONF{'logname'}.LCK";
my($LOGFILE) = "$CONF{'datadir'}icedj-$CONF{'logname'}.log";
my($REQUESTLOG) = "$CONF{'datadir'}request-$CONF{'logname'}.log";
my(@SONGINFO) = ('request','path','artist','title');
push(@SONGINFO, split(/\s+/,$CONF{'songinfo'}));
my($EXTENSIONS) = "(\.txt|\.html|\.tmpl|\.err)";
my(@WEEK) = qw(Sun Mon Tue Wed Thu Fri Sat);
my(@MONTH) = qw(EEK Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
my($S_DAY,$S_MONTH,$S_YEAR,$S_WEEK) = (localtime(time))[3..6];

my(@BITRATES) = split(/\s+/,$CONF{'bitrate'});
my(@SOURCES) = split(/\s+/,$CONF{'source'});
my(@SERVERS) = split(/\s+/,$CONF{'server'});
my(@PUBLIC) = split(/\s+/,$CONF{'public'});
my(@PASSWORDS) = split(/\s+/,$CONF{'password'});
my(@PORTS) = split(/\s+/,$CONF{'port'});
my(@SERVERSTAT)  = split(/\s+/,$CONF{'serverstat'});
my($shoutdatadir,$shoutpidfile,$shoutrc,$shoutflag,$shoutintplaylist,$shoutextplaylist);
my(@SHOUTDATADIR,@SHOUTPIDFILE,@SHOUTRC,@SHOUTFLAG,@SHOUTINTPLAYLIST,@SHOUTEXTPLAYLIST,@SHOUTPID);
foreach(@BITRATES) {
	$shoutdatadir = "$CONF{'datadir'}shout-$_";
	mkdir($shoutdatadir, 0775) unless(-d $shoutdatadir);
	push(@SHOUTDATADIR, $shoutdatadir);
	push(@SHOUTPIDFILE , "$shoutdatadir/shout.pid");
	push(@SHOUTRC, "$shoutdatadir/shoutrc");
	push(@SHOUTFLAG, "$shoutdatadir/shout.flag");
	push(@SHOUTINTPLAYLIST, "$shoutdatadir/shout.playlist");
	push(@SHOUTEXTPLAYLIST, "$shoutdatadir/shout.startlist");
	}
my(@MP3SPYSERVERS,@MP3SPYQUERY,$MP3SPYURL);
my($x) = 0;
if($CONF{'propagateURL'}) { $MP3SPYURL = "/$CONF{'propagateURL'}?"; }
else { $MP3SPYURL = '/mp3spy/song.php3?'; }
foreach(@SERVERS) {
	my($serverip,$broadcastport);
	if(/\d+\.\d+\.\d+\.\d+/) { $serverip = $_; }
	else {
		my(@serverip) = `host $_`;
		$serverip = join('',@serverip);
		unless(($serverip) = $serverip =~ /has address (\d+\.\d+\.\d+\.\d+)/) {
			dieLog("unable to resolve IP for $_");
			}
		}
	$broadcastport = $PORTS[$x]-1;
	push(@MP3SPYQUERY, "ip=${serverip}:${broadcastport}&title=");
	if($CONF{'propagateURL'}) {
		push(@MP3SPYSERVERS,$_);
		}
	else {
		push(@MP3SPYSERVERS,'update.mp3spy.com');
		}
	$x++;
	}



# ____ Check and report our PID _______________________________________________
# the PIDFILE is useful for quick finding and killing of icedj

if (-e $PIDFILE) {
	open(PIDFILE, $PIDFILE);
	my($pid) = <PIDFILE>;
	close(PIDFILE);
	chomp $pid;
	if(kill 0 => $pid) {
		print "icedj already streaming to $CONF{'logname'}\n";
		exit;
		}
	}

open(PIDFILE, ">$PIDFILE");
print PIDFILE "$$\n";
close(PIDFILE);
dieLog("Couldn't create PIDFILE $PIDFILE") unless (-e $PIDFILE);

# ____ Init ______________________________
initPlaylist();
initSchedule();
# ___ kill shout and give it time to die ___
foreach $shoutpidfile (@SHOUTPIDFILE) {
debug("checking for $shoutpidfile");
	if(-e $shoutpidfile) {
		#there was a copy of shout hanging around, kill it
		open(PIDFILE, $shoutpidfile);
		my($pid) = <PIDFILE>;
		close(PIDFILE);
		chomp $pid;
		if(kill 0 => $pid) {
			if(kill 9 => $pid) {
				writeLog("old streamer at PID $pid killed...");
				unlink($shoutpidfile);
				}
			else { dieLog("old streamer at PID $pid wouldn't die: $!"); }
			}
		else {
			writeLog("pidfile for deceased $pid removed...");
			unlink($shoutpidfile);
			}
		sleep(1);
		}
	}

# ____ Start Play loop ________________________________________________________
my($t,$f_shout);
my($firsttime) = 1;
$t = time;
while (1) {
	$f_shout = 0;
	if ($RESET>0||(-e $HUPFILE)) { #reseting iceDJ
		unlink($HUPFILE);
		resetStreamers();
		}

	KILLER() unless (-e $PIDFILE);	#PIDFILE is gone, time to commit seppuku

	foreach $shoutpidfile (@SHOUTPIDFILE) {
		$f_shout = 1 unless(-e $shoutpidfile);
		}
	if($f_shout) {
		%SONG = getNextSongInfo();
		initStreamers(%SONG);
		}

	prepareShow();

	if(-e $SHOUTFLAG[0]) { #got a flag, time to update song info
		generateHistory(%SONG);
		generatePlaylist($SONG{'uniqueID'}) if($CURRENT>=0);
		writeLog( "playing  $SONG{'title'} by $SONG{'artist'}" );
		updateMP3spy(%SONG) if($CONF{'updateMP3spy'});
		runDJscript(%SONG) if($CONF{'djscript'});
		%SONG = getNextSongInfo();
		unlink($SHOUTFLAG[0]);
		writeShoutPlaylists(%SONG);
		}

	if((time - $t) > $UPDATETIME||$firsttime) {
		$firsttime = 0;
		updateWeb();
		$t = time;
		}

	sleep($SLEEPTIME);
	}

exit(0);

##-----------------------------------------------------------------------------
#     Subs
##-----------------------------------------------------------------------------


# ____ initPlaylist ___________________________________________________________
sub initPlaylist {
	my($file,$temp,$i,$tempblock,$key,$r_song);
	writeLog( "Initializing Playlist..." );
	@REQUESTLIST = ();
	open(PL, $CONF{'datadir'}.$CONF{'playlist'});
	my(@temp) = <PL>;
	close(PL);
	chomp(@temp);
	# ___ build hash of all songs ___
	foreach $temp (@temp) {
		$temp = "0\t$temp";
		my(@songinfo) = split('\t',$temp);
		chomp @songinfo;
		my(%song);
		for($i=0;$i<=$#SONGINFO;$i++) {
			$song{$SONGINFO[$i]} = ($songinfo[$i])?$songinfo[$i]:"";
			}
		$song{'uniqueID'} = lc($song{'path'});
		$song{'uniqueID'}  =~ s/(\s|\W)//g;
		$song{'conf'} = $CONF{'conf'};
		push(@REQUESTLIST,\%song);
		}

	@TRACKLIST = @REQUESTLIST;
	my(@available);
	if($CONF{'shuffle'}) { @available = shuffle(@temp); }
	else { @available = reverse(@temp); }
	lockfiles();
	unlink($AVAILABLE, $USED);
	open(AVAIL, ">$AVAILABLE");
	print AVAIL @available;
	print AVAIL "\n";
	close(AVAIL);
	#init used list, nothing in there for now
	open(USED, ">$USED");
	print USED "";
	close(USED);
	unlockfiles();
	}

# ____ initSchedule __________________________________________________________
sub initSchedule {
	my(@temptab);
	open(SC, $CONF{'datadir'}.$CONF{'schedule'});
	@temptab = <SC>;
	close(SC);
	chomp @temptab;
	foreach(@temptab) { if($_) { push(@CRONTAB,$_); } }
	if(@CRONTAB) {
		writeLog("initializing schedule for $S_MONTH/$S_DAY/$S_YEAR...");
		@SCHEDULE = createSchedule(@CRONTAB);
		$SCHEDULE = 1;
		}
	else {
		writeLog("schedule file $CONF{'datadir'}.$CONF{'schedule'} was empty or did not exist");
		$SCHEDULE = 0;
		}
	}

# ____ initStreamers __________________________________________________________
sub initStreamers {
	my(%song) = @_;
	my($i);
	@SHOUTPID = ();
	writeLog("Spawning Shout...");	
	for($i;$i<=$#SHOUTRC;$i++) {
		# ___ write shout rcfile ___
		open(RC,">$SHOUTRC[$i]");
		print RC <<EOM;
autocorrection 0
autodetection 0
truncate 1
loop 1
shuffle 0
public $PUBLIC[$i]
port $PORTS[$i]
password $PASSWORDS[$i]
genre $CONF{'genre'}
base_dir $SHOUTDATADIR[$i]
url $CONF{'url'}
name $CONF{'name'}
playlist $SHOUTEXTPLAYLIST[$i]
EOM
		close(RC);

		# ___ write shout initial playlist ___
		open(PL,">$SHOUTEXTPLAYLIST[$i]");
		my($bitrate) = $BITRATES[$i]*1000;
		my($function) = ($i==0)?"touch $SHOUTFLAG[$i]":'';
		my($path);
		if($CONF{'sourceID'} eq 'pre') { $path = "$CONF{'mp3root'}$SOURCES[$i]$song{'path'}"; }
		else { $path = "$CONF{'mp3root'}$song{'path'}$SOURCES[$i]"; }
		my($line) = "$path:$function:$bitrate\n";
		print PL $line;
		close(PL);

		# ___ start shout ___
		my($pid1,$pid2,@args);
		if ($pid1 = fork()) { # Parent
			waitpid($pid1,0);
			debug(" spawn shout helper ($pid1) returned");
			}
		elsif (defined($pid1)) { ## child
			if ($pid2 = fork())    { ## still child
				exit(1);
				}
			elsif (defined($pid2)) { ## grandchild
				@args = ($CONF{'streamer'},$SERVERS[$i],'-C',$SHOUTRC[$i]);
				push(@args, '-d') if($i==0);
				writeLog("starting $BITRATES[$i]Kbps stream...");
				close(STDOUT) if($QUIET);
				exec(@args)||dieLog("couldn't spawn @args !");
				}
			else { ## child; 2nd fork failed
				dieLog("spawning $BITRATES[$i]Kbps stream failed");
				exit(1);
				}
			}
		else {              ## parent - fork failed
			dieLog("Spawning of Shout $SHOUTRC[$i] failed.");
			}
		}
	
	sleep(5);
	foreach(@SHOUTPIDFILE) {
		# ___ get shout's pid ___
		open(PIDFILE, $_);
		my($pid) = <PIDFILE>;
		close(PIDFILE);
		chomp $pid;
		debug("got a streamer at $pid");
		push(@SHOUTPID,$pid);
		}
	}

# ____ resetStreamer __________________________________________________________
#we've received a HUP signal and will re-initialize
sub resetStreamer {
	my($f_conf) = 0;

	writeLog( "Received HUP signal..." );
	
	open(CONF, $CONF);
	while (<CONF>) {
		chomp;
		if(/^#/) { next; } #comment, ignore line
		/(\S+)\s+(.*)$/;
		if($CONF{$1} ne $2) { $f_conf = 1; }
		$CONF{$1} = $2;
		}
	close(CONF);
	
#	if($f_conf) {
#		writeLog( "Resetting Streamers..." );
#		resetShout();
#		}
	
	writeLog( "Re-initializing..." );
	initPlaylist();
	initSchedule();
	$RESET = 0;
	}

# ____ writeShoutPlaylists ___________________________________________________________
sub writeShoutPlaylists {
	my(%song) = @_;
	my($i);
	for($i;$i<=$#SHOUTRC;$i++) {
		open(PL,">$SHOUTINTPLAYLIST[$i]");
		my($bitrate) = $BITRATES[$i]*1000;
		my($function) = ($i==0)?"touch $SHOUTFLAG[$i]":'';
		my($path);
		if($CONF{'sourceID'} eq 'pre') { $path = "$CONF{'mp3root'}$SOURCES[$i]$song{'path'}"; }
		else { $path = "$CONF{'mp3root'}$song{'path'}$SOURCES[$i]"; }
		my($line) = "$path:$function:$bitrate\n";
		print PL $line;
		close(PL);
		}
	}

# ____ generatePlaylist ________________________________________________________
sub generatePlaylist {
	my($uniqueID) = @_;
	debug("generating playlist with unique ID $uniqueID");
	my($file,$temp,$tempblock,$key,$r_song,$oddblock,$evenblock,$current);
	my(@files) = split(/\s+/,$CONF{'playlistpage'});
	foreach $file (@files) {
		debug("processing template $file");
		open(TEMPLATE, "$CONF{'webroot'}$file.tmpl");
		$current = 0;
		my(@template) = <TEMPLATE>;
		close(TEMPLATE);
		my($template) = join('',@template);
		my($header,$dummy1,$block,$dummy2,$footer) = $template =~ m#((\n|.)*)<!BLOCK>((\n|.)*)</!BLOCK>((\n|.)*)#;
		if($block =~ /<!CURRENT>/) { $current = $CURRENT = 1; debug("found a CURRENT tag");} 

		open(PLW, ">$CONF{'webroot'}$file.html");
		print PLW "$header\n";
		$oddblock = $evenblock = $block;
		$oddblock =~ s#<!EVEN>(\n|.)*?</!EVEN>##gi;
		$oddblock =~ s#<!ODD>((\n|.)*?)</!ODD>#$1#gi;
		$evenblock =~ s#<!ODD>(\n|.)*?</!ODD>##gi;
		$evenblock =~ s#<!EVEN>((\n|.)*?)</!EVEN>#$1#gi;
		my($i) = 0;
		foreach $r_song (@TRACKLIST) {
			$i++;
			my(%song) = %$r_song;
			if($i % 2) { $tempblock = $oddblock; }
			else { $tempblock = $evenblock; }
			foreach $key (keys %song) { 
				if($key =~ /^l_/) {
					$tempblock =~ s/<!$key>/<A HREF="$song{$key}">/gi
						unless($song{$key} eq "");
					}
				else {
					$tempblock =~ s/<!$key>/$song{$key}/gi;
					}
				}
			if($SCHEDULE=1&&$uniqueID ne $song{'uniqueID'}) {
				$tempblock =~ s#<!CURRENT>((\n|.)*?)</!CURRENT>##gi;
				}
			print PLW "$tempblock\n";
			}
		print PLW "$footer\n";
		close(PLW);
		}
	if(!$CURRENT) { $CURRENT = -1; }
	}

# ____ generateHistory ________________________________________________________
sub generateHistory {
	debug("generating history");
	my(%temp) = @_;
	my($file,$key,$oddblock,$evenblock, %info);
	foreach $key (keys %temp) { $info{"cur_$key"} = $temp{$key}; }
	%info = (%info,%SHOW) if($SHOW&&$temp{'show'});
	$info{'history'} = ($SHOW&&$temp{'show'})?"Show Playlist: ":"Recent: ";
	my(@TMPHISTORY) = ($SHOW&&$temp{'show'})?@SHOWHISTORY:@HISTORY;

	my(@files) = split(/\s+/,$CONF{'history'});
	foreach $file (@files) {
		open(TEMPLATE, "$CONF{'webroot'}$file.tmpl");
		my(@template) = <TEMPLATE>;
		close(TEMPLATE);
		my($template) = join('',@template);
		foreach $key (keys %info) { #$template =~ s/<!$key>/$info{$key}/gi; }
			if($key =~ /^cur_l_/) {
				$template =~ s/<!$key>/<A HREF="$info{$key}">/gi
					unless($info{$key} eq "");
				}
			else {
				$template =~s/<!$key>/$info{$key}/gi;
				}
			}
		my($header,$dummy1,$block,$dummy2,$footer) = $template =~ m#((\n|.)*)<!BLOCK>((\n|.)*)</!BLOCK>((\n|.)*)#;

		$oddblock = $evenblock = $block;
		$oddblock =~ s#<!EVEN>(\n|.)*?</!EVEN>##gi;
		$oddblock =~ s#<!ODD>((\n|.)*?)</!ODD>#$1#gi;
		$evenblock =~ s#<!ODD>(\n|.)*?</!ODD>##gi;
		$evenblock =~ s#<!EVEN>((\n|.)*?)</!EVEN>#$1#gi;
		open(HISTORY, ">$CONF{'webroot'}$file.html");
		print HISTORY "$header\n";
		my($i) = 0;
		foreach (@TMPHISTORY) {
			my(%song) = %$_;
			my($tempblock);
			$i++;
			if($i % 2) { $tempblock = $oddblock; }
			else { $tempblock = $evenblock; }
			if($SHOW&&$song{'counter'}&&$song{'show'}) {
				$song{'title'} = "$song{'counter'}. $song{'title'}";
				}
			foreach $key (keys %song) {
				if($key =~ /^l_/) {
					$tempblock =~ s/<!$key>/<A HREF="$song{$key}">/gi
						unless($song{$key} eq "");
					}
				else {
					$tempblock =~ s/<!$key>/$song{$key}/gi;
					}
				}
			print HISTORY "$tempblock\n";
			}
		print HISTORY "$footer\n";
		close(HISTORY);
		}
	$temp{'title'} =~ s/^(\d+)\. //i if($SHOW);
	if($#HISTORY>=10) { pop(@HISTORY); }
	unshift(@HISTORY, \%temp);
	}

# ____ checkSchedule ____________________________________________________________
sub checkSchedule {
	my($i,$show,@show,$time,$t);
	unless($SCHEDULE) { return; }
	($time,@show) = split(/\s+/,$SCHEDULE[0]);
	$show = join(' ',@show);
	my($cs,$cm,$ch,$cD,$cM,$cY,$cW) = localtime($time);
	$t = $time-time;
	$t = $t*-1 if($t<0);
	debug("looking at show $show with scheduled time $ch:$cm $cM/$cD/$cY");
	if($t < ($SLEEPTIME*2)) {
		debug("prepare show now");
		if($SHOW) { 
			debug("conflicting show, show queued");
			}
		shift(@SCHEDULE);
		unless(@SCHEDULE) {
			my($day) = timelocal(0,0,0,$S_DAY,$S_MONTH,$S_YEAR) + (60*60*24);
			($S_DAY,$S_MONTH,$S_YEAR,$S_WEEK) = (localtime($day))[3..6];
			writeLog("initializing schedule for $S_MONTH/$S_DAY/$S_YEAR...");
			@SCHEDULE = createSchedule(@CRONTAB);
			}
		foreach(@SCHEDULE) { debug("  show $_ left"); }
		unshift(@SHOWQUEUE,$show);
		return;
		}
	return;
	}

# ____ createSchedule(@crontab) _______________________________________________
# RETURNS: @fulltime-schedule
# CRON to fulltime converter (one day at a time)
#        minute (0-59),
#        hour (0-23),
#        day of the month (1-31),
#        month of the year (1-12),
#        day of the week (0-6 with 0=Sunday).
sub createSchedule {
	my(@crontab) = @_;
	my(@schedule,$time);
	foreach (@crontab) {
		my(@m,@h,@D,@M,@W,@command);
		($m[0],$h[0],$D[0],$M[0],$W[0],@command) = split(/\s+/);
		my($command) = join(' ',@command);
		(@m[1..2],@h[1..2],@D[1..2],@M[1..2],@W[1..2]) = (0,59,0,23,1,31,1,12,0,6);
		buildSchedItem(\@m,\@h,\@D,\@M,\@W);
		my($h,$m);
		foreach (@W) {
			if($_==$S_WEEK) {
				foreach (@M) {
					if($_==$S_MONTH) {
						foreach (@D) {
							if($_==$S_DAY) {
								foreach $h (@h) {
									foreach $m (@m) {
										$time = timelocal(0,$m,$h,$S_DAY,$S_MONTH,$S_YEAR);
										if(time>$time) {
											debug("$command \@ $m,$h,$S_DAY,$S_MONTH,$S_YEAR already past, not scheduled");
											}
										else {
											debug("scheduling $time $command => 0,$m,$h,$S_DAY,$S_MONTH,$S_YEAR");
											push(@schedule, "$time $command");
											}
										}
									}
								last;
								}
							}
						last;
						}
					}
				last;
				}
			}
		
		}
	if(@schedule) {
		@schedule = sort ascending @schedule;
		}
	else {
		my($day) = timelocal(0,0,0,$S_DAY,$S_MONTH,$S_YEAR) + (60*60*24);
		($S_DAY,$S_MONTH,$S_YEAR,$S_WEEK) = (localtime($day))[3..6];
		writeLog("initializing schedule for $S_MONTH/$S_DAY/$S_YEAR...");
		@schedule = createSchedule(@CRONTAB);
		}

	return(@schedule)
	}


# ____ buildSchedItem(\@r_item) _________________________________________________________
sub buildSchedItem {
	my(@r_item) = @_;
	my($r_item,$i);
	foreach $r_item (@r_item) {
		if($$r_item[0] =~ /,/) {
			@$r_item = split(',',$$r_item[0]);
			}
		elsif($$r_item[0] =~ /-/) {
			my($start,$end) = split('-',$$r_item[0]);
			@$r_item = ();
			for($i=$start;$i<=$end;$i++) { 
				push(@$r_item,$i);
				}
			}
		elsif($$r_item[0] eq '*') {
			my($start,$end) = (@{$r_item})[1..2];
			@$r_item = ();
			for($i=$start;$i<=$end;$i++) { 
				push(@$r_item,$i);
				}
			}
		else {
			@$r_item = $$r_item[0];
			}
		}
	}

# ____ prepareShow ____________________________________________________________
sub prepareShow {
	checkSchedule();
	return if($SHOW);
	my($show) = pop(@SHOWQUEUE);
	return unless($show);

	$SHOW = 1; #got a show ready, everyone
	@SHOWPLAYLIST = ();
	@SHOWHISTORY = ();

	if($show eq 'topten') { #topten is a built in show
		unless(@TOPTEN) {
			writeLog('ERROR: no @TOPTEN for topten show');
			$SHOW = 0;
			return;
			}
		my($r);
		debug("showinfo: $CONF{'datadir'}$CONF{'topten'}");
		open(SHOW,$CONF{'datadir'}.$CONF{'topten'});
		my(@showinfo) = <SHOW>;
		close(SHOW);
		$SHOW{'showname'} = shift(@showinfo);
		$SHOW{'showtime'} = shift(@showinfo);
		$SHOW{'trackinfo'} = shift(@showinfo);
		debug("preparing $SHOW{'showname'}");
		foreach $r (@TOPTEN) {
			my(%song) = %$r; #make sure we deal with a copy not the reference
			$song{'show'} = 1;
			push(@SHOWHISTORY, \%song);
			push(@SHOWPLAYLIST, \%song);
			debug("  adding $song{'path'}");
			}
		}
	else { #get the show playlist
		open(SHOW,$CONF{'datadir'}.$show);
		my(@playlist) = <SHOW>;
		close(SHOW);
		chomp(@playlist);

		$SHOW{'showname'} = shift(@playlist);
		$SHOW{'showtime'} = shift(@playlist);
		$SHOW{'trackinfo'} = shift(@playlist);
		my($i,$j);
		for($i=0;$i<=$#playlist;$i++) {
			my(@song,%song);
			my(@songinfo) = split('\t',$playlist[$i]);
			for($j=0;$j<=$#SONGINFO;$j++) {
				push(@song,$SONGINFO[$j],$songinfo[$j]);
				}
			%song = @song;
			$song{'show'} = 1;
			$SHOWHISTORY[$i] = \%song;
			$SHOWPLAYLIST[$i] = \%song;
			}
		}
	
	#flag the filesystem that we got a show
	writeLog("preparing to broadcast $SHOW{'showname'}...");
	open(SHOW, ">$CONF{'datadir'}$CONF{'logname'}.show");
	print SHOW "$SHOW{'showname'}\n";
	close(SHOW);

	$SHOW{'showmsg'} = <<"EOM";
<CENTER>
Now broadcasting:<BR>
<B>$SHOW{'showname'}</B>
</CENTER>
<P>$SHOW{'showtime'}
<P>
EOM
	}

# ____ getNextSongInfo ________________________________________________________
# RETURNS: %songInfo
sub getNextSongInfo {
	debug("getting next song");
	my($songinfo,%song);
	my($history) = "_";
	my($notDone) = 1;
	foreach(@HISTORY) { $history .= ${$_}{'uniqueID'}."_"; }
	if(!@SHOWPLAYLIST&&$SHOW) { #done with current show
		writeLog("end of $SHOW{'showname'}...");
		unlink("$CONF{'datadir'}$CONF{'logname'}.show");
		$SHOW = 0;
		}
	prepareShow();
	if($SHOW) {
		%song = %{pop(@SHOWPLAYLIST)};
		debug("getting $song{'title'} from showplaylist");
		}
	else {
		lockfiles();
		open(AVAIL, "$AVAILABLE");
		my(@available) = <AVAIL>;
		my(@extra);
		close(AVAIL);
		chomp(@available);
		while($notDone) {
			my($i,$request,@songinfo);
			if($#available < 1) { #available list is down to the last one
				writeLog("reshuffling list...");
				open(USED, "$USED");
				my(@used) = <USED>;
				close(USED);
				unlink($USED);
				chomp(@used);
				@available = shuffle(@available,@used);
				}
			$songinfo = pop(@available);
			@songinfo = split('\t',$songinfo);
			%song = ();
			for($i=0;$i<=$#SONGINFO;$i++) { 
				debug("->\$song{$SONGINFO[$i]} = $songinfo[$i]");
				$song{$SONGINFO[$i]} = $songinfo[$i];
				}
			$request = int(shift(@songinfo));
			$song{'uniqueID'} = lc($song{'path'});
			$song{'uniqueID'}  =~ s/(\s|\W)//g;
			foreach (keys %song) { debug(">>$_:$song{$_}"); }
			if($request||$history !~ /_${song{'uniqueID'}}_/) { $notDone = 0; }
			else {
				push(@extra,$songinfo);
				debug("  played too recently, putting back in the queue");
				}
			}
		unshift(@available,@extra);
		$songinfo =~ s/^\d\t/0\t/;
		open(USED, ">>$USED");
		print USED "$songinfo\n";
		close(USED);
		open(AVAILTMP, ">$AVAILABLE.tmp");
		print AVAILTMP @available;
		print AVAILTMP "\n";
		close(AVAILTMP);
		unlink($AVAILABLE);
		rename("$AVAILABLE.tmp",$AVAILABLE);
		unlockfiles();
		}
	#make sure the file actually exists
	my(@TMPSOURCES) = @SOURCES;
	unless(@TMPSOURCES) { push(@TMPSOURCES,""); }
	foreach (@TMPSOURCES) {
		my($path);
		if($CONF{'sourceID'} eq 'pre') { $path = "$CONF{'mp3root'}$_$song{'path'}"; }
		else { $path = "$CONF{'mp3root'}$song{'path'}$_"; }
		unless(-e $path) {
			writeLog("ERROR: $path does not exist, skipping");
			return( getNextSongInfo() );
			}
		}
	return(%song);
	}

# ____ updateMP3spy ___________________________________________________________
sub updateMP3spy {
	my(%song) = @_;
	debug("updateMP3spy...");
	my ($pid1, $pid2, @args, $i, $server);
	my($trackinfo,$retry,$key,$good,$val);
	if($SHOW{'trackinfo'}&&$SHOW) {
		$trackinfo = $SHOW{'trackinfo'};
		foreach $key (keys %song) { $trackinfo =~ s/<!$key>/$song{$key}/gi; }
		}
	elsif($CONF{'trackinfo'}) {
		$trackinfo = $CONF{'trackinfo'};
		foreach $key (keys %song) { $trackinfo =~ s/<!$key>/$song{$key}/gi; }
		}
	else {
		$trackinfo = "$song{'artist'} - $song{'title'}";
		}
	$trackinfo =~ s/([\W])/"%".unpack('H', $1).unpack('h', $1)/eg;
	$trackinfo =~ tr/ /+/;
	if ($pid1 = fork()) { # Parent
		waitpid($pid1,0);
		debug("updateMP3spy spawn helper ($pid1) returned");
		}
	elsif (defined($pid1)) { ## child
		if ($pid2 = fork())    { ## still child
			exit(1)
			}
		elsif (defined($pid2)) { ## grandchild
			#child does the updating
			foreach $server (@MP3SPYSERVERS) {
				unless($PUBLIC[$i]) {
					next;
					}
				while($retry < 5) {
					my $sock = IO::Socket::INET->new(	Proto => "tcp",
														PeerAddr => "$server",
														PeerPort => 80,
														Timeout => 30
														);
					if(!$sock) {
						$retry++;
						debug("  no socket, try again ($retry)");
						next;
						}
					$sock->autoflush(1);

					print $sock "GET ${MP3SPYURL}$MP3SPYQUERY[$i]${trackinfo} HTTP\1.0\n\n";

					while($val = <$sock>) {
						if ($val =~ /^OK$/) { $good = 1; }
						elsif($val =~ /^NO$/) { $good = 0; }
						}
					close $sock;
					if($good) { last; }
					$retry++;
					}
				if($retry >= 5) { writeLog("MP3spy Update Failed for $server"); }
				$i++;
				}
			debug("updateMP3SPUupdater exiting");
			exit;
			}
		else { ## child; 2nd fork failed
			writeLog("couldn't spawn updateMP3SPUupdate");
			exit(1);
			}
		}
	else {              ## parent - fork failed
		writeLog("couldn't spawn updateMP3spy()");
		}
	}

# ____ runDJscript ____________________________________________________________
sub runDJscript {
	my(%song) = @_;
	debug("runDJscript...");
	my ($pid1, $pid2, @args, $i,$j);
	if ($pid1 = fork()) { # Parent
		waitpid($pid1,0);
		debug("runDJscript spawn helper ($pid1) returned");
		}
	elsif (defined($pid1)) { ## child
		if ($pid2 = fork())    { ## still child
			exit(1)
			}
		elsif (defined($pid2)) { ## grandchild
			@args = split(/\s+/,$CONF{'djscript'});
			for($j=0;$j<=$#args;$j++) {
				if($args[$j] =~/<SONGINFO>/i) {
					my(@args2);
					for($i=1;$i<=$#SONGINFO;$i++) {
						push(@args2,$song{$SONGINFO[$i]});
						}
					splice(@args,$j,1,@args2);
					}
				}
			exec(@args)||writeLog("Unable to spawn @args");
			debug("runDJscript spawner exiting");
			exit;
			}
		else { ## child; 2nd fork failed
			writeLog("couldn't spawn runDJscript spawner");
			exit(1);
			}
		}
	else {              ## parent - fork failed
		writeLog("Execution of djscript failed.");
		}
	}

# ____ updateWeb ______________________________________________________________
sub updateWeb {
	my(@time) = getTime();
	writeLog("Updating Webpages...");
	debug("calling topten");
	topten(@time); #has to be done before fork, because of shared variables
	debug("calling updateServerstats");
	updateServerStat(@time);
	}

##-----------------------------------------------------------------------------
#     updateWeb Subs
##-----------------------------------------------------------------------------

# ____ topten _________________________________________________________________
sub topten {
	my(@time) = @_;
	my($M,$D,$Y,$h,$m,$s,$ip,$uniqueID,$songinfo,$line,$item,$key,$r_song);
	my($i,$r,$file,$template,$header,$footer,$dummy,$block,$oddblock,$evenblock);
	my(@songs, @template, %song, %info, %requests);
	
	$info{'fulltime'} = "$time[2]:$time[1]:$time[0] $time[4]/$time[3]/$time[5]";

	# ___ Parse Request Log ___
	open(LOG, "$REQUESTLOG");
	while(<LOG>) {
		($M,$D,$Y,$h,$m,$s,$ip,$uniqueID) = m#^<(\d+)/(\d+)/(\d+)\@(\d+):(\d+):(\d+)>\s+(.*?)\s+(.*)$#;
		if(time - (timelocal($s,$m,$h, $D,($M-1),$Y))<1209600) {
			#it happened in the past week, so we care
			$requests{lc($uniqueID)}++;
			}
		}

	# ___ Sort Requests ___
	foreach $r_song (@REQUESTLIST) {
		${$r_song}{'request'} = $requests{${$r_song}{'uniqueID'}};
		}
	my $lines = @REQUESTLIST;
	@REQUESTLIST = sort request_descending @REQUESTLIST;
	$lines = @REQUESTLIST;
	
	$i = 1;
	@TOPTEN = ();
	foreach $r_song (@REQUESTLIST) {
		next unless(${$r_song}{'request'});
		${$r_song}{'counter'} = $i;
		if($i<=10) {
			push(@TOPTEN, $r_song);
			foreach $key (keys %{$r_song}) {
				$info{"${i}_$key"} = $$r_song{$key};
				}
			}
		$i++;
		}

	# ___ Write Top Pages ___
	my(@files) = split(/\s+/,$CONF{'toppage'});
	foreach $file (@files) {
		my($fullblock);
		open(TEMPLATE, "$CONF{'webroot'}$file.tmpl");
		@template = <TEMPLATE>;
		close(TEMPLATE);
		$template = join('',@template);

		($header,$dummy,$block,$dummy,$footer) = $template =~ m#((\n|.)*)<!BLOCK>((\n|.)*)</!BLOCK>((\n|.)*)#;
		$oddblock = $evenblock = $block;
		$oddblock =~ s#<!EVEN>(\n|.)*?</!EVEN>##gi;
		$oddblock =~ s#<!ODD>((\n|.)*?)</!ODD>#$1#gi;
		$evenblock =~ s#<!ODD>(\n|.)*?</!ODD>##gi;
		$evenblock =~ s#<!EVEN>((\n|.)*?)</!EVEN>#$1#gi;

		open(TOP, ">$CONF{'webroot'}$file.html");
		
		$i = 0;
		foreach (@REQUESTLIST) {
			%song = %$_;
			next unless($song{'request'});
			my($tempblock);
			$i++;
			if($i % 2) { $tempblock = $oddblock; }
			else { $tempblock = $evenblock; }
			foreach $key (keys %song) {
				if($key =~ /^l_/) {
					$tempblock =~ s/<!$key>/<A HREF="$song{$key}">/gi
						unless($song{$key} eq "");
					}
				else {
					$tempblock =~ s/<!$key>/$song{$key}/gi;
					}
				}
			$fullblock .= "$tempblock\n";
			}

		foreach $key (keys %info) { 
			$header =~ s/<!$key>/$info{$key}/gi; 
			$footer =~ s/<!$key>/$info{$key}/gi; 
			}
		print TOP "$header\n";
		print TOP "$fullblock\n";
		print TOP "$footer\n";
		close(TOP);
		}
	}

# ____ updateServerStat _______________________________________________________
sub updateServerStat {
	my($s,$m,$h,$D,$M,$Y,$w) = @_;
	my($file,$xfile,$uptime,$listeners,$i,$a,$b,$stats,$dummy,$before,$block);
	my($serverstat,$after,$key,$subhash,$next);
	my($tempblock, $blockline);
	my(@files,@average,@sourcelist,@listenerlist,%stats,%temp);

	$i=1;
	# ___ get Server Stats ___
	foreach $serverstat (@SERVERSTAT) {
		my($f_stat,@stats);
		if($serverstat =~m#http://([^/]+)#) {
			debug("getting stats via http from $serverstat...");
			@stats = getHTTPStats($serverstat,$1);
			}
		elsif($CONF{'serverstat'} =~ m#^/#) {
			debug("getting stats from $serverstat...");
			open(STATS,$serverstat);
			@stats = <STATS>;
			close(STATS);
			}
		else {
			debug("getting stats from $CONF{'datadir'}$serverstat...");
			open(STATS,"$CONF{'datadir'}$serverstat");
			@stats = <STATS>;
			close(STATS);
			}
		chomp @stats;
		
		# ___ build Stats Hash ___
		foreach (@stats) {
			if($f_stat) { 
				($a,$b) = split(':');
				$a =~ s/\s+/_/g;
				$stats{"$a:$i"} = $b;
				}
			elsif(/Displaying server statistics:/) {
				$f_stat = 1;
				}
			elsif(/Listing sources\s+\((\d+)\):/) {
				$stats{"sources:$i"} = $1;
				}
			elsif(/Listing listeners\s+\((\d+)\):/) {
				$stats{"listeners:$i"} = $1;
				}
			elsif(/Icecast (.*) server uptime:\s+(\d*)\s+h\s+(\d*)\s+m\s+(\d*)\s+s/i) {
				$stats{"version:$i"} = $1;
				$stats{"updays:$i"} = int($2/24);
				$stats{"uphours:$i"} = $2%24;
				$stats{"upmins:$i"} = $3;
				$stats{"upsecs:$i"} = $4;
				}
			elsif(/^\d+\)\s+\[(.*):(.*)\] connected for (\d+) seconds, bytes transfered: (\d+) errors: (\d+)/) {
				my(%listener);
				$listener{'ip'} = $1;
				$listener{'host'} = $2;
				$listener{'seconds'} = $3;
				$listener{'bytes'} = $4;
				$listener{'errors'} = $5; 
				$listener{'server'} = $i; 
				push(@listenerlist,\%listener);
				}
			elsif(/^\d+\)\s+\[(.*):(.*)\] connected for (\d+) seconds, bytes transfered:\s+(\d+)(.*)$/) {
				my(%source);
				$source{'ip'} = $1;
				$source{'host'} = $2;
				$source{'seconds'} = $3;
				$source{'bytes'} = $4;
				$source{'streaming'} = $5; 
				$source{'server'} = $i; 
				push(@sourcelist,\%source);
				}
			}
		$stats{"bitrate:$i"} = $BITRATES[$i-1];
		$i++;
		}
	
	# ___ build Cummulative Stats Hash ___
	foreach(keys %stats) {
		($a,$b) = split(':');
		next if($b<=0);
		my($h1,$m1,$s1,$b1);
		my($h2,$m2,$s2,$b2);
		if(/average/i&&$b==1) { push(@average,$a); }
		if(($b1) = ($stats{$_} =~/(\d+)\s+bytes/)) {
			($b2) = ($temp{$a} =~/(\d+)\s+bytes/);
			$temp{$a} = ($b1 + $b2)." bytes";
			}
		elsif(($h1,$m1,$s1) = ($stats{$_} =~ /(\d+)\s+h\s+(\d+)\s+m\s+(\d+)\s+s/)) {
			($h2,$m2,$s2) = ($temp{$a} =~ /(\d+)\s+h\s+(\d+)\s+m\s+(\d+)\s+s/);
			if(($s2 += $s1) >= 60) { $s2 -= 60; $m2++; }
			if(($m2 += $m1) >= 60) { $m2 -= 60; $h2++; }
			$h2 += $h1;
			$temp{$a} = "$h2 h $m2 m $s2 s";
			}
		else {
			$temp{$a} += int($stats{$_})
			}
		}

	$i -= 1; #server count is inflated by one after first loop
	foreach (@average) {
		my($h,$m,$s);
		if($temp{$_} =~ s|(\d+)(\s+bytes)|int((int($1)/$i)).$2|ge) { next; }
		elsif(($h,$m,$s) = ($temp{$_} =~ /(\d+)\s+h\s+(\d+)\s+m\s+(\d+)\s+s/)) {
			$h = $h/$i;
			$m = $m/$i;
			$s = $s/$i;
			$m += int(60*($h - int($h)));
			$h = int($h)*1;
			$s = int($s) + int(60*($m - int($m)));
			$m = int($m)*1;	
			$temp{$_} = "$h h $m m $s s";
			}
		else {
			$temp{$_} = int(int($temp{$_})/$i);
			}
		}

	(%stats) = (%stats,%temp);
	
	# ___ define Time values ___
	$stats{'curHour'} = ($h>12)?$h-12:$h;
	$stats{'curHour'} = ($stats{'curHour'}==0)?12:$stats{'curHour'};
	$stats{'curHour24'} = $h;
	$stats{'curMinute'} = $m;
	$stats{'curSec'} = $s;
	$stats{'curAMPM'} = ($h>12)?'PM':'AM';
	$stats{'curDayName'} = $WEEK[$w];
	$stats{'curDay'} = $D;
	$stats{'curMonthName'} = $MONTH[$M];
	$stats{'curMonth'} = $M;
	$stats{'curYear'} = $Y;

	# ___ build Statsblock ___
	open(TEMPLATE, "$CONF{'datadir'}$CONF{'statsblock'}");
	my(@template) = <TEMPLATE>;
	close(TEMPLATE);
	($stats) = join('',@template);
	foreach $key (keys %stats) { $stats =~s/<!$key>/$stats{$key}/gi; }
	if(($before,$dummy,$block,$dummy,$after) = $stats =~ m#((\n|.)*)<!sourceblock>((\n|.)*)</!sourceblock>((\n|.)*)#i) {
		if(@sourcelist) {
			$tempblock = "";
			foreach $subhash (@sourcelist) {
				$blockline = $block;
				foreach $key (keys %{$subhash}) { $blockline =~s/<!$key>/${$subhash}{$key}/gi; }
				$tempblock .= $blockline;
				}
			$block = $tempblock;
			}
		else {
			$block = ""; 
			}
		$stats = $before.$block.$after;
		}
	if(($before,$dummy,$block,$dummy,$after) = $stats =~ m#((\n|.)*)<!listenerblock>((\n|.)*)</!listenerblock>((\n|.)*)#i) {
		if(@listenerlist) {
			$tempblock = "";
			foreach $subhash (@listenerlist) {
				$blockline = $block;
				foreach $key (keys %{$subhash}) { $blockline =~s/<!$key>/${$subhash}{$key}/gi; }
				$tempblock .= $blockline;
				}
			$block = $tempblock;
			}
		else {
			$block = ""; 
			}
		$stats = $before.$block.$after;
		}	

	# ___ get Files ___
	my(@temp) = `find $CONF{'webroot'}`;
	chomp(@temp);
	foreach  (@temp) { 
		if(/$EXTENSIONS$/) { push(@files,$_); }
		}

	my(@xfiles) = split(/\s+/,$CONF{'statsexclude'});
	my(@infiles) = split(/\s+/,$CONF{'statsinclude'});

	# ___ update Files ___
	foreach $file (@files) {
		$next = 0;
		foreach $xfile (@xfiles) {
			$next = 1 if($file =~ /^$CONF{'webroot'}$xfile($|\/)/);
			}
		next if($next);
		if($CONF{'statsinclude'}) {
			foreach $xfile (@infiles) {
				$next = 1 if($file =~ /^$CONF{'webroot'}$xfile($|\/)/);
				}
			next unless($next);
			}
		open(FH,$file);
		my(@in) = <FH>;
		close(FH);
		my($in,$line,$block,$position);
		foreach $line (@in) {
			if($line =~ /<!STATSBLOCK>/) {
				$position = 1;
				$block = $line;
				}
			elsif($position) {
				$block .= $line;
				if($line =~ m#</!STATSBLOCK>#) {
					$position = 0;
					$block =~ s#<!STATSBLOCK>((\n|.)*)</!STATSBLOCK>#<!STATSBLOCK>$stats</!STATSBLOCK>#;
					$in .= $block;
					}
				}
			else {
				$in .= $line;
				}
			}
		if($block) {
			open(FH,">$file");
			print FH $in;
			close(FH);
			}
		}
	
	}

# ____ getHTTPStats ___________________________________________________________
sub getHTTPStats {
	my($url,$server) = @_;
	my($sock,$line,@val);
	my($retry) = 0;
	while($retry < 5) {
		$sock = IO::Socket::INET->new(	Proto => "tcp",
										PeerAddr => $server,
										PeerPort => 80,
										Timeout => 30
									);
		if(!$sock) {
			$retry++;
			next;
			}
		$sock->autoflush(1);

		print $sock "GET $url HTTP\1.0\n\n";

		@val = <$sock>;
		close $sock;
		chomp(@val);

		if($val[0] =~ /HTTP/) {
			foreach $line (@val) {
				if($line =~ /Icecast .*: .*$/) { return(@val); }
				}
			}
		$retry++;
		sleep(10);
		}
	if($retry >= 5) {
		writeLog("Failed to get Serverstats");
		return;
		}
	}

##-----------------------------------------------------------------------------
#     Helper Subs
##-----------------------------------------------------------------------------

# ____ TimeBased Sort Helper __________________________________________________
sub time_desc {
	my($x) = split('\t',$a);
	my($y) = split('\t',$b);
	return(($y <=> $x));
	}

# ____Sort Routines ___________________________________________________________
sub ascending { ($a <=> $b) || ( $a cmp $b ); }
sub descending { ($b <=> $a) || ($a cmp $b); }
sub request_descending { 
	($$b{'request'} <=> $$a{'request'})
		||($$a{'request'} cmp $$b{'request'});
	}
sub title_descending { 
	($$b{'title'} <=> $$a{'title'})
		||($$a{'title'} cmp $$b{'title'});
	}
sub artist_descending { 
	($$b{'artist'} <=> $$a{'artist'})
		||($$a{'artist'} cmp $$b{'artist'});
	}
# ____ shuffle(@list) _________________________________________________________
# RETURNS: @list
# very simple randomizer for an array
sub shuffle {
	my(@in) = @_;
	my(@out, $i);
	rand(time);
	while(@in) { unshift(@out,splice(@in,int(rand($#in)),1)); }
	return(@out);
	}


# ____ lockfiles ______________________________________________________________
# locks the data files used for the playlist
# using the lockfile utility provided with procmail for file locking. If you do
# not wish to use it, or have some other file mechanism replace the lockfile
# calls here
sub lockfiles {
#uncomment the below line if you use lockfile() for locking
#	system("/opt/bin/lockfile -2 -r 20 -s 3 -l 30 $LOCKFILE") && dieLog('locking is screwed!');
	}

# ____ unlockfiles ____________________________________________________________
# unlocks the data files used for the playlist
# using the lockfile utility provided with procmail for file locking. If you do
# not wish to use it, or have some other file mechanism replace the lockfile
# calls here
sub unlockfiles {
#uncomment the below lines if you use lockfile() for locking
#	chmod(0666,$LOCKFILE);
#	unlink($LOCKFILE);
	#also want to reset the permissions on our files...
	chmod(0666,$AVAILABLE);
	chmod(0666,$USED);
	}

# ____ getTime _______________________________________________________________
sub getTime {
	my(@time) = localtime(time);
	$time[4]++;
	my($i);
	for($i=0;$i<=$#time;$i++) {
		if (length($time[$i])<2) {
			$time[$i] = "0$time[$i]";
			}
		}
	if($time[5]<100) {$time[5] = "19$time[5]"; }
	else { $time[5] = "20".($time[5]-100); }
	return(@time);
	}
# ____ writeLog _______________________________________________________________
sub writeLog {
	open(LOG, ">>$LOGFILE");
	my($s,$m,$h,$D,$M,$Y) = getTime();
	print LOG "<$M/$D/$Y\@$h:$m:$s>\t$_[0]\n";
	print "<$h:$m:$s> ($$) " if($DEBUG);
	print "$_[0]\n" unless($SILENT);
	close(LOG);
	}

# ____ debug _______________________________________________________________
sub debug {
	return unless($DEBUG);
	open(LOG, ">>$LOGFILE");
	my($s,$m,$h,$D,$M,$Y) = getTime();
	print LOG "<$M/$D/$Y\@$h:$m:$s>\tDEBUG: $_[0]\n";
	print "<$h:$m:$s> ($$) " unless($SILENT);
	print "  $_[0]\n" unless($SILENT);
	close(LOG);
	}

# ____ dieLog _______________________________________________________________
sub dieLog {
	open(LOG, ">>$LOGFILE");
	my($s,$m,$h,$D,$M,$Y) = getTime();
	print LOG "<$M/$D/$Y\@$h:$m:$s>\tFATAL ERROR: $_[0]\n";
	print "<$h:$m:$s> ($$) " unless($DEBUG);
	print "FATAL ERROR: $_[0]\n" unless($SILENT);
	close(LOG);
	foreach (@SHOUTPID) { kill 9 => $_; }
	foreach (@SHOUTPIDFILE) { unlink($_); }
	unlink $PIDFILE; #remove pid file upon death
	unlink $AVAILABLE;
	unlink $USED;
	exit;
	}


# ____ RESET ______________________________________________________________
# when we receive a HUP signal, prepare to reread the playlist
sub RESET {
	$RESET = 1;
	}

# ____ KILLER - Super Signal Handler __________________________________________
# this sub gets called by all handled signals. if $_killFun is defined, calls
# it for clean-up and death
sub KILLER {
	dieLog("stopped icedj for $CONF{'logname'}");
	}

