#!/usr/bin/perl
#
# nzbperl.pl -- version 0.6.5
# 
# for more information:
# http://noisybox.net/computers/nzbperl/ 
#
#########################################################################################
# Copyright (C) 2004 jason plumb
#
# 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.
#########################################################################################
#
# General TODOs:
#   * Consider putting tempfile (.parts files) in /tmp or other customize location
#     perhaps with parameter
#   * User specified output directory.
#   * Other items listed on the project webpage :-)

use strict;
use File::Basename;
use IO::File;
use IO::Select;
use XML::DOM;
use Getopt::Long;
use Time::HiRes;	# timer stuff
use Term::ReadKey;	# for no echo password reading
use Term::Cap;
use Cwd;

my $version = '0.6.5';
my $ospeed;
my $terminal = Tgetent Term::Cap { TERM => undef, OSPEED => $ospeed };
my $recv_chunksize = 5*1024;	# How big of chunks we read at once from a connection (this is pulled from ass)
my $DECODE_DBG_FILE = '/tmp/nzbdbgout.txt';
my $UPDATE_URL = 'http://noisybox.net/computers/nzbperl/nzbperl_version.txt';

my $dispchunkct = 250;			# Number of data lines to read between screen updates.
my $targkBps = 0;
my ($medbw, $lowbw) = (95, 35);	# Defaults for low and medium speed settings.
my $sleepdur = 0;				# Used when throttling

# Make stdout not buffered.
my $old_fh = select(STDOUT);
$| = 1;
select($old_fh);

my $quitnow = 0;
my $showinghelpscreen = 0;
my $skipthisfile = 0;
my $usecolor = 1;
my $logfile;

# These are getting hefty, so they're now 5 per line
my (	$server, $port, $user, $pw, $keepparts, 
		$keepbroken, $keepbrokenbin, $help, $nosort, $overwritefiles, 
		$connct, $nocolor, $insane, $dropbad, $skipfilect, 
		$reconndur, $filterregex, $configfile, $uudeview, $daemon, 
		$dlrelative, $dlpath, $noupdate, $ssl, $socks_server, 
		$socks_port, $proxy_user, $proxy_passwd, $http_proxy_server, $http_proxy_port, 
		$dlcreate, $dlcreategrp, $noansi, $queuedir, $rcport,
		$nothread, $postDecProg, $ipv6
		) =
	(	'', -1, '', '', 0, 
		0, 0, 0, 0, 0, 
		2, 0, 0, 0, 0, 
		300, undef, "$ENV{HOME}/.nzbperlrc", undef, 0, 
		undef, undef, 0, undef, undef, 
		-1, undef, undef, undef, -1, 
		undef, undef, 0, undef, undef,
		undef, undef, undef);

# How commandline args are mapped to vars.  This map is also used by config file processor
my %optionsmap = ('server=s' => \$server, 'user=s' => \$user, 'pw=s' => \$pw, 
				'help' => \$help, 'med=s' => \$medbw, 'low=s' => \$lowbw, 
				'speed=s' => \$targkBps, 'keepparts' => \$keepparts, 
				'keepbroken' => \$keepbroken, 'keepbrokenbin' => \$keepbrokenbin, 
				'nosort' => \$nosort, 'redo' => \$overwritefiles, 'conn=i' => \$connct, 
				'nocolor' => \$nocolor, 'log=s' => \$logfile,
				'insane' => \$insane, 'dropbad' => \$dropbad,
				'skip=i' => \$skipfilect, 'retrywait=i' => \$reconndur,
				'filter=s' => \$filterregex, 'config=s' => \$configfile,
				'uudeview=s' => \$uudeview, 'daemon' => \$daemon,
				'dlrelative' => \$dlrelative, 'dlpath=s' => \$dlpath, 'noupdate' => \$noupdate,
				'ssl' => \$ssl, 'socks_server=s' => \$socks_server, 'socks_port=i' => \$socks_port,
				'socks_user=s' => \$proxy_user, 'socks_passwd=s' => \$proxy_passwd,
				'http_proxy=s' => \$http_proxy_server, 'dlcreate'=>\$dlcreate, 'dlcreategrp' => \$dlcreategrp,
				'noansi' => \$noansi, 'queuedir=s' => \$queuedir, 'rcport=i' => \$rcport,
				'nothread' => \$nothread, 'postdec=s' => \$postDecProg, 'ipv6' => \$ipv6);

if(defined(my $errmsg = handleCommandLineOptions())){
	showUsage($errmsg); 
	exit 1;
}

if(not $ipv6){
	use IO::Socket::INET;
}
if(not $nocolor){
	use Term::ANSIColor;
}
if(not haveUUDeview()){	# Verify that uudeview is installed
	pc("* Please install and configure uudeview and try again.\n", "bold red");
	exit 1;
}

displayShortGPL();

checkForNewVersion();

if($user and !$pw){
	print "Password for '$user': ";
	$pw = readPassword();
}
if(defined($proxy_user) and not defined($proxy_passwd)){
	print "SOCKS Password for '$proxy_user': ";
	$proxy_passwd = readPassword();
}


my $lastdirchecktime = 0;
my %nzbfiles;	# the hash/queue of nzb files we're handling
# $nzbfiles{<filename>}->{'read'}     : 1 if we've parsed/loaded it
# $nzbfiles{<filename>}->{'finished'} : 1 if all files have been downloaded

my @fileset;
while(scalar(@ARGV) > 0){
	my $nzbfilename = shift @ARGV; #$ARGV[0];
	my @fsparts = parseNZB($nzbfilename);
	if(!defined($fsparts[0])){
		exit;
	}
	push @fileset, @fsparts;
}
sortFilesBySubject();	# It checks $sort inside
regexAndSkipping();		# It checks options inside too

print "Looks like we've got " . scalar @fileset . " possible files ahead of us.\n";

my @suspectFileInd;
if($insane){
}
else{
	doNZBSanityChecks();
	if($dropbad){
		dropSuspectFiles();
	}
}

my $rc_sock = undef;
my @rc_clients;
startRemoteControl();

my %totals;
$totals{'total size'} = computeTotalNZBSize(@fileset);
my @statusmsgs;
statMsg('Welcome -- nzbperl started!');

# Descriptions of what's in the connection hash (for sanity sake)
#
# $conn->{'sock'}       : the socket for comms
# $conn->{'msg'}        : message that describes what's going on
# $conn->{'file'}       : the file it's working on
# $conn->{'segnum'}     : the segment number of the file it's working on
# $conn->{'segbytes'}   : number of bytes read in the current segment
# $conn->{'filebytes'}  : number of bytes read in the current file
# $conn->{'bstatus'}    : status about how we're handling a body (starting/finishing)
# $conn->{'buff'}       : where body data is buffered
# $conn->{'tmpfilename'}: temporary file name
# $conn->{'tmpfile'}    : temporary file handle
# $conn->{'bwstarttime'}: time when the bandwdith applied
# $conn->{'bwstartbytes'}: bytes read on file when bandwidth applied
# $conn->{'truefname'}  : true filename on disk (assumed after decoding)
# $conn->{'skipping'}   : indicates we're in the middle of a skipping operation
# $conn->{'last data'}  : time when data was last seen on this channel
# $conn->{'sleep start'}: time that we started sleeping (for retries)
# $conn->{'isbroken'}   : set when one or more parts fails to download
# $conn->{'tfseq'}      : temporary file sequence id

$totals{'total file ct'} = scalar @fileset;

my @lastdrawtime = Time::HiRes::gettimeofday();
my @conn;
createNNTPConnections();
if($user){
	doLogins() or die "Error authenticating to server.\nPlease check the user/pass info and try again.";
}

if($daemon){
	print "nzbperl is running in --daemon mode, output suppressed\n";
	print "Check log for additional details during run...\n";
	my $pid = fork;
	$pid and print "Daemon started [$pid]\n" and exit;
	#close STDIN;
	#close STDOUT;
	#close STDERR;
}

# Start up the decoding thread...
my ($decMsgQ, $decQ);
$decMsgQ = Thread::Queue->new;	# For status msgs
$decQ = Thread::Queue->new;

my $decThread = threads->new(\&file_decoder_thread);

clearScreen();
my ($oldwchar, $wchar, $hchar, $wpixels, $hpixels);  	# holds screen size info

unlink $DECODE_DBG_FILE;  # Clean up on each run

my @queuefileset = @fileset;
my @dlstarttime = Time::HiRes::gettimeofday();
while(1){

	doFileAssignments();
	doBodyRequests();

	doReceiverPart();

	queueNewNZBFilesFromDir();	# queue up new nzb files from dir (guards inside)

	# See if queuefileset is empty AND all sockets don't have files
	# when that happens, that's when we're done.
	if(not scalar @queuefileset){		# no more files in queue
		doBodyRequests();		# total hack, but that's where decoding happens...
	}

	dequeueNextNZBFileIfNecessary();
	doRemoteControls(); 

	if(not scalar @queuefileset){		# no more files in queue
		my $done = 1;
		foreach my $i (1..$connct){
			if($conn[$i-1]->{'file'}){
				$done = 0;	# not done, at least 1 connection still going
				last;
			}
		}

		if($done){
			cursorPos(0, 1+5+(3*$connct)+9);
			pc("All downloads complete!\n", 'bold white');
			last;
		}
	}
	$quitnow and last;
}

cursorPos(0, (1+5+(3*$connct)+8));

if($quitnow){# Do some cleanups
	foreach my $c (@conn){
		next unless $c->{'file'};
		if($c->{'tmpfile'}){
			pc("Closing and deleting " . $c->{'tmpfilename'} . "...\n", 'bold white');
			undef $c->{'tmpfile'};	# causes a close
			unlink $c->{'tmpfilename'};
		}
	}
}

# TODO: Clean up server socket for remote control schtuff

disconnectAll();
pc("Waiting for file decoding thread to terminate...\n", 'bold white');
$decQ->enqueue('quit now');
$decThread->join;
pc("Thanks for using ", 'bold yellow');
pc("nzbperl", 'bold red');
pc("! Enjoy!\n\n", 'bold yellow');

#########################################################################################
# This is the thread that does file decoding
# It uses two queues for communication -- decQ for files to decode, decMsgQ for status
# messages back to the main thread.
#########################################################################################
sub file_decoder_thread {
	my ($isbroken, $tmpfilename, $truefilename, $decodedir);

	while(1){
		# We get 4 things on the q per file...isbroken, tmpfilename, truefilename, decodedir
		$isbroken= $decQ->dequeue;
		last unless defined($isbroken);
		($isbroken =~ /^quit now$/) and last;	# Time to shut down

		$tmpfilename = $decQ->dequeue;
		$truefilename = $decQ->dequeue;
		$decodedir = $decQ->dequeue;
		
		doUUDeViewFile($isbroken, $tmpfilename, $decodedir, $truefilename);
	}
}
#########################################################################################
# Does multiplexed comms receiving
#########################################################################################
sub doReceiverPart {
	my $select = IO::Select->new();

	foreach my $i (1..$connct){
	    next unless (defined($conn[$i-1]->{'sock'}));
	    $select->add($conn[$i-1]->{'sock'});
	}

	# If there are no active connections, we need to do a little sleep to prevent maxing out cpu.
	# the select->can_read call passes right through if it has no handles.
	(not $select->count()) and select undef, undef, undef, 0.1;

	my @ready = $select->can_read(0.25);

	foreach my $i (1..$connct){
		my $conn = $conn[$i-1];

		next unless $conn->{'file'};	# This connection must be working on a file...otherwise next

		# TODO: Create a way to disable reconnection (when we don't want to do it)
		if(!defined($conn->{'sock'})){
			doReconnectLogicPart($i-1);
			next;
		}
		
		my $canread = 0;
		foreach my $fh (@ready) {
		    if ($fh == $conn->{'sock'}) {
				$canread = 1;
				last;
		    }
		}

		if ($canread) {
			my ($recvret, $buff);
			if (ref($conn->{'sock'}) eq "IO::Socket::SSL") {
			    $recvret = $conn->{'sock'}->sysread($buff, $recv_chunksize);
			} 
			else {
			    $recvret = recv($conn->{'sock'}, $buff, $recv_chunksize, 0);
				if(defined($recvret)){
					$recvret = length $buff;
				}
				else{
					$recvret = -1;
				}
			}

			if(($recvret < 0) or !length($buff)){
				# TODO: Determine how to gracefully handle the crap we've already downloaded 
				if (ref($conn->{'sock'}) eq "IO::Socket::SSL") {
					$conn->{'sock'}->shutdown( 2 );
					$conn->{'sock'}->close( SSL_no_shutdown => 1 );
				} 
				else {
					$conn->{'sock'}->close;
				}
				$conn->{'sock'} = undef;
				$conn->{'sleep start'} = time;
				statMsg(sprintf("* Remote disconnect on connection #%d", $i));
				drawStatusMsgs();
			}
			
			$conn->{'buff'} .= $buff;

			if(not ($conn->{'bstatus'} =~ /starting/)){ # only bump these up if we're not starting...
				$conn->{'segbytes'} += length($buff);
				$conn->{'filebytes'} += length($buff);
				$totals{'total bytes'} += length($buff);
			}

			$conn->{'last data'} = time;

			# Spool all lines from the buffer into the output file.
			spoolOutConnBuffData($i, $conn);
		}
		drawScreenAndHandleKeys();
		doThrottling();
	}

	if ($#ready < 0) {
		drawScreenAndHandleKeys();
		doThrottling();
	}
}


#########################################################################################
# spoolOutConnBuffData - spool the given connection's data to the output file.
# There's other stuff here too....it should be made simpler.
#########################################################################################
sub spoolOutConnBuffData {
	my ($i, $conn) = @_;
	while(1){
		my $ind1 = index $conn->{'buff'}, "\r\n";
		last unless $ind1 >= 0;
		my $line = substr $conn->{'buff'}, 0, $ind1+2, '';

		if($conn->{'bstatus'} =~ /starting/){
			startSegmentOnConnection($i, $conn, $line);
			next;
		}

		# Try and detect the "real" filename
		if(not $conn->{'truefname'}){
			my $tfn = getTrueFilename($line);
			if($tfn){
				$conn->{'truefname'} = $tfn;
				statMsg("Conn. $i: Found true filename: $tfn");

				my $tfndisk = $tfn;			# Find out where it's going on disk...

				if(defined($dlpath)){
					if (defined($dlcreate)) {	# if we like to create nicely organized subdirs
						my $targdir = $dlpath . $conn->{'file'}->{'nzb path'};
						mkdir $targdir unless (-d $targdir);
						$tfndisk = $targdir . "$tfn";
					} 
					elsif (defined($dlcreategrp)){
						my $targdir = $dlpath . $conn->{'file'}->{'groups'}->[0];
						mkdir $targdir unless (-d $targdir);
						$tfndisk = $targdir . "$tfn";
					}
					else {
						$tfndisk = $dlpath . $tfn;
					}
				}
				elsif(defined($dlrelative)){
					$tfndisk = $conn->{'file'}->{'nzb path'} . $tfn;
				}
				if(-e $tfndisk){
					if(!$overwritefiles){
						# We can't just close and delete, because there will likely still be 
						# data waiting in the receive buffer.  As such, we have to set a flag
						# to indicate that the file already exists and should be skipped...
						# This is perhaps a bit silly -- we have to finish slurping in the
						# BODY part before we can start working on the next file...
						statMsg("Conn $i: File already exists on disk (skipping after segment completes)");
						$conn->{'skipping'} = 1;
					}
				}
			}
		}
		
		if($line =~ /^\.\r\n/){		# detect end of BODY..
			$conn->{'bstatus'} = 'finished';
			if($conn->{'skipping'}){

				$totals{'total file ct'}--;
				$totals{'total bytes'} -= $conn->{'filebytes'}; # Remove bytes downloaded
				$totals{'total size'} -= $conn->{'file'}->{'totalsize'}; # Remove file bytes from job total 

				undef $conn->{'tmpfile'}; # causes a close
				unlink $conn->{'tmpfilename'};
				$conn->{'file'} = undef;
				$conn->{'skipping'} = undef;	# no longer skipping (for next time)
			}
			last;
		}
		else{
			$line =~ s/^\.\././o;
			print {$conn->{'tmpfile'}} $line;
		}
	}
}

#########################################################################################
# startSegmentOnConnection - Handles an input line when a segment is just starting
# on a connection.  This looks into detecting missing segments and handles server 
# responses that mean various things.
#########################################################################################
sub startSegmentOnConnection {
	my ($i, $conn, $line) = @_;
	my ($mcode, $msize, $mbody, $mid) = split /\s+/, $line;

	# We're just starting, need to slurp up 222 (or other) response
	if($line =~ /^2\d\d\s.*\r\n/s){
		# Bad case where server sends a 5xx message after a 2xx (222)
		if(!$msize and ($conn->{'buff'} =~ /^5\d\d /)){
			# Handle this error condition (display message to user)
			my $errline = $conn->{'buff'};
			$errline =~ s/\r\n.*//s;
			statMsg(sprintf("Conn. %d: Server returned error: %s", $i, $errline));
		}
		else{
			#$conn[$i-1]->{'buff'} =~ s/2\d\d\s.*\r\n//;
			$conn->{'segbytes'} = length($conn->{'buff'});
		}
		$conn->{'bstatus'} = 'running';
	}
	else{ # This is an error condition -- often when the server can't find a segment
		$line =~ s/\r\n$//;
		statMsg( sprintf("Conn. %d FAILED to fetch part #%d (%s)", $i, 
						$conn->{'segnum'}+1, $line));
		drawStatusMsgs();
		$conn->{'bstatus'} = 'finished';  # Flag BODY segment as finished
		$conn->{'isbroken'} = 1;


		# Ok, so now that a segment fetch FAILED, we need to determine how to continue...
		# We will look at the keep variables to determine how to continue...
		# If keepbroken or keepbrokenbin are set, we will keep downloading parts...otherwise we will bump
		# up the segnum so that we skip all remaining segments (if any)
		
		if($keepbroken or $keepbrokenbin){		# If we shound continue downloading this broken file
			# Subtract the size of the current segment from the totals
			# (for this file and for the grand totals)
			my $failedsegsize = @{$conn->{'file'}->{'segments'}}[$conn->{'segnum'}]->{'size'};
			$totals{'total size'} -= $failedsegsize ;
			$conn->{'file'}->{'totalsize'} -= $failedsegsize;
		}
		else{
			statMsg(sprintf("Conn. %d aborting file (failed to fetch segment #%d)", 
					$i, $conn->{'segnum'}+1));
			
			# Adjust totals due to skipping failed file
			$totals{'total file ct'}--;
			$totals{'total bytes'} -= $conn->{'filebytes'}; # Remove bytes downloaded
			$totals{'total size'} -= $conn->{'file'}->{'totalsize'}; # Remove file bytes from job total 


			$conn->{'segnum'} = scalar @{$conn->{'file'}->{'segments'}} - 1;
			undef $conn->{'tmpfile'};	# causes a close
			unlink $conn->{'tmpfilename'};
			$conn->{'file'} = undef;
		}
	}
}

#########################################################################################
# Handles reconnection logic
#########################################################################################
sub doReconnectLogicPart {
	my $i = shift;
	my $conn = $conn[$i];

	my $remain = $reconndur - (time - $conn->{'sleep start'});
	if($remain > 0){	# still sleeping
		return;
	}
	#my $iaddr = inet_aton($server) || die "Error resolving host: $server";

	statMsg(sprintf("Connection #%d attempting reconnect to %s:%d...", $i+1, $server, $port));
	($conn->{'sock'}, my $line) = createSingleConnection($i, "$server:$port", 1);

	if(!$conn->{'sock'}){		# couldn't reconnect
		statMsg($line);
		$conn->{'sleep start'} = time;	# reset reconnect timer
		return;
	}

	my $msg = sprintf("Connection #%d reestablished.", $i+1);
	$user and $msg .= "..performing login";
	statMsg($msg);
	drawStatusMsgs();

	if($user){	#need to authenticate...
		doSingleLogin($i, 1);
		statMsg(sprintf("Login on connection #%d complete.", $i+1));
	}

	$conn->{'sleep start'} = undef;
	# These two lines reset our state so that we restart the segment we were on
	# prior to the disconnect.  Sure, a bit convoluted, but it's used elsewhere.
	$conn->{'bstatus'} = 'finished';
	$conn->{'segnum'}--;
}

#########################################################################################
# Heuristically determines the "true" filename.  Returns filename or undef
#########################################################################################
sub getTrueFilename {
	my $line = shift;
	$line =~ s/\s+$//;
	if($line =~ /^=ybegin/){			# Yenc candidate
		# I'm assuming that the last tag on the line is "name=...", which I honestly have no idea
		# if that's always true.  :)
		$line =~ s/.* name=//;
		return $line;
	}
	elsif($line =~ /^begin \d+ /){		# UUencoded candidate
		$line =~ s/^begin \d+ //;
		return $line;
	}
	else{
		return undef;
	}
}

#########################################################################################
# Handles segments and detects when we're done with a file
#########################################################################################
sub doBodyRequests {
	foreach my $i (1..$connct){
		my $conn = $conn[$i-1];
		my $file = $conn->{'file'};
		next unless $file;			# Bail if we don't have a file
		if($conn->{'segnum'} < 0){
			$conn->{'segnum'} = 0;
			my $seg = @{$file->{'segments'}}[0];
			$conn->{'seg'} = $seg;
			my $msgid = $seg->{'msgid'};

			sockSend($conn->{'sock'}, 'BODY <' . $msgid . ">\r\n");

			$conn->{'bstatus'} = 'starting';
		}
		elsif($conn->{'bstatus'} =~ /finished/){ # finished a segment
			$conn->{'segnum'}++;

			if($conn->{'segnum'} >= scalar @{$file->{'segments'}}){ # All segments for this file exhausted.

				cursorPos(5, 6+(3*($i-1)));
				my $len = pc("File finished! Sending details to decoder queue...", 'bold white');
				print ' ' x ($wchar-$len-6);
				statMsg("Conn. $i: Finished downloading " . $conn->{'file'}->{'name'});

				doDecodeOrQueueCompletedFile($conn);
				drawStatusMsgs();

				$totals{'finished files'}++;
				$conn->{'file'} = undef;
				$conn->{'seg'} = undef;

			}
			else{
				my $segnum = $conn->{'segnum'};
				my $seg = @{$file->{'segments'}}[$segnum];
				$conn->{'seg'} = $seg;
				my $msgid = $seg->{'msgid'};

				sockSend($conn->{'sock'}, 'BODY <' . $msgid . ">\r\n");

				$conn->{'bstatus'} = 'starting';
			}
		}
	}
}
#########################################################################################
# doStartFileDecoding - initiates or performs a decode for a completed file.
# If nothread is set, this will decode in place, otherwise it just queues the request
# to decode to the decoder thread.
#########################################################################################
sub doDecodeOrQueueCompletedFile {
	my $conn = shift;
	my $file = $conn->{'file'};
	undef $conn->{'tmpfile'};	# causes a close
	my $outdir = "./";	# default to current dir
	if(defined($dlpath)){
		if (defined($dlcreate)) {
			$outdir = $dlpath . $file->{'nzb path'};
		} 
		elsif(defined($dlcreategrp)){
			$outdir = $dlpath . $file->{'groups'}->[0];
		}
		else {
			$outdir = $dlpath;
		}
	}
	elsif(defined($dlrelative)){
		$outdir = $file->{'nzb path'};	# extract to same dir as nzb file
	}
	my $tmpfilename = $conn->{'tmpfilename'};
	my $truefilename = $conn->{'truefname'};
	my $isbroken = $conn->{'isbroken'};
	$isbroken = 0 unless (defined($isbroken)); # ensure a definite value
	if($nothread){
		doUUDeViewFile($isbroken, $tmpfilename, $outdir, $truefilename);
	}
	else{
		# Queue the items to the decoding thread
		$decQ->enqueue($isbroken, $tmpfilename, $truefilename, $outdir);
	}
}

#########################################################################################
# Decodes a file to disk and handles cleanup (deleting/keeping parts)
#########################################################################################
sub doUUDeViewFile {
	my ($isbroken, $tmpfilename, $decodedir, $truefilename) = @_;

	statOrQ("Starting decode of $truefilename");

	# Do the decode and confirm that it worked...
	# TODO: Make the debug file configurable and system flexible

	if(!$isbroken or $keepbrokenbin){
		my $kb = '';
		$keepbrokenbin and $kb = '-d';	# If keeping broken, pass -d (desparate mode) to uudeview
		my $rc = system("$uudeview -i -a $kb -q \"$tmpfilename\" -p \"$decodedir\">> $DECODE_DBG_FILE 2>&1");
		$rc and $isbroken = 1;	# If decode failed, file is broken

		if($rc){		# Problem with the decode
			statOrQ("FAILED decode of $tmpfilename (see $DECODE_DBG_FILE for details)");
		}
		else{
			statOrQ("Completed decode of " . $truefilename);
		}
	}

	# Decide if we need to keep or delete the temp .parts file
	if($keepparts or ($isbroken and $keepbroken)){
		my $brokemsg = $isbroken ? ' broken' : '';
		statOrQ("Keeping$brokemsg file segments in $tmpfilename (--keepparts given)");
		# TODO: rename to .broken
	}
	else {
		unlink($tmpfilename) or statOrQ("Error removing $tmpfilename from disk: $!");
	}

	runPostDecodeProgram($tmpfilename, $decodedir, $truefilename, $isbroken);
}
#########################################################################################
# runPostDecodeProgram -- Possibly runs an external program after a file has been
# decoded (regardless of success).
#########################################################################################
sub runPostDecodeProgram {
	my ($tmpfilename, $decodedir, $truefilename, $isbroken) = @_;
	return unless defined($postDecProg);

	$truefilename = $decodedir . $truefilename;
	
	# This is a little strange...but showing the env vars onto the command is 
	# the only way I could find to pass environments from a perl thread to 
	# an external prog.  I wish there was a better way (like using $ENV, but
	# that fails)
	my $cmd = sprintf("NZBP_FILE='%s' NZBP_TEMPFILE='%s' NZBP_ISBROKEN='%s' %s", 
			$truefilename, $tmpfilename, $isbroken, $postDecProg);
	statMsg("Running post-decoding program : $postDecProg");
	drawStatusMsgs();
	system($cmd);
	statMsg("Finished running post-decoding program.");
	drawStatusMsgs();
				
}
#########################################################################################
# Shifts from the file queue and assigns the files to a connection.  When a file is
# assigned, the first segment is not assigned.
#########################################################################################
sub doFileAssignments {
	foreach my $i (1..$connct){
		my $conn = $conn[$i-1];
		next if $conn->{'file'};	# already working on a file

		my $file = shift @queuefileset;
		last unless $file;
		statMsg(sprintf("Conn. %d starting file: %s", $i, $file->{'name'}));
		$conn->{'file'} = $file;
		$conn->{'segnum'} = -1;
		$conn->{'filebytes'} = 0;
		$conn->{'truefname'} = undef;
		$conn->{'bwstartbytes'} = 0;
		$conn->{'isbroken'} = 0; # Assume unbroken until we know it is
		@{$conn->{'bwstarttime'}} = Time::HiRes::gettimeofday();
		$conn->{'tfseq'}++;

		# Create temp filename and open
		my $tmpfile = 'nzbperl.tmp' . time . '.' . $i . '.' . $conn->{'tfseq'} . '.parts';
		if(defined($dlpath)){		# stick in dlpath if given
			$tmpfile = $dlpath . $tmpfile;
		}
		elsif(defined($dlrelative)){ # otherwise stick in relative dir to nzbfile
			$tmpfile = $file->{'nzb path'} . $tmpfile;
		}
		$conn->{'tmpfilename'} = $tmpfile;
		$conn->{'tmpfile'} = undef;	# just to be absolutely sure

		open $conn->{'tmpfile'}, ">$tmpfile" or 
			(statMsg("*** ERROR opening $tmpfile (critical!)") and next);
		statMsg("Opened temp file $tmpfile");
		binmode $conn->{'tmpfile'};
	}
}

#########################################################################################
# Decides if its time to do the next nzb file...which is when the @queuefileset array
# is empty and there is at least 1 idle connection.
#########################################################################################
sub dequeueNextNZBFileIfNecessary {
	
	return if scalar(@queuefileset);	# still have queued files

	my $done = 1;
	foreach my $i (1..$connct){
		if(not $conn[$i-1]->{'file'}){	# the connection is idle
			queueNewNZBFilesFromDir(1);	# force a dircheck first
			dequeueNextNZBFile();
			drawStatusMsgs();
			return;
		}
	}
}

#########################################################################################
# Pulls out the next nzb file in queue, parses it, and then add its files/parts to 
# @queuefileset.
#########################################################################################
sub dequeueNextNZBFile {
	my @keys = keys %nzbfiles;
	foreach my $key (@keys){
		if(not $nzbfiles{$key}->{'read'}){
			statMsg("Moving to next nzb file in queue: $key");
			my @newset = parseNZB($queuedir . '/' . $key, 1);
			if(!defined($newset[0])){
				statMsg("Warning: no new files loaded from queued nzb file");
				return;
			}
			push @queuefileset, @newset;
			statMsg("Loaded " . scalar(@newset) . " new files to download from nzb file: $key");
			$totals{'total file ct'} += scalar @newset;
			$totals{'total size'} += computeTotalNZBSize(@newset);
			$nzbfiles{$key}->{'read'} = 1;
			last;
		}
	}
}
#########################################################################################
# Looks at the nzbfile hash and counts the number that haven't been read (are queued) 
#########################################################################################
sub countQueuedNZBFiles {
	my @keys = keys %nzbfiles;
	my $ct = 0;
	foreach my $key (@keys){
		if($nzbfiles{$key}->{'read'} == 0){
			$ct++;
		}
	}
	return $ct;
}

#########################################################################################
# queues new nzb files from the queue dir if they exist and adds them to the hash/queue
# of all nzb files we're processing.
#########################################################################################
sub queueNewNZBFilesFromDir {
	my $forcecheck = shift;
	return unless $queuedir and not scalar @queuefileset;
	return unless $forcecheck or (time - $lastdirchecktime > 15);	# don't check more than once every 15 seconds
	$lastdirchecktime = time;

	opendir(QDIR, $queuedir);
	my @candidates = grep(/\.nzb$/, readdir(QDIR));
	foreach my $file (@candidates){
		if( !defined($nzbfiles{$file})){	# not queued yet
			statMsg("Queueing new nzb file found on disk: $file");
			$nzbfiles{$file}->{'read'} = 0;
		}
	}
	closedir(QDIR);

}

#########################################################################################
# Start up the remote control(s)
#########################################################################################
sub startRemoteControl {
	return unless defined($rcport);	# nuthin to do

	eval "use XML::Simple;";
	($@) and die "ERROR: XML::Simple required if using remote control...Please install it.";

	$rc_sock = createRCMasterSocket();
	
	print "Remote control server socket created.\n";
}

#########################################################################################
# creates the remote control master port, using either ipv4 or ipv6
#########################################################################################
sub createRCMasterSocket {
	my $ret;

	my %opts = (Listen => 5, LocalAddr => 'localhost',
				LocalPort => $rcport,
				Proto=>'tcp', Type => SOCK_STREAM, Reuse => 1); 
	if($ipv6){
		$ret = IO::Socket::INET6->new( %opts ) or die "Error creating remote control socket: $!";
	}
	else{
		$ret = IO::Socket::INET->new( %opts ) or die "Error creating remote control socket: $!";
	}
	return $ret;
}

#########################################################################################
# Main loop entry point for handling remote control stuff
#########################################################################################
sub doRemoteControls {
	return unless defined($rcport);	
	getNewRcClients();
	handleRcClients();
	my @tmprcc;
	foreach my $client (@rc_clients){				#clean up dropped clients
		if(defined($client->{'closenow'})){
			statMsg(sprintf("Remote control client from %s:%s disconnected.", $client->{'ip'}, $client->{'port'}));
			close $client->{'sock'};
		}
		else{
			push @tmprcc, $client;
		}
	}
	@rc_clients = @tmprcc;
}

#########################################################################################
# handleRcClients -- read and handle all remote commands from all clients
#########################################################################################
sub handleRcClients{
	for (my $i=0; $i < scalar @rc_clients; $i++){
		my $client = $rc_clients[$i];
		my $cmd = readRcClientCommand($client);
		defined($cmd) and handleRcClientCmd($client, $cmd);
	}
}
#########################################################################################
# handleRcClientCmd - Handle's an rc client command
#########################################################################################
sub handleRcClientCmd {
	my ($client, $cmdstr) = @_;
	#statMsg("DEBUG: Got command $cmdstr");
	my ($cmd, $params, $responsemsg) = ($cmdstr, $cmdstr);
	$cmd =~ s/\s+.*//;
	$params =~ s/^\w+\s+//;
	$params = '' unless $params ne $cmd;
	if($cmd =~ /ping/i){
		$responsemsg = sprintf("PONG! %s", $params);
	}
	elsif($cmd =~ /^quit/i){
		sendRemoteResponse($client, "Nice having ya.");
		$client->{'closenow'} = 1;
		return;
	}
	elsif($cmd =~ /^keys/i){
		my @keys = split //, $params;
		foreach my $key (@keys){
			handleKey($key);
		}
		$responsemsg = sprintf("Ok, processed %d keystrokes", scalar @keys);
	}
	elsif($cmd =~ /^summary/i){
		$responsemsg = generateRcSummary();
	}
	elsif($cmd =~ /^queue/i){
		if(defined($nzbfiles{$params})){	# not queued yet
			$responsemsg = "Error: Refusing to queue file already queued ($params).";
		}
		elsif(not -e $params){
			$responsemsg = "Error: File does not exist ($params)";
		}
		else{
			$responsemsg = "Queueing new nzb file found on disk: $params";
			statMsg($responsemsg);
			$nzbfiles{$params}->{'read'} = 0;
		}
	}
	else{
		$responsemsg = "Sorry, command not understood.";
	}
	sendRemoteResponse($client, $responsemsg);
}
#########################################################################################
# sendRemoteResponse -- send a remote command response to the remote client.
#########################################################################################
sub sendRemoteResponse {
	my ($client, $msg) = @_;
	my $sock = $client->{'sock'};
	# simple protocol, eh?
	sockSend($sock, sprintf("%d\r\n%s\r\n", length($msg)+2, $msg));
}
#########################################################################################
# readRcClientCommand -- Attempts to read a command from the client socket
# returns the command or undef
#########################################################################################
sub readRcClientCommand {
	my $client = shift;
	my $buff = readNewRcClientSockData($client);
	if(defined($buff)){
		my $nlindex = index $client->{'data'}, "\r\n";
		if($nlindex >= 0){
			my $cmd = substr $client->{'data'}, 0, $nlindex+2, '';
			$client->{'data'} = substr $client->{'data'}, length($cmd);
			$cmd =~ s/^\s+//; $cmd =~ s/\s+$//;
			return $cmd;
		}
	}
	return undef;
}
#########################################################################################
# readNewRcClientSockData -- Pulls client data off the socket if there is any.
#########################################################################################
sub readNewRcClientSockData {
	my $client = shift;
	my $sock = $client->{'sock'};
	my $sockfn = fileno($sock);
	my ($rin, $win, $ein) = ('', '');
	my ($rout, $wout, $eout);
	vec($rin, $sockfn, 1) = 1;
	vec($win, $sockfn, 1) = 1;
	vec($ein, $sockfn, 1) = 1;
	my $nfound = select($rout=$rin, $wout=$win, $eout=$ein, 0);  
	return undef unless $nfound > 0;
	if(vec($rout, $sockfn,1) == 1){
		recv $sock, my $buff, $recv_chunksize, undef;
		if(not length($buff)){
			$client->{'closenow'} = 1;
			return undef;
		}
		$client->{'data'} .= $buff;
		return $buff;
	}
	if((vec($eout, $sockfn, 1) == 1) || (vec($wout, $sockfn, 1) != 1)){
		$client->{'closenow'} = 1;
	}
	return undef;
}

#########################################################################################
# Accepts new connections from clients and adds them to the list.
#########################################################################################
sub getNewRcClients {
	while(1){
		my ($rin,$rout) = ('','');
		vec($rin, fileno($rc_sock), 1)  = 1;
		my $nfound = select($rout=$rin, undef, undef, 0);  
		last unless ($nfound > 0);
		my $nclient;
		my $client_addr = accept($nclient, $rc_sock);
		#my $old = select($nclient);
		#$| = 1;	# make nonbuffered
		#select($old);
		my ($clientport, $clientippart) = sockaddr_in($client_addr);
		my $clientip = inet_ntoa($clientippart);
		statMsg("New remote control connection from " . $clientip . ":" . $clientport);
		sockSend($nclient, "nzbperl version $version\r\n");
		push @rc_clients, {'sock' => $nclient, 'ip' => $clientip, 'port' => $clientport};
	}
}

#########################################################################################
# generateRcSummary - generates a summary of information for a remote request for it.
#########################################################################################
sub generateRcSummary {
	my %s;
	$s{'connections'} = $connct;
	my $tspeed = $targkBps ? hrsv($targkBps*1024) . "Bps" : "unlimited";
	$s{'speeds'} = {'current' => getCurrentSpeed(), 'target' => $tspeed, 'session' => getTotalSpeed()};
	$s{'completed'} = {'files' => $totals{'finished files'}, 'size' => hrsv($totals{'total bytes'})};
	$s{'completed'}->{'files'} = 0 unless $s{'completed'}->{'files'};
	$s{'remaining'} = {'files' => $totals{'total file ct'}-$totals{'finished files'},
						'time' => getETA(), 'size' => hrsv($totals{'total size'} - $totals{'total bytes'}),
						'queued_nzb_files' => countQueuedNZBFiles()};
	my $summary = XML::Simple->new()->XMLout(\%s, rootname => 'summary', noattr => 1);
	$summary =~ s/\s+$//;
	return $summary ;
}
#########################################################################################
# Creates all connections and adds them to the @conn global
#########################################################################################
sub createNNTPConnections {
	foreach my $i (1..$connct){
		#my $iaddr = inet_aton($server) || die "Error resolving host: $server";
		my $paddr = "$server:$port";
		($conn[$i-1]->{'sock'}, my $line) = createSingleConnection($i-1, $paddr);
	}
	return 1;
}

#########################################################################################
# Connects to an NNTP server and attempts to read the greet string line.
# Returns the socket and the greet line.
#########################################################################################
sub createSingleConnection {
	my ($i, $paddr, $silent) = @_;
	my ($osock, $sock) = (undef, undef);

	if (defined($socks_server)) {
	    porlp(sprintf("Attempting SOCKS connection #%d %s:%d ->%s:%d...",
				   $i+1, $socks_server, $socks_port, $server, $port), $silent);

		my %sockparam = (ProxyAddr => $socks_server, ProxyPort => $socks_port,
			ConnectAddr => $server, ConnectPort => $port );

		if(defined($proxy_user)){		# Add authentication info
			$sockparam{'AuthType'} = 'userpass';
			$sockparam{'Username'} = $proxy_user;
			$sockparam{'Password'} = $proxy_passwd;
		}

	    $osock = IO::Socket::Socks->new( %sockparam );
	} 
	elsif (defined($http_proxy_server)) {
	    porlp(sprintf('Attempting HTTP Proxy connection #%d %s:%d -> %s:%d...',
				   $i+1, $http_proxy_server, $http_proxy_port, $server, $port), $silent);
	    $osock = Net::HTTPTunnel->new( 'proxy-host' => $http_proxy_server,
					   'proxy-port' => $http_proxy_port,
					   'remote-host' => $server,
					   'remote-port' => $port );
	} 
	else {
	    porlp(sprintf('Attempting connection #%d to %s:%d...', $i+1, $server, $port), $silent);
	    $osock = createNNTPClientSocket($paddr);
	}
	if(!$osock){
		return (undef, "Error connecting to server: '$!'");
	}
	not $silent and pc("success!\n", 'bold white');

	if (defined($ssl)) {
	    porlp(sprintf("Establishing SSL connection #%d to %s:%d...", $i+1, $server, $port), $silent);
	    $sock = IO::Socket::SSL->start_SSL($osock);
	    die "SSL error: " . IO::Socket::SSL::errstr() . $!  unless (defined($sock));
	} 
	else {
	    $sock = $osock;
	}

	my $line = blockReadLine($sock);	# read server connection/response string
	not $line =~ /^(200|201)/ and die "Unexpected server response: $line" . "Expected 200 or 201.\n";
	if (ref($sock) eq "IO::Socket::SSL") {
	    my ($subj, $iss, $cipher) = ($sock->peer_certificate("subject"),
					 $sock->peer_certificate("issuer"),
					 $sock->get_cipher());
	    pc("cipher: $cipher: Subject $subj: Issuer: $iss\n", "bold white");
	}
	return ($sock, $line);
}

#########################################################################################
# Encapsulates creating a socket for use with NNTP.  Pulled to a sub because it can 
# handle IPv6 sockets if the option is set.
#########################################################################################
sub createNNTPClientSocket {
	my $paddr = shift;
	my %opts = (PeerAddr => $paddr, Proto => 'tcp', Type => SOCK_STREAM);
	$ipv6 and return IO::Socket::INET6->new(%opts); 
	return IO::Socket::INET->new(%opts);
}

#########################################################################################
# Attempts to perform a login on each connection
#########################################################################################
sub doLogins {
	foreach my $i (1..$connct){
		doSingleLogin($i-1);
	}
	return 1;
}

#########################################################################################
# Logs in a single connection.  Pass in the connection index.
#########################################################################################
sub doSingleLogin {
	my ($i, $silent) = @_;
	my $conn = $conn[$i];
	my $sock = $conn[$i]->{'sock'};
	not $silent and printf("Attempting login on connection #%d...", $i+1);

	sockSend($sock, "AUTHINFO USER $user\r\n");

	my $line = blockReadLine($sock);
	if($line =~ /^381/){
		
		sockSend($sock, "AUTHINFO PASS $pw\r\n");
		
		$line = blockReadLine($sock);
		$line =~ s/\r\n//;
		(not $line =~ /^281/) and not $silent and print ">FAILED<\n* Authentication to server failed: ($line)\n" and exit(0);
		not $silent and print "success!\n";
	}
	elsif($line =~ /^281/){ # not sure if this happens, but this means no pw needed I guess
		not $silent and print "no password needed, success!\n";
	}
	else {
		not $silent and print "server returned: $line\n";
		die ">LOGIN FAILED<\n";
	}
}

#########################################################################################
# Computes and returns the total speed for this session.
#########################################################################################
sub getTotalSpeed {
	my $runtime = Time::HiRes::tv_interval(\@dlstarttime);
	return uc(hrsv($totals{'total bytes'}/$runtime)) . 'Bps';
}

#########################################################################################
# Looks at all the current connections and calculates a "current" speed
#########################################################################################
sub getCurrentSpeed {
	my $sumbps = 0;
	my $suppresshsrv = shift;
	foreach my $i (1..$connct){
		my $c = $conn[$i-1];
		next unless $c->{'file'};	# skip inactive connections
		$sumbps += ($c->{'filebytes'} - $c->{'bwstartbytes'})/Time::HiRes::tv_interval($c->{'bwstarttime'});
	}
	$suppresshsrv and return $sumbps;
	return uc(hrsv($sumbps)) . 'Bps';
}
#########################################################################################
# gets the estimated ETA in hrs:mins:secs
#########################################################################################
{
  my @old_speeds;
  sub getETA {
	  my ($h, $m, $s);
	  my $curspeed = getCurrentSpeed(1) || 0; # in bytes/sec
	  
	  if (push(@old_speeds, $curspeed) > 20) { # keep the last 20 measurements
		 shift(@old_speeds);
	  }
	  
	  my $avgspeed = 0;
	  foreach my $i (@old_speeds) {
		 $avgspeed += $i;
	  }
	  $avgspeed /= scalar(@old_speeds);
	  if ($avgspeed == 0) {
		 return "??:??:??";
	  }
	  
	  my $remainbytes = $totals{'total size'} - $totals{'total bytes'};
	  my $etasec = $remainbytes / $avgspeed;
	  $h = int($etasec/(60*60));
	  $m = int(($etasec-(60*60*$h))/60);
	  $s = $etasec-(60*60*$h)-(60*$m);
	  if($h > 240){	# likely bogus...just punt
		 return "??:??:??";
	  }
	  return sprintf("%.2d:%.2d:%.2d", $h, $m, $s);
	}
}

#########################################################################################
# Checks the last paint time and updates the screen if necessary.  Also checks for 
# keyboard keys.
#########################################################################################
sub drawScreenAndHandleKeys {
	$daemon and return;	# don't draw screen when in daemon mode...RC keys handled elsewhere
	if($showinghelpscreen){
		cursorPos(40, 14);
		pc("ETA: " . getETA(), 'bold green');
		pc(")", 'bold white');
		cursorPos(0, (1+5+(3*$connct)+8));
	}
	elsif((Time::HiRes::tv_interval(\@lastdrawtime) > 0.5) or # Refresh screen every 0.5sec max
		($decMsgQ->pending > 0)){							  # or we got status messages from decoder thread

		($wchar, $hchar, $wpixels, $hpixels) = GetTerminalSize();
		if($oldwchar != $wchar){
			$oldwchar and statMsg("Terminal was resized (new width = $wchar), redrawing");
			clearScreen();
			drawBorder();
		}
		$oldwchar = $wchar;
		@lastdrawtime = Time::HiRes::gettimeofday();
		drawHeader();
		drawConnInfos();
		drawStatusMsgs();

		cursorPos(0, (1+5+(3*$connct)+8));
		pc("'?' for help> ", 'bold white');
	}
	my $char;
	while (defined ($char = getch()) ) {	# have a key
		$char =~ s/[\r\n]//;
		handleKey($char);
	}
}
#########################################################################################
# getch -- gets a key in nonblocking mode
#########################################################################################
sub getch {
	$daemon and return;
	ReadMode ('cbreak');
	my $char;
	$char = ReadKey(-1);
	ReadMode ('normal');                  # restore normal tty settings
	return $char;
}

#########################################################################################
# Does bandwidth throttling
#########################################################################################
sub doThrottling {
	not $targkBps and return;		# Max setting, don't throttle.
	$quitnow and return;			# Don't bother if quitting
	my $curbps = getCurrentSpeed(1)/1024; # in kBps
	# TODO: Using percentages could likely make this way better.
	# (ie. inc/dec sleep duration by error percentage %)
	if($curbps > $targkBps){		# We're going too fast...
		if($sleepdur == 0){
			$sleepdur = 0.001;		# arbitrary 1ms add
		}
		else{
			$sleepdur *= 1.5;
		}
		if($sleepdur > 1.0){		# cap at 1 second sleep time, which is rediculously long anyway
			$sleepdur = 1.0;
		}
	}
	elsif($curbps < $targkBps){
		if($sleepdur > 0){
			if($sleepdur < 0.00001){	# lowest thresshold at 10us
				$sleepdur = 0;
			}
			else{
				$sleepdur -= ($sleepdur * 0.5);
			}
		}
	}
	if($sleepdur > 0){ 				# throttle if appropriate
		select undef, undef, undef, $sleepdur;
	}
}

#########################################################################################
# Trim the middle out of a string to shorten it to a target length
#########################################################################################
sub trimString {
	my $string = shift;
	my $target_len = shift;

	my $len = length($string);

	if($target_len >= $len || $target_len < 5) {
		return $string;
	}
	my $chop = $len - $target_len + 3; # 3 for the ...
	substr($string, ($len - $chop) / 2, $chop) = "...";
	return $string;
}

#########################################################################################
# Handles a keypress
#########################################################################################
sub handleKey {

	if($showinghelpscreen){
		$showinghelpscreen = 0;
		clearScreen();
		$oldwchar = 0;		# Hack to force border(s) to be redrawn
		return;	# cancel help screen display
	}

	my $key = shift;
	if($key =~ /q/){
		$quitnow = 1;
		statMsg("User forced quit...exiting...");
		# TODO: Close open files and delete parts files.
		drawStatusMsgs();
		updateBWStartPts();
	}
	elsif($key =~ /1/){
		$targkBps = $lowbw;
		statMsg("Setting bandwidth to low value ($lowbw" . "kBps)");
		drawStatusMsgs();
		updateBWStartPts();
	}
	elsif($key =~ /2/){
		$targkBps = $medbw;
		statMsg("Setting bandwidth to medium value ($medbw" . "kBps)");
		drawStatusMsgs();
		updateBWStartPts();
	}
	elsif($key =~ /3/){
		$targkBps = 0;	# set to high 
		statMsg("Setting bandwidth to maximum (unlimited)");
		drawStatusMsgs();
		updateBWStartPts();
	}
	elsif($key =~ /h/ or $key =~ /\?/){
		statMsg("Displaying help screen at user's request");
		showHelpScreen();
	}
	elsif($key =~ /c/){
		$usecolor = $usecolor ^ 0x01; #invert value
	}
	elsif($key =~ /\+/){
		if($targkBps){
			$targkBps++;
			statMsg("Nudging bandwidth setting up to " . $targkBps . "kBps");
			drawStatusMsgs();
			updateBWStartPts();
		}
	}
	elsif($key =~ /-/){
		if(!$targkBps){ # Set to unlimited
			$targkBps = int(getCurrentSpeed(1)/1024)-1;
			statMsg("Nudging bandwidth setting down to " . $targkBps . "kBps");
			
		}
		elsif($targkBps > 1){ # Bottom out at 1
			$targkBps--;
			statMsg("Nudging bandwidth setting down to " . $targkBps . "kBps");
		}
		drawStatusMsgs();
		updateBWStartPts();
	}
	else {
		statMsg("Unknown key: $key (try 'h' for help)");
	}
}


#########################################################################################
# When the bandwidth changes, update all bw baselines for all connections
#########################################################################################
sub updateBWStartPts {
	foreach my $i (1..$connct){
		my $c = $conn[$i-1];
		$c->{'bwstartbytes'} = $c->{'filebytes'};
		@{$c->{'bwstarttime'}} = Time::HiRes::gettimeofday();
	}
}

#########################################################################################
# Draws the header that contains summary info etc.
#########################################################################################
sub drawHeader(){
	cursorPos(2, 1);
	my $len = 0;
	$len += pc("nzbperl v.$version", 'bold red');
	$len += pc(" :: ", 'bold white');
	$len += pc("noisybox.net", 'bold red');
	my $queuedCount = countQueuedNZBFiles();
	if($queuedCount > 0){
		$len += pc("  [", 'bold blue');
		$len += pc(sprintf("+%d nzb files queued", $queuedCount), 'bold cyan');
		$len += pc("]", 'bold blue');
	}
	if(scalar @rc_clients > 0){
		$len += pc("  [", 'bold blue');
		$len += pc(sprintf("%d remotes", scalar @rc_clients), 'bold cyan');
		$len += pc("]", 'bold blue');
	}
	pc((' ' x ($wchar-$len-4)), 'white');

	cursorPos(2, 3);
	$len += pc("Files remaining: ", 'bold white');
	$len += pc($totals{'total file ct'}-$totals{'finished files'}, 'bold green');
	#$len += pc(scalar(@queuefileset)+1, 'bold green');
	$len += pc(" of ", 'white');
	$len += pc($totals{'total file ct'}, 'bold green');
	my $dlperc = $totals{'total size'} == 0 ? 0 : int(100.0*$totals{'total bytes'} / $totals{'total size'});
	$len += pc(' [', 'bold blue');
	$len += pc(hrsv($totals{'total bytes'}) . 'B', 'bold green');
	$len += pc('/', 'bold white');
	$len += pc(hrsv($totals{'total size'}) . 'B', 'bold green');
	$len += pc(']', 'bold blue');
	$len += pc(" ", 'white');
	$len += pc($dlperc. '%', 'bold yellow');
	$len += pc("  ETA: ", 'bold white');
	$len += pc(getETA(), 'bold yellow');
	pc((' ' x ($wchar-$len-4)), 'white');

	cursorPos(2, 2);
	$len = pc("Current speed: ", 'bold white');
	$len += pc(getCurrentSpeed(), 'bold green');
	$len += pc(" (", 'bold blue');
	$len += pc("target", 'white');
	$len += pc(' = ', 'white'); 
	if($targkBps){
		$len += pc(hrsv($targkBps*1024) . "Bps", 'bold green');
	}
	else{
		$len += pc("unlimited!", 'bold red');
	}
	$len += pc(")", 'bold blue');
	$len += pc("  Session speed: ", 'bold white');
	$len += pc(getTotalSpeed(), 'bold green');
	pc((' ' x ($wchar-$len-4)), 'white');
}


#########################################################################################
# Draws statuses for all individual connections
#########################################################################################
sub drawConnInfos(){
	my $startrow = 6;
	my $len;
	foreach my $i(1..$connct){
		my $conn = $conn[$i-1];

		cursorPos(2, $startrow+(3*($i-1)));

		if(!defined($conn->{'sock'})){	# connection closed
		#if(!$conn->{'sock'}->connected){	# connection closed
			$len = pc(sprintf("%d: ", $i), 'bold white');
			$len += pc("Connection is closed", 'bold red');
			if($conn->{'sleep start'}){	# will be a reconnect
				my $remain = $reconndur - (time - $conn->{'sleep start'});
				$len += pc(sprintf(" (reconnect in %s)", hrtv($remain)), 'bold yellow');
			}
			pc((' ' x ($wchar-$len-4)), 'white');

			cursorPos(2, $startrow+(3*($i-1))+1);
			pc((' ' x ($wchar-4)), 'white');
			next;
		}

		if(not $conn->{'file'}){
			if(scalar(@queuefileset) == 0){
				# This connection has no more work to do...
				$len = pc(sprintf("%d: Nothing left to do...", $i), 'bold cyan');
				pc((' ' x ($wchar-$len-4)), 'white');
				cursorPos(2, $startrow+(3*($i-1))+1);
				$len = pc("   <waiting for others to finish>", 'bold cyan');
				pc((' ' x ($wchar-$len-4)), 'white');
			}
			next;
		}

		my $file = $conn->{'file'};
		my $filesize = $file->{'totalsize'};
		my $filebytesread = $conn->{'filebytes'};
		my $segnum = $conn->{'segnum'}+1;
		my $segct = scalar @{$conn->{'file'}->{'segments'}};
		my $segbytesread = $conn->{'segbytes'};
		my $cursegsize = @{$file->{'segments'}}[$segnum-1]->{'size'};

		$len = pc(sprintf("%d: Downloading: ", $i), 'bold white');
		my $fn = $file->{'name'};
		if( length($fn) + $len > $wchar-4){
			$fn = substr($fn, 0, $wchar-4-$len);
		}
		$len += pc($fn, 'white');
		if($len < $wchar-4){
			pc(' ' x ($wchar-$len-4), 'white');
		}

		cursorPos(2, $startrow+(3*($i-1))+1);
		my $perc = 0;
		$filesize and $perc = int(($filebytesread/$filesize)*25);
		if ($noansi) {
			($perc > 25) and $perc = 25;	# cap progress bar length
			$len = pc("   |", 'bold white');
			if($perc){
				$len += pc('#' x ($perc-1), 'bold white');
				$len += pc('#', 'bold red');
			}
			$len += pc('-' x (25-$perc), 'white');                                                                                                    
			$len += pc("| ", 'bold white');                                                                                                           
		} 
		else {
			$len = pc("\x1B(0" . "   [", 'bold white');
			if($perc){
				$len += pc('a' x ($perc-1), 'bold white');
				$len += pc('a', 'bold red');
			}
			$len += pc('q' x (25-$perc), 'white');
			$len += pc("] " . "\x1B(B", 'bold white');
		}
		if($filesize){
			$len += pc( sprintf("%2d", ($filebytesread/$filesize)*100) . "%", 'bold yellow');
		}
		else{
			$len += pc("??%", 'bold yellow');
		}
		$len += pc(' ' x (7-length(hrsv($filebytesread))) . "[", 'bold white');
		#$len += pc(sprintf("%5s", hrsv($filebytesread)), 'bold green');
		$len += pc(hrsv($filebytesread), 'bold green');
		$len += pc("/", 'bold white');
		$len += pc(hrsv($filesize), 'bold green');
		$len += pc("]", 'bold white');
		$len += pc("  [part ", 'bold white');
		$len += pc($segnum, 'bold cyan');
		$len += pc("/", 'bold white');
		$len += pc($segct, 'bold cyan');
		$len += pc(" ", '');
		$len += pc(sprintf("%4s", hrsv($segbytesread)), 'bold cyan');
		$len += pc("/", 'bold white');
		$len += pc(hrsv($cursegsize), 'bold cyan');
		$len += pc("]", 'bold white');

		pc((' ' x ($wchar - $len - 4)), 'white');

	}

}

#########################################################################################
sub drawStatusMsgs {
	# TODO:  Consider saving state about status messages -- could save cycles by not
	#        automatically drawing every time.
	$showinghelpscreen and return;
	my $row = 3*$connct + 6 + 1;
	my $statuslimit = 6;	# number of lines to show.

	# Pull any decode messages from the queue and append them
	# This might not be the *best* place for this...
	while($decMsgQ->pending > 0){
		statMsg($decMsgQ->dequeue);
	}

	# Trim status messages to size
	while( scalar(@statusmsgs) > $statuslimit){
		shift @statusmsgs;
	}
	foreach my $line (@statusmsgs){
		cursorPos(2, $row);
		if(length($line) > $wchar-4){
			$line = substr($line, 0, $wchar-4);	# Clip line
		}
		else{
			$line .= (' ' x ($wchar-4-length($line)));
		}
		pc($line, 'white');
		$row++;
	}
	cursorPos(0, (1+5+(3*$connct)+8));
	pc("'?' for help> ", 'bold white');
}

#########################################################################################
# Draws a border around the screen.
#########################################################################################
sub drawBorder {
	drawVLine(0);
	drawVLine($wchar);
	drawHLine(0, "top");
	drawHLine(4, "middle");
	drawHLine(1+5+(3*$connct), "middle");
	drawHLine(1+5+(3*$connct)+7, "bottom");
}

sub drawHLine {
	my $ypos = shift;
	my $hpos = shift;
	cursorPos(0, $ypos);
	if ($noansi) {
		pc('+' . ('-' x ($wchar-2)) . '+', 'bold white');
	} 
	else {
		if ($hpos eq "top") {
			pc("\x1B(0" . 'l' . ('q' x ($wchar-2)) . 'k' . "\x1B(B", 'bold white');
		} 
		elsif ($hpos eq "middle") {
			pc("\x1B(0" . 't' . ('q' x ($wchar-2)) . 'u' . "\x1B(B", 'bold white');
		} 
		elsif ($hpos eq "bottom") {
			pc("\x1B(0" . 'm' . ('q' x ($wchar-2)) . 'j' . "\x1B(B", 'bold white');
		}
	}
}
sub drawVLine {
	my $xpos = shift;
	my $height = shift;
	not $height and $height = (1+5+(3*$connct)+7);
	foreach(0..$height){
		cursorPos($xpos, $_);
		if ($noansi) {
			pc('|', 'bold white');
		} 
		else {
			pc("\x1B(0" . "x" . "\x1B(B", 'bold white');
		}
	}
}

#########################################################################################
# helper for printing in color (or not)
#########################################################################################
sub pc {
	my ($string, $colstr) = @_;
	!defined($colstr) and $colstr = "white";	# default to plain white
	$daemon and return length($string);
	if($usecolor){
		print colored ($string, $colstr);
	}
	else{
		print $string;
	}
	return length($string);
}

sub clearScreen {
	!$daemon and 
		$terminal->Tputs('cl', 1, *STDOUT);			# clears screen
}

#########################################################################################
# Positions the cursor at x,y.  Looks at $daemon first.
#########################################################################################
sub cursorPos {
	my ($x, $y) = @_;
	!$daemon and 
		$terminal->Tgoto('cm', $x, $y, *STDOUT);
}

#########################################################################################
# Print or log, depending on $silent
#########################################################################################
sub porl {
	porlp(shift, $daemon);
}
#########################################################################################
# porlp :: "Print or log [with] param" - 
#########################################################################################
sub porlp {
	my $msg = shift;
	my $lognotprint = shift;
	if($lognotprint){
		chomp $msg;
		statMsg($msg);
	}
	else{
		print $msg;
	}
}
#########################################################################################
# statOrQ - calls statMsg or enqueues the message, based on the value of $nothread
#########################################################################################
sub statOrQ {
	my $msg = shift;
	if($nothread){
		statMsg($msg);
	}
	else{
		$decMsgQ->enqueue($msg);
	}
}
#########################################################################################
# Adds a status message with timestamp
#########################################################################################
sub statMsg {
	my $str = shift;
	my @t = localtime;
	my $msg = sprintf("%0.2d:%0.2d - %s", $t[2], $t[1], $str);
	my $logtimepart = sprintf("%d-%02d-%02d %0.2d:%0.2d:%0.2d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]);
	push @statusmsgs, $msg;
	if($logfile){
		open LOGFH, ">>" . $logfile or 
				(push @statusmsgs, sprintf("%0.2d:%0.2d:%0.2d - Error writing to log file  %s", $logfile) and return 1); 
		print LOGFH sprintf("%s - %s\n", $logtimepart, $str);
		close LOGFH;
	}
	return 1;
}


#########################################################################################
# Socket send that can handle both SSL and regular socket...
#########################################################################################
sub sockSend {
	my ($sock, $msg) = @_;
	if (ref($sock) eq "IO::Socket::SSL") {
	    $sock->syswrite($msg, undef);
	} 
	else {
		send $sock, $msg, undef;
	}
}


#########################################################################################
# Reads a line from the socket in a blocking manner.
#########################################################################################
sub blockReadLine {
	my $sock = shift;
	my ($line, $buff) = ('', '');
	while(1){
		if (ref($sock) eq "IO::Socket::SSL") {
		    $sock->sysread($buff, 1024);
		}
		else{
			defined(recv($sock, $buff, 1024, 0)) or last;
		}
	    $line .= $buff;
	    last if $line =~ /\r\n$/;
	}
	return $line;
}


#########################################################################################
# Gracefully close down all server connections.
#########################################################################################
sub disconnectAll {
	foreach my $i (1..$connct){
		my $sock = $conn[$i-1]->{'sock'}, 
		print "Closing down connection #$i...";
		not $sock and print "(already closed)\n" and next;

		sockSend($sock, "QUIT\r\n");

		my $line = blockReadLine($sock);
		$line =~ /^205/ and print "closed gracefully!";
		print "\n";
		if (ref($sock) eq "IO::Socket::SSL") {
		    $sock->shutdown( 2 );
		    $sock->close( SSL_no_shutdown => 1);
		} 
		else {
		    #$sock->close;
		    close($sock);
		}
		$conn[$i-1]->{'sock'} = undef;
	}
}


#########################################################################################
# human readable time value (from seconds)
#########################################################################################
sub hrtv {
	my $sec = shift;
	if($sec < 60){
		return $sec . "s";
	}
	my $h = int($sec/(60*60));
	my $m = int(($sec - ($h*60*60))/60.0);
	my $s = $sec - ($h*60*60) - ($m*60);
	if($h){
		return sprintf("%02d:%02d:%02d", $h, $m, $s);
	}
	else{
		return sprintf("%02d:%02d", $m, $s);
	}
}

#########################################################################################
# human readable size value
#########################################################################################
sub hrsv {
	my $size = shift;  # presumed bytes
	my $k = 1.0*$size/1024;
	my $m = 1.0*$size/(1024*1024);
	my $g = 1.0*$size/(1024*1024*1024);
	if($g > 1){
		return sprintf("%0.2fG", $g);
	}
	if($m > 1){
		return sprintf("%0.2fM", $m);
	}
	if($k > 1){
		return sprintf("%dk", $k);
	}
	return sprintf("%0.2f", $size);
}

#########################################################################################
# read password without echoing it
#########################################################################################
sub readPassword {
	ReadMode 2;	# no echo
	my $pw = <STDIN>;
	chomp $pw;
	ReadMode 0; # default
	print "\n";
	return $pw;
}

#########################################################################################
# Determines the total file size for all segments in the NZB file
#########################################################################################
sub computeTotalNZBSize {
	my @fileset = @_;
	my $tot = 0;
	foreach my $file (@fileset){
		foreach my $seg (@{$file->{'segments'}}){
			$tot += $seg->{'size'};
		}
	}
	return $tot;
}
#########################################################################################
# Parse NZB file and return array of files
# TODO: The structure returned from this function should really be documented....but
# for now, if you need it, use Dumper to view the format.  Should be self explanatory.
#########################################################################################
sub parseNZB {
	my $nzbfilename = shift;
	my $lognoprint = shift;
	$nzbfiles{basename($nzbfilename)}->{'read'} = 1;
	my $nzbdir = derivePath($nzbfilename);
	if ((defined($dlpath)) and (defined($dlcreate))) {
	    my $nzbbase = basename($nzbfilename);
	    if ($nzbbase =~ /msgid_[0-9]*_(.*).nzb/) {
			# Filter name from NewzBin style names
			$nzbdir = $1 . "/";
	    } 
		elsif ($nzbbase =~ /(.*).nzb/) {
			# Strip the .nzb extension and 
			$nzbdir = $1 . "/";
	    } 
		else {
			# Just use the nzb file as a directory itself
			$nzbdir = $nzbbase . "/";
	    }
	}
	my $parser = new XML::DOM::Parser;
	my @fileset;
	porlp("Loading and parsing nzb file: " . $nzbfilename . "\n", $lognoprint);
	my $nzbdoc;
	eval {
		$nzbdoc = $parser->parsefile($nzbfilename);
	};
	if($@){
		my $errmsg = $@;
		$errmsg =~ s/^\s+//; $errmsg =~ s/\s+$//;
		if($lognoprint){
			statMsg("The nzb file is BROKEN and the XML could not be parsed.");
		}
		else{
			pc("\n");
			pc(" Sorry, but nzb file is broken!  The xml could not be parsed:\n", 'bold yellow');
			pc("\n");
			pc(" $errmsg\n\n", 'bold yellow');
			pc(" *** nzbperl requires valid, well-formed XML documents.\n\n", 'bold red');
		}
		return undef;
	}

	my $files = $nzbdoc->getElementsByTagName("file");
	my $totalsegct = 0;
	foreach my $i (0..$files->getLength()-1){
		my $fileNode = $files->item($i);
		my $subj = $fileNode->getAttributes()->getNamedItem('subject');
		my $postdate = $fileNode->getAttributes()->getNamedItem('date');

		my %file;
		$file{'nzb path'} = $nzbdir;
		$file{'name'} = $subj->getValue();
		$file{'date'} = $postdate->getValue();

		my @groupnames;
		for my $group ($fileNode->getElementsByTagName('group')) {
			push @groupnames, $group->getFirstChild()->getNodeValue();
		}
		$file{'groups'} = \@groupnames;

		my @segments;
		for my $seg ($fileNode->getElementsByTagName('segment')) {
			my %seghash;

			my $size = $seg->getAttributes()->getNamedItem('bytes')->getValue();
			$file{'totalsize'} += $size;

			my $segNumber = $seg->getAttributes()->getNamedItem('number')->getValue();

			$seghash{'msgid'} = $seg->getFirstChild()->getNodeValue();
			$seghash{'size'} = $size;
			$seghash{'number'} = $segNumber;

			$segments[$segNumber-1] = \%seghash;
		}
		$totalsegct += scalar @segments;
		$file{'segments'} = \@segments;

		push @fileset, \%file;
	}
	$nzbdoc->dispose;

	porlp("Loaded $totalsegct total segments for " . $files->getLength() . " file(s).\n", $lognoprint);
	return @fileset;
}

#########################################################################################
# Filters out files if there is a filter regex, and skips over files from --skip <n>
#########################################################################################
sub regexAndSkipping {
	if(defined($filterregex)){
		print "Filtering files on regular expression...\n";
		my $orgsize = scalar @fileset;
		my @nset;
		while(scalar(@fileset) > 0){
			my $f = shift @fileset;
			if( $f->{'name'} =~ /$filterregex/){
				push @nset, $f;
			}
		}
		if(scalar @nset < 1){
			pc("\nWhoops:  Filter removed all files (nothing left)...aborting!\n\n", 'bold yellow') and exit 0;
		}
		printf("Kept %d of %d files (filtered %d)\n", scalar(@nset), $orgsize, $orgsize-scalar(@nset));
		@fileset = @nset;
	}

	if($skipfilect){
		if($skipfilect >= scalar @fileset){
			pc("\nWhoops:  --skip $skipfilect would skip ALL " . scalar @fileset . 
					" files...aborting!\n\n", 'bold yellow') and exit 0;
		}
		print "Removing $skipfilect files from nzb set (--skip $skipfilect)\n";
		while($skipfilect > 0){
			shift @fileset;
			$skipfilect--;
		}
	}
	
	return @fileset;
}

#########################################################################################
# Sorts files in the fileset based on the name
#########################################################################################
sub sortFilesBySubject {
	if(!$nosort){
		print "Sorting files by filename (subject)...";
		@fileset = 
			sort {
				$a->{'name'} cmp $b->{'name'};
			} @fileset;
		print "finished.\n";
			
	}
}
#########################################################################################
# Derives a path from a filename (passed on commandline).
# The result isn't necessarily absolute, can be relative
#########################################################################################
sub derivePath {
	my $filename = shift;
	if($filename =~ /\//){		# then it has path information, likely not windows compat
		$filename =~ s/(^.*\/).*/\1/;
		return $filename;
	}
	return './';
}
#########################################################################################
# Main entry point for NZB file sanity checking
#########################################################################################
sub doNZBSanityChecks(){
	print "Analyzing sanity of NZB file segment completeness...\n";
	@suspectFileInd = getSuspectFileIndexes();
	my $badfilect = scalar @suspectFileInd;
	not $badfilect and pc("All files pass segment size sanity checks!  Swell.\n", 'bold green') and return;

	SMENUDONE:
	while(1){
		pc(sprintf("There are %d of %d files that may have missing or broken segments.\n", $badfilect, scalar @fileset), 'bold yellow');
		pc("It is likely that these files will be unusable if downloaded.\n", 'bold yellow');
		($dropbad or $insane) and return;	# User selection not needed.
		print "\n How do you want to proceed?\n\n";
		print " k)eep everything and try all files anyway (--insane)\n";
		print " d)rop files suspected broken (--dropbad)\n";
		print " v)iew gory details about broken segments\n";
		print " q)uit now\n";
		print "\n -> ";
		while(1){
			my $char;
			if(defined ($char = getch()) ) {	# have a key
				print "\n";
				if($char =~ /q/){
					exit 1;
				}
				elsif($char =~ /k/){
					print "Setting --insane option...\n";
					$insane = 1;
					last SMENUDONE;
				}
				elsif($char =~ /d/){
					print "Setting --dropbad option...\n";
					$dropbad = 1;
					last SMENUDONE;
				}
				elsif($char =~ /v/){
					showSuspectDetails(@suspectFileInd);
				}
				last;
			}
			else{
				select undef, undef, undef, 0.1;
			}
		}
	}
}

#########################################################################################
# Shows details about suspect files...
#########################################################################################
sub showSuspectDetails {
	my @susFileInd = @_;
	foreach my $fileind (1..scalar @susFileInd){
		my $file = @fileset[$susFileInd[$fileind-1]];
		my $avgsize = avgFilesize($file);
		print "------------------------------------------------------\n";
		printf(" * File: %s\n", $file->{'name'});
		printf("   Posted on: %s (%d days ago)\n", 
				scalar localtime $file->{'date'},
				(time - $file->{'date'})/(60*60*24) );
		printf("   Adjusted average part size = %d bytes\n", $avgsize);
		my @sids = getSuspectSegmentIndexes($file, $avgsize);
		foreach my $si (@sids){
			my $seg = @{$file->{'segments'}}[$si];
			my $percdiff = 100;
			$avgsize and $percdiff = 100*(abs($seg->{'size'} - $avgsize)/$avgsize);
			printf("      Part %d : %d bytes (%.2f%% error from average)\n",
					$si+1, $seg->{'size'}, $percdiff);
		}
	}
	print "------------------------------------------------------\n";
}

#########################################################################################
# Looks at the fileset and returns an array of file indexes that are suspect
#########################################################################################
sub getSuspectFileIndexes {
	my @ret;
	foreach my $fileind (1..scalar @fileset){
		my $file = @fileset[$fileind-1];
		my $avg = avgFilesize($file);
		#printf("File has average size = %d\n", $avg);
		my $segoffct = 0;

		my @suspectSegInd = getSuspectSegmentIndexes($file, $avg);
		if(scalar @suspectSegInd){
			push @ret, $fileind-1;
		}
	}
	return @ret;
}

#########################################################################################
sub getSuspectSegmentIndexes {
	my $MAX_OFF_PERC = 25;		# Percentage of segment size error/diff to trigger invalid
	my ($file, $avg) = @_;
	my @ret;
	#print "DEBUG: File subject = " . $file->{'name'} . "\n";
	#print "DEBUG: It has " . (scalar @{$file->{'segments'}}) . " parts\n";
	foreach my $i (1..(scalar @{$file->{'segments'}}-1)){  # Last segment is allowed to slide...
		my $seg = @{$file->{'segments'}}[$i-1];
		my $percdiff = 100;
		$avg and $percdiff = 100*(abs($seg->{'size'} - $avg)/$avg);
		#printf("   seg $i of %d is %0.2f off avg [%d versus %d (avg)]\n", scalar @{$file->{'segments'}}, $percdiff, $seg->{'size'}, $avg);
		if($percdiff > $MAX_OFF_PERC){
			push @ret, $i-1;
		}
	}
	return @ret;
}

#########################################################################################
sub dropSuspectFiles(){ my @newset; my $dropct = 0; foreach my $i (0..scalar @fileset-1){
		if($i == $suspectFileInd[0]){
			my $ind = shift @suspectFileInd;
			my $file = @fileset[$ind];
			printf("Dropping [%s] from filset (suspect)\n", $file->{'name'});
			$dropct++;
			next;
		}
		push @newset, shift @fileset;
	}
	@fileset = @newset;
	pc(sprintf("Dropped %d suspect files from NZB (%d files remain)\n", $dropct, scalar @fileset), 'bold yellow');
	print " -> short delay (for user review)";
	foreach(5,4,3,2,1){
		print "...$_";
		sleep 1;
	}
	print "...let's go!\n";
}

#########################################################################################
# Not a true average, but an average of all segments except the last one...
# ...unless there's only one segment, in which case it's the segment size.
#########################################################################################
sub avgFilesize {
	my $file = shift;
	my @segs = @{$file->{'segments'}};
	return $segs[0]->{'size'} unless scalar @segs > 1;
	my ($sum, $ct) = (0, 0);
	foreach my $i (1..scalar(@segs)){
		my $seg = @segs[$i-1];
		last unless $i < scalar(@segs);
		$ct++;
		$sum += $seg->{'size'};
	}
	return $sum*1.0/($ct*1.0);
}

#########################################################################################
# Parse command line options and assign sane globals etc.
#########################################################################################
sub handleCommandLineOptions {
	my @saveargs = @ARGV;

	GetOptions(%optionsmap);
	
	my $errmsg;
	# This is the facility for trapping stderr from GetOptions, so that we 
	# can pretty print it at the bottom of the help screen.
	local $SIG{'__WARN__'} = sub {
		$errmsg = $_[0];
		chomp $errmsg;
	};

	# First see if the config file is there, if so, slurp options from it.
	my $optionsAreOk;
	if(-e $configfile){
		readConfigFileOptions();
		$optionsAreOk = eval 'GetOptions(%optionsmap)';
		return $errmsg unless $optionsAreOk;
	}
	else {
		print "Config file $configfile does not exist.  Skipping.\n";
	}
	
	# Now restore the commandline args and parse those (overriding config file options)
	@ARGV = @saveargs;	# restore
	$optionsAreOk = eval 'GetOptions(%optionsmap)';
	return $errmsg unless $optionsAreOk;
	if($help){
		return "";
	}
	$nocolor and $usecolor = 0;

	not $optionsAreOk and return "";

	if(!$nothread){
		use threads;
		use Thread::Queue;
	}

	if(not $ARGV[0] and (not defined $queuedir)){		# No NZB file given?
		return "Missing nzb file or directory queue.";
	}

	if(not length($server)){
		$server = $ENV{'NNTPSERVER'};
		not $server and return "Must provide --server or set \$NNTPSERVER environment";
	}
	if($server =~ /:\d+$/){
		$port = $server;
		$port =~ s/.*://;
		$server =~ s/:.*//;
	}

	#$dlpath = './' unless defined($dlpath);
	$dlpath = cwd unless defined($dlpath);

	# Make sure that dlpath ends with a slash
	$dlpath and (not ($dlpath =~ /\/$/)) and ($dlpath .= '/');

	if($dropbad and $insane){	# conflicting
		return "Error: --dropbad and --insane are conflicting (choose one)";
	}

	if(defined($queuedir) and !$dropbad and !$insane){
		return "Use of --queuedir requires either --dropbad or --insane.\n" . 
				" Please choose one and try again.";
	}

	if(defined($postDecProg) and not -e $postDecProg){
		return "--postdec program \"$postDecProg\" does not exist.\n" . 
				" Please confirm the program and try again.";
	}

	if($dlpath and $dlrelative){ # conflicting options
		return "Error: --dlrelative and --dlpath <dir> are conflicting (choose one)";
	}

	if($port == -1) {
	    if (defined($ssl)) {
			(undef, undef, $port, undef) = getservbyname("nntps", "tcp");
	    } 
		else {
			(undef, undef, $port, undef) = getservbyname("nntp", "tcp");
	    }
	}

	if(defined($socks_server) and defined($http_proxy_server)){
		return "Error: --socks_server and --http_proxy are conflicting (choose one)";
	}
	if(defined($dlcreate) and defined($dlcreategrp)){
		return "Error: --dlcreate and --dlcreategrp are conflicting (choose one)";
	}

	if (defined($ssl)) {
	    eval "use IO::Socket::SSL;";		# use module only if option is enabled.
		($@) and return "ERROR: --ssl was specified, but IO::Socket::SSL isn't available.\r\n" . 
						" Please install IO::Socket::SSL to use --ssl and try again.";
	}

	if (defined($socks_server)) {	
	    eval "use IO::Socket::Socks;"; 		# use module only if option enabled 
		($@) and return "ERROR: --socks_server was specified, but IO::Socket::Socks isn't available.\r\n" . 
						" Please install IO::Socket::Socks to use a SOCKS server and try again.";

	    if ($socks_port == -1) {
			if($socks_server =~ /:\d+$/){
				$socks_port = $socks_server;
				$socks_port =~ s/.*://;
				$socks_server =~ s/:.*//;
			} 
			else {
				(undef, undef, $socks_port, undef) = getservbyname("socks", "tcp");
			}
	    }
	}

	if (defined($http_proxy_server)) {
	    eval "use Net::HTTPTunnel;";		# use module only if option enabled
		($@) and return "ERROR: --http_proxy was specified, but Net::HTTPTunnel isn't available.\r\n" . 
						" Please install Net::HTTPTunnel to use an HTTP proxy and try again.";

		if($http_proxy_server =~ /:\d+$/){
			$http_proxy_port = $http_proxy_server;
			$http_proxy_port =~ s/.*://;
			$http_proxy_server =~ s/:.*//;
		} 
		else {
			(undef, undef, $http_proxy_port, undef) = getservbyname("webcache", "tcp");
		}
	}

	if(defined($ipv6)){
	    eval "use IO::Socket::INET6;";		# use ipv6 module if option given
		($@) and return "ERROR: --ipv6 was given and the IO::Socket::INET6 module could not be found.\r\n" . 
						" You must install the IO::Socket::INET6 module to use IPv6";
	}

	return undef;	# success
}

# Helper to detect that uudeview is installed.  Always a good idea, ya'know, since we're
# dependant on it!
sub haveUUDeview {
	if(defined($uudeview)){	# Given on commandline or config file
		if(-e $uudeview){
			pc("uudeview found: $uudeview\n", "bold green");
			return 1;
		}
		pc("Warning: uudeview not found at location $uudeview\n", "bold yellow");
	}
	my @paths = split /:/, $ENV{'PATH'};	# path sep different on winderz?
	foreach my $p (@paths){
		$p =~ s/\/$//;
		$p = $p . "/uudeview";
		if(-e $p){
			pc("uudeview found: $p\n", "bold green");
			$uudeview = $p;
			return 1;
		}
	}
	pc("Error: uudeview not found in path...aborting!\n", "bold red");
	return 0;
}

#########################################################################################
# Reads options from the config file and tucks them into @ARGV, so that they all
# look like they were passd on the commandline.  So, when this returns (successfully),
# ARGV contains the config file contents.  ARGV must be preserved externally.
#########################################################################################
sub readConfigFileOptions(){
	print "Reading config options from $configfile...\n";
	open CFG, "<$configfile" or die "Error opening $configfile for config options";
	my $line;
	my @opts;
	while($line = <CFG>){
		chomp $line;
		$line =~ s/^\s+//;
		$line =~ s/^-+//;	# In case dashes in config file
		next if $line =~ /^#/;
		next unless length($line);
		push @opts, "--$line";
	}
	close CFG;
	@ARGV = @opts;
}
#########################################################################################
# Checks for a newer version, disabled with --noupdate
#########################################################################################
sub checkForNewVersion {
	$noupdate and return;	# they don't want update checking
	print "Checking for availability of newer version...\n";
	eval "use LWP::Simple;";
	if($@){
		print "LWP::Simple is not installed, skipping up-to-date check.\n";
		return;
	}
	my $remote_ver = eval "get \"$UPDATE_URL\"";
	chomp $remote_ver;

	if($remote_ver eq $version){
		print "Look like you're running the most current version.  Good.\n";
	}
	else{
		pc("A newer version of nzbperl is available: ", 'bold red');
		pc('version ' . $remote_ver . "\n", 'bold white');
		pc("You should consider downloading it from ", 'bold white');
		pc("http://noisybox.net/computers/nzbperl/\n", 'bold yellow');
		pc("This delay is intentional: ");
		foreach(1..8){
			print "..." . (9-$_);
			sleep 1;
		}
		pc("\n");
	}

}

#########################################################################################
sub displayShortGPL {
	print <<EOL

  nzbperl version $version, Copyright (C) 2004 Jason Plumb
  nzbperl comes with ABSOLUTELY NO WARRANTY; This is free software, and 
  you are welcome to redistribute it under certain conditions;  Please 
  see the source for additional details.

EOL
;
}

#########################################################################################
# Shows a help screen for interactive keys
#########################################################################################
sub showHelpScreen {
	clearScreen();
	print <<EOL

  Hi.  This is the nzbperl help screen. 
  You can use the following keys while we're running:

  '1'   : Switch to low bandwidth mode ($lowbw kBps)
  '2'   : Switch to med bandwidth mode ($medbw kBps)
  '3'   : Switch to high bandwidth mode (unlimited)
  '+'   : Nudge target bandwidth setting up 1 kBps
  '-'   : Nudge target bandwidth setting down 1 kBps
  'c'   : Toggle color on or off
  'q'   : Quit the program (aborts all downloads)
  '?'   : Show this help screen

  Connected to $server:$port
  (Your download is still in progress:  
  
  [ Press any key to return to the main screen ]

EOL
;
	drawVLine(0, 17);
	drawVLine($wchar, 17);
	drawHLine(0, 'top');
	drawHLine(17, 'bottom');

	cursorPos(40, 14);
	pc("ETA: " . getETA(), 'bold green');
	pc(")", 'bold white');
	$showinghelpscreen = 1;
}

#########################################################################################
# Show program usage
#########################################################################################
sub showUsage {
my $errmsg = shift;
print <<EOL

  nzbperl version $version -- usage:

  nzbperl <options> <file1.nzb> ... <file.nzb>

  where <options> are:

 --config <file>   : Use <file> for config options (default is ~/.nzbperlrc)
 --server <server> : Usenet server to use (defaults to NNTPSERVER env var)
                   : Port can also be specified with --server <server:port>
 --user <user>     : Username for server (blank of not needed)
 --pw <pass>       : Password for server (blank to prompt if --user given)
 --conn <n>        : Use <n> server connections (default = 2)
 --ssl             : Connect to server using SSL (secure sockets layer).
                   : May be combined with --http_proxy or --socks_server to 
                   : use a proxy server with SSL.
 --socks_server <s>: Connect using <s> as a socks proxy server. Defaults to
                   : port 1080, but can use --socks_server <server:port> to
                   : use an alternative port.
 --http_proxy <s>  : Use <s> as an http proxy server to use.  Defaults
                   : to port 8080, but can use --http_proxy <server:port> to
                   : use an alternative port.
 --proxy_user <u>  : Authenticate to the proxy using <u> as the username
 --proxy_passwd <p>: Use <p> as the proxy user password (otherwise prompted)
 --ipv6            : Use IPv6 sockets for communication
 --keepparts       : Keep all encoded parts files on disk after decoding
 --keepbroken      : Continue downloading files with broken/missing segments
                   : and leave the parts files on disk still encoded.
 --keepbrokenbin   : Decode and keep broken decoded files (binaries) on disk.
 --dlpath <dir>    : Download and decode all files to <dir>  
                   : (default downloads to current dirctory)
 --dlrelative      : Download and decode to the dir that the nzbfiles are in
                   : (default downloads to current directory)
 --dlcreate        : Create download directories per nzb file
 --dlcreategrp     : Create download dirctories with usenet group names
 --queuedir <dir>  : Monitor <dir> for nzb files and queue new ones
 --postdec <prog>  : Run <prog> after each file is decoded, env var params.
 --redo            : Don't skip over existing downloads, do them again
 --insane          : Bypass NZB sanity checks completely
 --dropbad         : Auto-skip files in the NZBs with suspected broken parts
 --skip <n>        : Skip the first <n> files in the nzb (don't process)
 --med <kBps>      : Set "med" bandwidth to kBps (default is 95kBps)
 --low <kBps>      : Set "low" bandwidth to kBps (default is 35kBps)
 --speed <speed>   : Explicitly specify transfer bandwidth in kBps
 --log <file>      : Log status messages into <file> (default = none)
 --nothread        : Don't decode files in a thread (single threaded operation)
                   : (Note: Causes downloads to be paused during file decoding)
 --daemon          : Run in background as daemon (use log for status)
 --rcport <port>   : Enable remote control functionality on port <port>
 --retrywait <n>   : Wait <n> seconds between reconnect tries (default = 300)
 --nosort          : Don't sort files by name before processing
 --filter <regex>  : Filter NZB contents on <regex> in subject line
 --uudeview <app>  : Specify full path to uudeview (default found in \$PATH)
 --nocolor         : Don't use color
 --noansi          : Don't use ANSI characters (text only)
 --noupdate        : Don't check for newer versions at startup
 --help            : Show this screen

  During runtime, press 'h' or '?' to see a list of key commands.

  nzbperl version $version, Copyright (C) 2004 Jason Plumb
  nzbperl comes with ABSOLUTELY NO WARRANTY; This is free software, and 
  you are welcome to redistribute it under certain conditions;  Please 
  see the source for additional details.

EOL
;
if($errmsg and (length($errmsg))){
	print " *****************************************************************\n";
	print " ERROR:\n";
	print " $errmsg\n";
	print " *****************************************************************\n";
}

}

