#!/usr/bin/perl -w
use strict;
# Copyright (c) 2002/2003 Eike Frost (btrackalyzer@kefro.st)
# This is Free Software under the terms of the General Public License (GPL).

# Of course donations never go amiss, but that's up to you ;)

# This program parses a BitTorrent tracker logfile and extracts various
# data from it. It's neither perfect nor pretty.

# Oh, and it's sufficiently slow, too.

# $Id: trackerlyze.pl,v 1.18 2003/09/06 06:07:10 eike Exp $

# Homepage : http://ei.kefro.st/projects/btrackalyzer/

use Time::Local;
use POSIX qw(strftime);
use Storable;

# Some configuration settings
my $reannounce_interval_max = 30*60; # This is the reannounce interval set; controls backdrift
my $checkpoint_interval = 5*60;      # How often the graph is updated
my $timeout_downloaders_interval = 45*60; # after this much time, we stop considering peers
my $drop_downloaders_interval = 800*60; # after this much time, we drop peers
my $drop_overdue_stats = 1;          # If stats don't fit into writestats_time_back, drop previous data
my $writestats_time_back = 30*60;    # only generate stats for data this old
my $reannounce_interval_avg_samples = 5000; # do the stats on the last X sampls
my $do_reannounce_stats = 0;         # do reannounce stats. SLOW.
my $reannounce_age = 0;              # 0 = consider ALL reannounces, 1 = only past $checkpoint_interval
my $display_file_stats = 0;          # display statistics while running
my $fetchnamesize = 0;               # Try to fetch filenames/sizes from IO
my $verbose = 1;                     # Be verbose on errors & current status
my $always_do_filestats = 0;         # do filestats even if the data isn't current. SLOW !
my $always_do_peakstats = 1;         # do peakstats on historic data. SLOWER.
my $autosaveperiods = 36;            # Save state every X graphing intervals
my $printstateperiods = 72;          # Print state every X processed intervals
my $allowpeerstats = 1;              # Allow complete peerstats on per-file basis
my $alwaysgraph = 0;                 # Graph even on historic data (SLOW !)

my $statefilename = 'btrackalyze.state';

#my $io = new trackalyze::IO::file   # Object to fetch/save stats to files
#    ({statusdir => './status',      # Directory to put data into
#      connectionstats => 0,         # Save connection duration stats
#      completionstats => 0,         # Save completion duration stats
#      peerstats => 0,               # Do/Save peerstats
#     });

my $io = new trackalyze::IO::sql      # use this for SQL
   ({dbname => 'btrackalyzer',        # Database to use
     dbuser => 'btrackalazer',        # Database username
     dbpass => 'secret',              # Database Password
     statstable => 'btstats',         # Database Statistics table
     misctable => 'btmisc',           # Database Miscellaneous stats table
     peerstatstable => 'btpeerstats', # Table for Peerstats, if desired
     metadatatable => 'btmetadata',   # Table for supplying metadata
     peerstats => 0,                  # Do/Save peerstats
    });

#my $io = new trackalyze::IO::multiplex  # Of course you can also use more
#        (new trackalyze::IO::file,      # than one module if your heart so
#         new trackalyze::IO::sql);      # desires / your machine handles it.

#my $io = new trackalyze::IO::Dummy;     # This blackholes statswriting

my $graph = new trackalyze::Graph::RRDs    # Shared rrdtool for graphing
   ({rrd => 'torrent.rrd',                 # RRDatabase to use
     outputdir => './graphs',              # Output directory (graphs)
     graphtitle => 'BitTorrent tracker -', # Title prefix
     });

#my $graph = new trackalyze::Graph::rrdtool  # External rrdtool for graphing
#   ({rrd => 'torrent.rrd',                  # RRDatabase to use
#     outputdir => './graphs',               # Output directory (graphs)
#     graphtitle => 'BitTorrent tracker - ', # Title prefix
#     useexternal => './graph.sh'});         # Use external script for graphing?

#my $graph = new trackalyze::Graph::Dummy;   # This blackholes graphing


# Below this comes non-configurable stuff ...

my (%peers, %files);

# Variables to be used

my $inittime           =          # date/time of first read line
my $upped              =          # graphed data (usually < transferred data)
my $lasttime           =          # date of last read line
my $totalmaxconnected  =          # maximum of connected users of all time
my $totalcompleted     =          # Total completed downloads thus far
my $terminate          =          # terminate the loop ?
my $proclines          =          # Processed lines in this run
my $parts              =          # counter of intervals processed
my $finishedpeers      =          # peers we have served
my $totalconnected     =          # currently connected peers
my $totalleechers      =          # Total Leechers
my $totalseeds         =          # Total Seeds
my $totalgzip          =          # Total GZIP-encoded announces
my $totalannounces     =          # Total Announces
my $totalannouncesize  =          # Total size of answered announces
my $totalscrapes       =          # Total scrapes
my $totalscrapesize    =          # Total scrapes' size
my $totalothersize     =          # Total size of all other responses
my $firsttime          =          # Timestamp of first log-line
my $showstate          =          # Show current status on next line
my $nextgraph          =          # Create a Graph at next opportunity
my $nextfilestats      =          # Write out Filestats at next opportunity
my $nosave             =          # Save state at end ?
   0;

# Timeslots to consider
my $nslots = int($reannounce_interval_max / $checkpoint_interval);
my @slots = ();

# for reannounce interval stats
my %reannounce;
$reannounce{number} = 0;
$reannounce{current} = 0;

# Slots start out empty
for (my $i = $nslots-1; $i >=0 ; $i--) {
    $slots[$i]{timestamp} = 0;
    $slots[$i]{upped} = 0;
}

loadruntime ();

# Variables used in each iteration -- declared here.
my ($ip, $gzip, $timestamp, $getrequest, $fgetrequest, $getmethod, $getparam,
    $statuscode, $size, $tday, $tmonth, $tyear, $thour, $tminute, $tsecond, 
    $tadjust, $line, %r);

# Should we catch a sigint, stop after processing the current line
$SIG{INT}  = sub { $terminate++; };
$SIG{HUP}  = sub { $showstate = 1; };
$SIG{USR1} = sub { $nextgraph = 1; };
$SIG{USR2} = sub { $nextfilestats = 1; };

# To convert from HTTP Log Timestamps to real timestamps
my %months = ('Jan'=>0,'Feb'=>1,'Mar'=>2,'Apr'=>3,'May'=>4,'Jun'=>5,
              'Jul'=>6,'Aug'=>7,'Sep'=>8,'Oct'=>9,'Nov'=>10,'Dec'=>11);

my $lineregex = qr/
               ((?:\d{1,3}\.){3}\d{1,3})\s # IP                  $1
               ([^ ]+)\s [^ ]+\s           # Ident, Username     $2
               \[([^\]]+)\]\s              # Timestamp           $3
               \"([^"\s]+)\s               # Method              $4
                 ([^"\s]+)\s               # Request             $5
                 ([^"]+)"\s                # Parameters          $6
               (\d)+\s                     # Status Code         $7
               (\d)+                       # Size returned       $8
              /x;

my $begin = time;

if (defined $ARGV[0]) {
  if ($ARGV[0] eq '--writeout') {
      $terminate = 1;
      $nosave = 1;
      $always_do_filestats = 1;

      $nextfilestats = 1;
      $graph->graph ($lasttime - $reannounce_interval_max);
      filespeeds ();
      filestats ();    
  }
}

while ((! $terminate) and ($line = <STDIN>)) {
    # Split the line into its parts

    ($line =~ m/$lineregex/o) or next;

    my ($ip, $gzip, $timestamp, $getmethod, $fgetrequest, $getparam,
        $statuscode, $size) = ($1, $2, $3, $4, $5, $6, $7, $8);

    # Convert timestamp. This used to be done by Date::Parse, which is
    # way too slow in this case.
    eval {
      ($tday, $tmonth, $tyear, $thour, $tminute, $tsecond, $tadjust) = 
          unpack "A2xA3xA4xA2xA2xA2", $timestamp;
      $timestamp = timegm ($tsecond, $tminute, $thour, $tday, $months{$tmonth}, $tyear);
    };

    if ($@) { 
        print "The previous error message was due to a malformed line ".
              "but is non-fatal..\n";
        print "Line in question : $line\n";
        next; 
    }

    if ($timestamp < $lasttime) {
    	next;
    }

    # we only process announces in the further lines. If any other other
    # lines are to be processed, process them here.

    (undef, $getrequest) = split '/announce\?', $fgetrequest, 2;
    unless (defined $getrequest) {
        (undef, $getrequest) = split '/scrape', $fgetrequest, 2;
        if (defined $getrequest) {
            $totalscrapes++;
            $totalscrapesize+=$size;
        } else {
            if (defined $size) {
                $totalothersize+=$size;
            }
        }
        next;
    }

    my @r = map { split '=' } split '&', $getrequest;
    if (scalar @r % 2 == 0) { %r = @r; } else { next; }

    # test whether needed parameters have been given
    (defined $r{peer_id}) and (defined $r{uploaded}) and 
    (defined $r{downloaded}) and (defined $r{left}) and
    (defined $r{info_hash}) or next;

    # let's do some sanity checks
    
    (! $r{peer_id} =~ /%/) and next; 
    (! $r{info_hash} =~ /%/) and next;

    # Convert hashes into readable form
    $r{info_hash} =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge; 
    $r{info_hash} = unpack ("H*", $r{info_hash});
    $r{peer_id} =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge; 
    $r{peer_id} = unpack ("H*", $r{peer_id});

    # let's do some more sanity checks
     
    (length ($r{peer_id}) != 40) and next;
    (length ($r{info_hash}) != 40) and next;

    # Make sure numbers really ARE numbers ...

    $r{uploaded} =~ tr/0-9//cd;
    $r{downloaded} =~ tr/0-9//cd;
    $r{left} =~ tr/0-9//cd;

    # Count announces and gzips

    ($gzip eq 'gzip') and $totalgzip++;
    $totalannounces++;
    $totalannouncesize += $size;
                   
    # Sometimes, the client submits its own IP instead of the originating host
    if (defined $r{$ip}) {
      $ip = $r{$ip};
    }    
    
    # If we don't really know this peer (yet), create some empty hashes 
    (! exists $peers{$r{peer_id}}) and createpeerentry ($r{peer_id}, 
                                       $timestamp, $r{info_hash});
    (! exists $files{$r{info_hash}}) and createfileentry ($r{info_hash}, $timestamp);

    # Set some data about the peer from the current line
    $peers{$r{peer_id}}{ip} = $ip;
    $peers{$r{peer_id}}{left} = $r{left};        

    # Did this announce contain an event ? If so, handle it accordingly
    if (defined $r{event}) {
        if ($r{event} eq 'started') {
            ($r{downloaded} > 0) and next;
            $files{$r{info_hash}}{started}++; 
            $peers{$r{peer_id}}{started} = $timestamp;
        } elsif ($r{event} eq 'stopped') {
            $peers{$r{peer_id}}{stopped} = $timestamp;
        } elsif ($r{event} eq 'completed') {
            ($r{left} > 0) and next;
            $files{$r{info_hash}}{completed}++; 
            $peers{$r{peer_id}}{completedat} = $timestamp;
            $totalcompleted++;
        }
    }

    # Grab some info about files/torrents from the line and take note of them
    $files{$r{info_hash}}{lastseen} = $timestamp;

       
    # So, how do the transfers look ?
    if (defined $peers{$r{peer_id}}{up}) {
        my $diff = $r{uploaded} - $peers{$r{peer_id}}{up};
       
        # some sanity limits
        if (($diff > 0) and ($diff < 2**32*$nslots)) {
            # The traffic difference is over this long a timespan ...
            my $timediff = $timestamp - $peers{$r{peer_id}}{lastseen};

            # ... so we distribute it over this many timeslots ...
            my $takeslots = int ($timediff / $checkpoint_interval);
            ($takeslots < 1) and $takeslots = 1;
	    ($takeslots > $nslots) and (! $drop_overdue_stats) and $takeslots = $nslots;

            # ... which gives us this much per timeslot
	    my $taketraffic = int ($diff / $takeslots);
            # sanity-check, again        
            if (! ($taketraffic > 2**32)) { 
                # increment each slot
                for (my $i = $nslots-1 ; ($i > $nslots-1-$takeslots) and ($i >= 0); $i--) {
                    $slots[$i]{upped} += $taketraffic;
                }
                # Note the difference for the file
                $files{$r{info_hash}}{up} += $diff;
            } else {
                # Speed is way higher than anticipated; either fake or errors
                # note that this can and will also happen when starting
                # in the middle of a log ...
                if (($diff > 0) and ($verbose)) {
                    print "HUGE traffic : diff : $diff, peer : $r{peer_id}, " .
                           "ip : $ip, time : $timestamp; " .
                           "Logline : \n $line \n";
                }
            }
        }
    } else {
        # This is a new peer (?)
        if (($peers{$r{peer_id}}{firstseen} == $timestamp) and 
            ((! defined $r{event}) or ($r{event} ne 'started'))) {
            print "Not counting record from unknown peer that has not\n",
                  "\"started\"; This is normal if starting in the middle.\n";
        } else {
            if (($r{uploaded} < 2**32) and ($r{uploaded} > 0)) {
                $slots[$nslots-1]{upped} += $r{uploaded};
                $files{$r{info_hash}}{up} += $r{uploaded};

            } else {
                if (($r{uploaded} > 0) and ($verbose)) {
                    print "HUGE traffic (disregard if starting in the middle " . 
                          "of a log): diff : $r{uploaded}, " .
                           "peer : $r{peer_id}, ip : $ip, time : " .
                           "$timestamp; Logline : \n $line \n";
                }
            }
        }
    }
    
    # If we have seen this peer before, it's a reannounce
    if (exists $peers{$r{peer_id}}{lastseen}) {
	my $announce_interval = $timestamp - $peers{$r{peer_id}}{lastseen};

        # These are done over the trackers life or over the past
        # writeout-interval
        # Create new average
	if ($do_reannounce_stats) {
          $reannounce{current} = ($reannounce{current} * $reannounce{number} 
                                  + $announce_interval);
          $reannounce{current} /= ++$reannounce{number}; #/
        }

        # If the client has been seen < 1 second prior to this, avoid
        # divisions by zero
	if ($announce_interval == 0) { $announce_interval++; }

        # Current upload speed of this peer
	$peers{$r{peer_id}}{speedup} = (($r{uploaded} - $peers{$r{peer_id}}{up}) / $announce_interval);
        if ($peers{$r{peer_id}}{speedup} < 0) { $peers{$r{peer_id}}{speedup} = 0; }
        # Current download speed of this peer
	$peers{$r{peer_id}}{speeddown} = (($r{downloaded} - $peers{$r{peer_id}}{down}) / $announce_interval);
        if ($peers{$r{peer_id}}{speeddown} < 0) { $peers{$r{peer_id}}{speeddown} = 0; }
    } else {
        # We don't know anything about speeds, yet.
	$peers{$r{peer_id}}{speedup} = 0;
	$peers{$r{peer_id}}{speeddown} = 0;
    }
    
    # Some more data we might be interested in, later.
    $peers{$r{peer_id}}{lastseen} = $timestamp;        
    $peers{$r{peer_id}}{up} = $r{uploaded};
    $peers{$r{peer_id}}{down} = $r{downloaded};    
    
    # The graphing magic. Ouch.

    # if we are just starting, set up some stuff
    if ($inittime == 0) {
        $inittime = $timestamp; 
        $firsttime = $timestamp;

        # slots for before the big bang
        for (my $i = 0; $i < $nslots; $i++) {
            $slots[$i]{timestamp} = $timestamp - (($nslots-$i)*$checkpoint_interval);
        }

        $graph->setup ($slots[0]{timestamp} - $checkpoint_interval);
    } else { 
        # else, we may have stuff to graph
        # but only if a checkpoint_interval has passed ...
        if ($inittime + $checkpoint_interval-1 < $timestamp) {
            # ... and as often as is needed to get to the current interval
            while ($inittime + $checkpoint_interval-1 < $timestamp) {
	            $inittime += $checkpoint_interval;
                
                # move slots back one
                my %curslot; # this, we will work on later.
                $curslot{timestamp} = $slots[0]{timestamp};
                $curslot{upped} = $slots[0]{upped};

                for (my $i = 0; $i < $nslots-1; $i++) {
                    $slots[$i]{timestamp} = $slots[$i+1]{timestamp}; 
                    $slots[$i]{upped} = $slots[$i+1]{upped}; 
                }
                $slots[$nslots-1]{timestamp} = $inittime;
                $slots[$nslots-1]{upped} = 0;

                # increase $upped with value from moved-out slot
                $upped += $curslot{upped};
                
                # update the difference in the rrdtool/graphing database
                $graph->update ($curslot{timestamp}, $upped, 
                        $totalconnected, $totalseeds);

                # If there's need for graphing, graph.
                if ($alwaysgraph or $nextgraph or
                    (time - $timestamp < $writestats_time_back)) {
                    $nextgraph = 0;
                    $graph->graph ($timestamp - $reannounce_interval_max); 
                }
                # Give some state information if so instructed
                if (((time - $timestamp < $writestats_time_back) or 
                    ($parts % $printstateperiods == 0)) and $verbose) {
                    $showstate = 1;
                }
                # Just in case, save state information.
    	        if ($parts++ % $autosaveperiods == 0) {
                    saveruntime ();
                }
            }

            # Clean up after dead & dropped peers
            peerclean ();

            # If used, calculate filespeeds & write file  stats
            if ((time - $timestamp < $writestats_time_back) or 
                $always_do_filestats or $always_do_peakstats or
                $nextfilestats) {               
               filespeeds ();
               filestats ();
            }

            # set back reannounce-stats
            if ($reannounce_age) {
               $reannounce{number} = 0;
               $reannounce{current} = 0;
            }
        }
    }
    
    $proclines++;

    # Let's give the user some feedback how far along we are in parsing
    # (showstate is being set to 1 by a sighandler ...)
    if ($showstate == 1) {
        $showstate = 0;

        print "processed $proclines lines in ". readablespan (time - $begin + 1) .
              " (". sprintf ("%.2f", ($proclines / (time - $begin + 1))).
              " lines/s). \n";
        print "current line's timestamp : " . strftime ("%a %b %e %H:%M:%S %Y", 
               localtime ($timestamp)), "\n";
        print "total transferred so far : ", readable($upped), " in (".
              readablespan ($timestamp - $firsttime).")\n";
    }

    # This line is done for, save the time
    $lasttime = $timestamp;
}

# The loop ended; now let's see what final stats we have for this run,
# and clean up after ourselves

print "Total uploaded and graphed   : " . readable($upped) . " ($upped)\n";
print "Final reannounce avg         : " . $reannounce{current} . "\n";
print "Connected clients            : " . $totalconnected . "\n";
print "Clients in recent memory     : " . scalar(keys %peers) . "\n";
print "Log lines in this run        : " . $proclines . "\n";
print "Time taken                   : " . (time () - $begin) . "\n";
print "Lines/sec                    : " . ($proclines / (time - $begin + 1)) . "\n";

unless ($nosave) {
    print "Calculating Final filestats ...\n";
    filestats ();
    print "Saving current runtime state\n";
    saveruntime ();
}

print "All done.\n";
exit 0;

# Taking care of the hashes

# This writes out statistics for files. Clunky, slow.
sub filestats { 

    # Gathering of filenames/sizes
    if ($display_file_stats and $fetchnamesize) {
        foreach my $file (keys %files) {
            if ($files{$file}{filename} eq '') { 
                $files{$file}{filename} = $io->getfilename($file);
            }
            if ($files{$file}{filesize} == 0) { 
                $files{$file}{filesize} = $io->getfilesize($file);
            }
        }
    }

    my @recs;
    # Fetch / deduce statistics for each file 
    foreach my $file (keys %files) {   	    
        my %record;

        # Use the max speed as speed indicator (figures should be close,
        # anyway. Also calculate max.
	$record{speed} = $files{$file}{speedup} > 
                               $files{$file}{speeddown} ? 
                               $files{$file}{speedup} : 
                               $files{$file}{speeddown};
    

        # These peakcalculations are done on always_do_peakstats ...
        if ($record{speed} > $files{$file}{peakspeed}) {
            $files{$file}{peakspeed} = $record{speed};
        }
        if ($files{$file}{clients} > $files{$file}{peakclients}) {
            $files{$file}{peakclients} = $files{$file}{clients};
        }
        if ($files{$file}{seeds} > $files{$file}{peakseeds}) {
            $files{$file}{peakseeds} = $files{$file}{seeds};
        }
        if ($files{$file}{clients} - $files{$file}{seeds} > $files{$file}{peakleechers}) {
            $files{$file}{peakleechers} = $files{$file}{clients} - $files{$file}{seeds};
        }

        # ... no need to write out unless asked to, though.

        if (! ($display_file_stats or (time - $files{$file}{lastseen} < 
            $writestats_time_back) or $always_do_filestats or $nextfilestats)) {
            next;
        }

	$record{speedfmt} = readablemetric ($record{speed}) . "/s";
	$record{peakspeed} = $files{$file}{peakspeed};
	$record{peakspeedfmt} = readablemetric ($files{$file}{peakspeed}) . "/s";

        $record{hash} = $file;
	    
        # Prepare some information from the hashes
        $record{up} = $files{$file}{up};
  	$record{upfmt} = sprintf("%10s", readable($files{$file}{up}));

        $record{completed} = $files{$file}{completed};
        $record{completedfmt} = sprintf ("%6s", $files{$file}{completed});

	$record{firstseen} = $files{$file}{firstseen};
        $record{firstseenfmt} = strftime ("%m%d %H:%M", localtime ($files{$file}{firstseen}));

	$record{lastseen} = $files{$file}{lastseen};
	$record{lastseenfmt} = strftime ("%m%d %H:%M", localtime ($files{$file}{lastseen}));

	$record{span} = $files{$file}{lastseen} - $files{$file}{firstseen};
	$record{spanfmt} = readablespan ($files{$file}{lastseen} - $files{$file}{firstseen});


        $record{peercompleteavg} = $files{$file}{peercomplduraverage};
        $record{peercompletemax} = $files{$file}{peercompldurmax};
        $record{peercompletemin} = $files{$file}{peercompldurmin};
        $record{peercompleteavgfmt} = readablespan ($files{$file}{peercomplduraverage});
        $record{peercompletemaxfmt} = readablespan ($files{$file}{peercompldurmax});
        $record{peercompleteminfmt} = readablespan ($files{$file}{peercompldurmin});

        $record{peerconnavg} = $files{$file}{peerconnduraverage};
        $record{peerconnmax} = $files{$file}{peerconndurmax};
        $record{peerconnavgfmt} = readablespan ($files{$file}{peerconnduraverage});
        $record{peerconnmaxfmt} = readablespan ($files{$file}{peerconndurmax});

        $record{seedsfmt} = pad(3, $files{$file}{seeds}) . "/" . 
                            pad(3, $files{$file}{clients});

        $record{seeds} = $files{$file}{seeds};
        $record{clients} = $files{$file}{clients};
        $record{leechers} = $files{$file}{clients} - $files{$file}{seeds};

        $record{peakseeds} = $files{$file}{peakseeds};
        $record{peakclients} = $files{$file}{peakclients};
        $record{peakleechers} = $files{$file}{peakleechers};
	
        # only do peer statistics if it's actually requested by IO
        if ($allowpeerstats and $io->dopeerstats($file)) {    
            $record{peerstats} = "";
     	    for (my $i = 0; $i < 2; $i++) {
  	        foreach my $peer (@{$files{$file}{peers}}) {
		    if ((defined $peers{$peer}{killed}) and ($i != 1)) {
		        next;
     		    }
	            if ((! defined $peers{$peer}{killed}) and ($i == 1)) {
		        next;
		    }  
		
     	            $record{peerstats} .= "IP : " . sprintf ("%15s", $peers{$peer}{ip});
	            (defined $peers{$peer}{killed}) and $record{peerstats}.='D';
	            $record{peerstats} .= " :: ";
	            $record{peerstats} .= "speedup : " .    sprintf ("%9s", readable ($peers{$peer}{speedup}))  . "/s :: " ;
	            $record{peerstats} .= "speeddown : " .  sprintf ("%9s", readable ($peers{$peer}{speeddown})) . "/s :: ";
    	            $record{peerstats} .= "left : " .       sprintf ("%8s", readable ($peers{$peer}{left})) . 
		                          " : (" . sprintf ("%5s", sprintf ("%.1f", $peers{$peer}{left}/($peers{$peer}{down} + $peers{$peer}{left} + 1)*100)) . "%) :: "; 
  	            $record{peerstats} .= "uploaded : ".    sprintf ("%8s", readable ($peers{$peer}{up})) . " :: ";
	            $record{peerstats} .= "downloaded : " . sprintf ("%8s", readable ($peers{$peer}{down})) . " :: ";
	            $record{peerstats} .= "firstseen : ".   $peers{$peer}{firstseen} . " :: ";
	            $record{peerstats} .= "lastseen : " .   $peers{$peer}{lastseen} . " :: ";
		    $record{peerstats} .= "duration : " .   readablespan ($peers{$peer}{lastseen} - $peers{$peer}{firstseen}) . " :: ";
		    if (defined $peers{$peer}{completedat}) {
		        my $duration = $peers{$peer}{completedat} - $peers{$peer}{firstseen};
  		        $record{peerstats} .= "completed : " . readable ($peers{$peer}{down}) .
		                              " in " . readablespan ($duration) . " @ " . 
					      sprintf ("%9s", readable ($peers{$peer}{down} / ($duration+1))) .
  					      "/s :: ";
    		    }
	        $record{peerstats} .= "\n";
                }
            }
	}  

        # either way, hand the data over to IO	      
        if ((time - $record{lastseen} < $writestats_time_back) or 
            $always_do_filestats or $nextfilestats) {
            $io->save (\%record);
        }

        if ($display_file_stats) {
            push @recs, \%record;
        }
    }

    if ((time - $lasttime < $writestats_time_back) or 
        $always_do_filestats or $nextfilestats) {
        $nextfilestats = 0;
        # Some global stats that might be of interest
        $io->savemisc (totalconnected => $totalconnected,
             totalmaxconnected        => $totalmaxconnected,
             reannounce_interval_avg  => $reannounce{current},
             totalleechers            => $totalleechers,
             totalseeds               => $totalseeds,
             totalupped               => $upped,
             totaluppedfmt            => sprintf("%10s", readable($upped)),
             totalcompleted           => $totalcompleted,
             totalgzip                => $totalgzip,
             totalannounces           => $totalannounces,
             totalannouncesize        => $totalannouncesize,
             totalscrapes             => $totalscrapes,
             totalscrapesize          => $totalscrapesize,
             totalothersize           => $totalothersize,
             );
    }

    # if we want to display stats on screen, sort them first, then give columns
    if ($display_file_stats) { 
        @recs = sort {${$a}{up} <=> ${$b}{up}} @recs; 
        print "Hash/Filename                                 ".
              "upped    dls  firstseen  timespan src/con\n";
    
        foreach my $re (@recs) {

          my %r = %{$re};
          # If we know a filename, use that. If not, use the hash.    
          if ($files{$r{hash}}{filename} ne '') {
              $r{name} = $files{$r{hash}}{filename};
          } else {
      	      $r{name} = $r{hash};
          }

          # shrink name, print out
          my $fname;
          if (length ($r{name}) > 40) {
      	      $fname = substr ($r{name},0,20) . '...' . 
                       substr ($r{name}, -17);
	  } else {
	      $fname = $r{name};
	  }

          print $fname . " " . $r{upfmt} . " ".  $r{completedfmt} . " " . 
                $r{firstseenfmt} . " " . $r{spanfmt} . " " . 
                $r{seedsfmt} . "\n";

        }
    }
}

# calculates current up/down speeds
sub filespeeds { 
    foreach my $file (keys %files) {
         $files{$file}{speedup} = 0;
         $files{$file}{speeddown} = 0;
    }
    foreach my $peer (keys %peers) {
         $files{$peers{$peer}{hash}}{speedup} += $peers{$peer}{speedup};
         $files{$peers{$peer}{hash}}{speeddown} += $peers{$peer}{speeddown};
    }
}

# calculates the current connections/file
sub connectionsperfile { 
    foreach my $file (keys %files) {
       $files{$file}{clients} = 0;
       $files{$file}{seeds} = 0;
       $files{$file}{peers} = ();
    }

    foreach my $peer (keys %peers) {
        if (! defined $peers{$peer}{killed}) {
            $files{$peers{$peer}{hash}}{clients}++;
	    if ((defined $peers{$peer}{left}) and ($peers{$peer}{left} == 0)) {
                $files{$peers{$peer}{hash}}{seeds}++;
	    }
	}

        if ($allowpeerstats) {
            push @{$files{$peers{$peer}{hash}}{peers}}, $peer;
        }
    }
}

# cleans up the peers hash (kill dead peers, tally up stats)
sub peerclean { 
    $totalconnected = 0;
    $totalseeds = 0;
    $totalleechers = 0;

    foreach my $peer (keys %peers) {
        if (($peers{$peer}{lastseen} + $timeout_downloaders_interval > $lasttime) and 
	    (defined $peers{$peer}{killed})) {
	    # client came back to life after timeout
	    delete $peers{$peer}{killed};
	    $peers{$peer}{nostat} = 1;
	    $finishedpeers--;
	} elsif (($peers{$peer}{lastseen} + $drop_downloaders_interval < $lasttime) and
	         (defined $peers{$peer}{killed})) {
            # drop time has been reached
	    delete $peers{$peer};
	} elsif ((! defined $peers{$peer}{killed}) and 
	         ($peers{$peer}{lastseen} + $timeout_downloaders_interval < $lasttime) or 
                 (defined $peers{$peer}{stopped})) {
	    # client exceeded timeout
            $finishedpeers++;
            # Has this peer been looked at before and come back from the dead ?
	    if (! defined $peers{$peer}{nostat}) {
                # How long have we tracked this peer
                my $peerlifetime = $peers{$peer}{lastseen} - 
                                   $peers{$peer}{firstseen};
                # What's the current average for the current torrent ?
                my $avg = $files{$peers{$peer}{hash}}{peerconnduraverage};
                # How many peers have finished this torrent before ?
                my $fpeers = $files{$peers{$peer}{hash}}{finishedpeers}++;

                # Calculate new average
                $files{$peers{$peer}{hash}}{peerconnduraverage} = 
                  int (($fpeers * $avg + $peerlifetime) / ($fpeers + 1));
                # Is this the new king for longest lifetime ?
                ($files{$peers{$peer}{hash}}{peerconndurmax} < $peerlifetime) 
                   and $files{$peers{$peer}{hash}}{peerconndurmax} = $peerlifetime;

                # if we have seen both start and complete events, we can do some more stuff
                if (defined $peers{$peer}{completedat} and defined $peers{$peer}{started}) {
                    # if we know the filesize of this torrent, we can deduce
                    # whether more than 90% were transferred. If so, proceed,
                    # otherwise just drop it
		    if (($files{$peers{$peer}{hash}}{filesize} == 0) or 
		        (($files{$peers{$peer}{hash}}{filesize} != 0) and 
			 (abs ($peers{$peer}{down} - $files{$peers{$peer}{hash}}{filesize}) <
			   $files{$peers{$peer}{hash}}{filesize}*0.1))) {

                        # Calculate new average for duration for completion
                        $files{$peers{$peer}{hash}}{peercomplduraverage} =
                            int (($files{$peers{$peer}{hash}}{completedpeers} 
                                * $files{$peers{$peer}{hash}}{peercomplduraverage} 
                                + $peerlifetime) / (++$files{$peers{$peer}{hash}}{completedpeers}));

                        # new winner for maximum ?
                        ($files{$peers{$peer}{hash}}{peercompldurmax} < $peerlifetime) and
                            $files{$peers{$peer}{hash}}{peercompldurmax} = $peerlifetime;

                        # and what about the minimum ?
                        (($files{$peers{$peer}{hash}}{peercompldurmin} > $peerlifetime) or
                         ($files{$peers{$peer}{hash}}{peercompldurmin} == 0)) and
                            $files{$peers{$peer}{hash}}{peercompldurmin} = $peerlifetime;
		    }			    
                }
	    }	

	    if (defined $peers{$peer}{stopped}) {
	        # if a "stopped" event was received, no need to keep the
                # peer around
		delete $peers{$peer};
	    } else {
                # Do not consider this peer for anything anymore; keep it around
                # in case it comes back (connection problems and the like)

    	        $peers{$peer}{killed} = 1;
            }

        } else {
            if (! defined $peers{$peer}{killed}) {               
  	       
                $totalconnected++;
                if ((defined $peers{$peer}{left}) and ($peers{$peer}{left} == 0)) {
                    $totalseeds++;
                } else {
                    $totalleechers++;
                }
            }
        }
    }

    if ($totalconnected > $totalmaxconnected) {
        $totalmaxconnected = $totalconnected;
    }
    
    connectionsperfile ();
}

# initialize fields for a new/unknown torrent/file
sub createfileentry { 
    my $info_hash = shift;
    my $timestamp = shift;
    $files{$info_hash} = {};
    $files{$info_hash}{peerconnduraverage} = 0;  # peer connection duration average
    $files{$info_hash}{peerconndurmax} = 0;      # peer connection duration maximum
    $files{$info_hash}{finishedpeers} = 0;       # peers that have stopped coming back
    $files{$info_hash}{completedpeers} = 0;      # peers that have downloaded the whole file
    $files{$info_hash}{peercomplduraverage} = 0; # peer completion duration average
    $files{$info_hash}{peercompldurmax} = 0;     # peer completion duration maximum 
    $files{$info_hash}{peercompldurmin} = 0;     # peer completion duration minimum (>90%)
    $files{$info_hash}{filename} = '';           # filename associated with this torrent
    $files{$info_hash}{filesize} = 0;            # filesize associated with this torrent
    $files{$info_hash}{completed} = 0;           # number of completed downloads
    $files{$info_hash}{up} = 0;                  # number of transferred bytes
    $files{$info_hash}{speedup} = 0;             # current upload-speed
    $files{$info_hash}{speeddown} = 0;           # current download-speed
    $files{$info_hash}{started} = 0;             # How many clients have started to download ?
    $files{$info_hash}{completed} = 0;           # ... and how many have finished ?
    $files{$info_hash}{clients} = 0;             # How many clients are currently on the file ?
    $files{$info_hash}{seeds} = 0;               # ... how many of those are seeds ?
    $files{$info_hash}{peakspeed} = 0;           # Peak speed seen on this file
    $files{$info_hash}{peakseeds} = 0;           # Peak number of seeds
    $files{$info_hash}{peakleechers} = 0;        # Peak number of leeches
    $files{$info_hash}{peakclients} = 0;         # Peak number of clients
    $files{$info_hash}{firstseen} = $timestamp;  # We just created the record ...
    $files{$info_hash}{lastseen} = $timestamp;   # We just created the record ...
}

# initialize fields for a new/unknown peer
sub createpeerentry { 
    my $peer_id = shift;
    my $timestamp = shift;
    my $info_hash = shift;
    $peers{$peer_id} = {};
    $peers{$peer_id}{firstseen} = $timestamp;
    $peers{$peer_id}{hash} = $info_hash;
}

# Auxiliary Routines 

# Saves runtime data for picking up later
sub saveruntime { 
    my %save = (
             'version' => 1,
             'peers' => \%peers,
	     'files' => \%files,
	     'lasttime' => $lasttime,
	     'upped' => $upped,
	     'inittime' => $inittime,
             'slots' => \@slots,
             'totalmaxconnected' => $totalmaxconnected,
             'totalconnected' => $totalconnected,
             'totalleechers' => $totalleechers,
             'totalseeds' => $totalseeds,
             'totalcompleted' => $totalcompleted,
             'totalgzip' => $totalgzip,
             'totalannounces' => $totalannounces,
             'totalannouncesize' => $totalannouncesize,
             'totalscrapes' => $totalscrapes,
             'totalscrapesize' => $totalscrapesize,
             'totalothersize' => $totalothersize,
             'firsttime' => $firsttime,
             'finishedpeers' => $finishedpeers,
             'reannounce' => \%reannounce);

    store \%save, $statefilename;
}

# Loads runtime data from previous runs
sub loadruntime { 
    if (-e $statefilename) {
        my $retrieve = retrieve($statefilename);
        my %retrieve = %$retrieve;
        %peers = %{$retrieve{peers}};
        %files = %{$retrieve{files}};
        %reannounce = %{$retrieve{reannounce}};
        $lasttime = $retrieve{lasttime};
        $inittime = $retrieve{inittime};
        $totalmaxconnected = $retrieve{totalmaxconnected};
        $totalconnected = $retrieve{totalconnected};
        $finishedpeers = $retrieve{finishedpeers};
        $upped = $retrieve{upped};
        @slots = @{$retrieve{slots}};
        if (defined $retrieve{version} and $retrieve{version} >= 1) {
          $totalseeds = $retrieve{totalseeds};
          $totalleechers = $retrieve{totalleechers};
          $totalcompleted = $retrieve{totalcompleted};
          $totalgzip = $retrieve{totalgzip};
          $totalannounces = $retrieve{totalannounces};
          $totalannouncesize = $retrieve{totalannouncesize};
          $totalscrapes = $retrieve{totalscrapes};
          $totalscrapesize = $retrieve{totalscrapesize};
          $totalothersize = $retrieve{totalothersize};
          $firsttime = $retrieve{firsttime};         
        } else {
          print "The statefile was created with an earlier version of\n",
                "this script. A couple of statistics will seem weird for\n",
                "a while, but you /should/ be able to continue using the\n",
                "old data, although there will be warnings and possibly\n",
                "some errors for the currently analyzed peers. Also, the\n",
                "RRD either needs a change or to be set up anew. See\n",
                "the webpage for further notes ...\n";
          $totalseeds = 0;
          $totalleechers = 0;
          $totalcompleted = 0;
          $totalgzip = 0;
          $totalannounces = 0;
          $totalannouncesize = 0;
          $totalscrapes = 0;
          $totalscrapesize = 0;
          $totalothersize = 0;
          $firsttime = 0;
        }
    }
}

# spits out some more readable numbers for byte figures
sub readable  { 
    my $d = shift;    
    ($d > 1024**5) and return (sprintf ("%.2f", $d/(1024**5))) . "PiB";
    ($d > 1024**4) and return (sprintf ("%.2f", $d/(1024**4))) . "TiB";
    ($d > 1024**3) and return (sprintf ("%.2f", $d/(1024**3))) . "GiB";
    ($d > 1024**2) and return (sprintf ("%.2f", $d/(1024**2))) . "MiB";
    ($d > 1024)    and return (sprintf ("%.2f", $d/(1024)))    . "KiB";
    return sprintf ("%.2f", $d) . "b";
}

# Spits out some more readable numbers for metric bit figures
sub readablemetric  { 
    my $d = shift;    
    ($d > 1000**5) and return (sprintf ("%.2f", $d/(1000**5))) . "PB";
    ($d > 1000**4) and return (sprintf ("%.2f", $d/(1000**4))) . "TB";
    ($d > 1000**3) and return (sprintf ("%.2f", $d/(1000**3))) . "GB";
    ($d > 1000**2) and return (sprintf ("%.2f", $d/(1000**2))) . "MB";
    ($d > 1000)    and return (sprintf ("%.2f", $d/(1000)))    . "kB";
    return sprintf ("%.2f", $d) . "b";
}

# 0-pad numbers to x columns
sub pad { 
    my $cols = shift;
    my $num = my $temp = shift;
    if (! defined $temp) {
        $temp = $num = 0;
    }
    while ($temp >= 10) {
        $temp /= 10;
        $cols--;
    }    
    return $cols > 0 ? ('0' x --$cols).$num : $num;
}

# a more readable timespan
sub readablespan { 
    my $span = shift;
    my $days = int ($span / 86400);
    my $hours = int (($span % 86400) / 3600);
    my $minutes = int ((($span % 86400) % 3600) / 60);
    return pad(2, $days) . 'd' . pad(2, $hours) . 'h' . 
           pad(2, $minutes) . 'm';
}

# Handle IO (with files)
package trackalyze::IO::file; 

no strict 'refs';

sub new { 
    my $proto = shift;
    my $class = ref($proto) || $proto;

    my $p = shift;
    if (! defined $p) {
        $p = {};
    }

    my $self = {
         statusdir => defined $p->{statusdir} ? $p->{statusdir} : 'status',
         miscsuffix => defined $p->{miscsuffix} ? $p->{miscsuffix} : '',
         connectionstats => defined $p->{connectionstats} ? $p->{connectionstats} : 1,
         completionstats => defined $p->{completionstats} ? $p->{completionstats} : 1,
         peerstats => defined $p->{peerstats} ? $p->{peerstats} : 1,
         peakstats => defined $p->{peakstats} ? $p->{peakstats} : 1,
               };

    bless ($self, $class);
    return $self;
}

# returns filename for torrent-hash
sub getfilename ($) { 
    my $self = shift;
    my $file = shift;
    if (-e "${$self}{statusdir}/$file.filename") {
        return `cat ${$self}{statusdir}/$file.filename`;
    }
    return '';
}

# returns filesize for torrent-hash
sub getfilesize ($) { 
    my $self = shift;
    my $file = shift;
    if (-e "${$self}{statusdir}/$file.filename") {
        return `cat ${$self}{statusdir}/$file.filesize`;
    }    
    return 0;
}

# saves data about one torrent
sub save ($) { 
    my $self = shift;
    my $rrecord = shift;
    my %r = %{$rrecord};
    my $statusdir = ${$self}{statusdir};

    open TR, ">$statusdir/$r{hash}.transferred";
    print TR $r{upfmt};
    close TR;

    open TR, ">$statusdir/$r{hash}.completed";
    print TR $r{completed};
    close TR;

    open TR, ">$statusdir/$r{hash}.duration";
    print TR $r{spanfmt};
    close TR;

    open TR, ">$statusdir/$r{hash}.connected";
    print TR $r{clients};
    close TR;

    open TR, ">$statusdir/$r{hash}.sources";
    print TR $r{seeds};
    close TR;

    open TR, ">$statusdir/$r{hash}.leechers";
    print TR $r{leechers};
    close TR;

    open TR, ">$statusdir/$r{hash}.speed";
    print TR $r{speedfmt};
    close TR;    

    open TR, ">$statusdir/$r{hash}.consolidated";
    print TR $r{clients} . ":" . $r{seeds} . ":" . $r{completed} . ":" . 
             $r{upfmt}     . ":" . $r{spanfmt} . ":" . $r{speed};
    close TR;

    if ($self->{peakstats}) {
      open TR, ">$statusdir/$r{hash}.peakseeds";
      print TR $r{peakseeds};
      close TR;

      open TR, ">$statusdir/$r{hash}.peakleechers";
      print TR $r{peakleechers};
      close TR;

      open TR, ">$statusdir/$r{hash}.peakclients";
      print TR $r{peakclients};
      close TR;

      open TR, ">$statusdir/$r{hash}.peakspeed";
      print TR $r{peakspeedfmt};
      close TR;
    }

    if ($self->{completionstats}) {
      open TR, ">$statusdir/$r{hash}.peercompleteavg";
      print TR $r{peercompleteavgfmt};
      close TR;

      open TR, ">$statusdir/$r{hash}.peercompletemax";
      print TR $r{peercompletemaxfmt};
      close TR;

      open TR, ">$statusdir/$r{hash}.peercompletemin";
      print TR $r{peercompleteminfmt};
      close TR;
    }

    if ($self->{connectionstats}) {    
      open TR, ">$statusdir/$r{hash}.peerconnectionaverage";
      print TR $r{peerconnavgfmt};
      close TR;
    
      open TR, ">$statusdir/$r{hash}.peerconnectiomax";
      print TR $r{peerconnmaxfmt};
      close TR;
    }
    
    if ($self->{peerstats}) {
      open TR, ">$statusdir/$r{hash}.peerstats";
      print TR $r{peerstats};
      close TR;
    }
}

# saves miscellaneous data
sub savemisc ($) { 
    my $self = shift;
    my %r = @_;
    my $statusdir = ${$self}{statusdir};

    open TR, ">$statusdir/total.connected" . $self->{miscsuffix};
    print TR $r{totalconnected};
    close TR;        
    
    open TR, ">$statusdir/total.maxconnected" . $self->{miscsuffix};
    print TR $r{totalmaxconnected};
    close TR;        

    open TR, ">$statusdir/announce_interval_avg" . $self->{miscsuffix};
    print TR $r{reannounce_interval_avg};
    close TR;

    open TR, ">$statusdir/total.leechers" . $self->{miscsuffix};
    print TR $r{totalleechers};
    close TR;

    open TR, ">$statusdir/total.seeds" . $self->{miscsuffix};
    print TR $r{totalseeds};
    close TR;

    open TR, ">$statusdir/total.transferred" . $self->{miscsuffix};
    print TR $r{totaluppedfmt};
    close TR;

    open TR, ">$statusdir/total.completed" . $self->{miscsuffix};
    print TR $r{totalcompleted};
    close TR;

    open TR, ">$statusdir/total.gzip" . $self->{miscsuffix};
    print TR $r{totalgzip};
    close TR;

    open TR, ">$statusdir/total.announces" . $self->{miscsuffix};
    print TR $r{totalannounces};
    close TR;

    open TR, ">$statusdir/total.announcesize" . $self->{miscsuffix};
    print TR $r{totalannouncesize};
    close TR;

    open TR, ">$statusdir/total.scrapes" . $self->{miscsuffix};
    print TR $r{totalscrapes};
    close TR;

    open TR, ">$statusdir/total.scrapesize" . $self->{miscsuffix};
    print TR $r{totalscrapesize};
    close TR;

    open TR, ">$statusdir/total.othersize" . $self->{miscsuffix};
    print TR $r{totalothersize};
    close TR;
}

# should peerstats be generated for file X ?
sub dopeerstats ($) { 
    my $self = shift;
    return $self->{peerstats};
}

package trackalyze::IO::sql; 
# Provisions for saving all this data to SQL tables

# If you want peerstats to be generated for a specific hash,
# insert a line into the peerstats table with the hash specified.
# there is also a table for metadata, which isn't touched by this
# code; you can store metadata there that may be used here, though.
# [Note : this may be removed soon, the use is dubious at best :/]

no strict 'refs';

# Only try to load DBI and DBD::mysql modules if they are available
BEGIN {
  eval('use DBI;'); 
  eval('use DBD::mysql;'); 
}

sub new { 
    my $proto = shift;
    my $class = ref($proto) || $proto;

    my $p = shift;
    if (! defined $p) {
        $p = {};
    }

    if (not exists $INC{'DBI.pm'}) {
         print "Sorry, DBI module could NOT be loaded (is it installed ?).\n";
         exit 0;
    }

    my $self = {
         dbname => defined $p->{dbname} ? $p->{dbname} : 'trackalyzer',
         dbuser => defined $p->{dbuser} ? $p->{dbuser} : 'trackalyzer',
         dbpass => defined $p->{dbpass} ? $p->{dbpass} : 'secret',
         statstable => defined $p->{statstable} ? $p->{statstable} : 'torrents',
         misctable => defined $p->{misctable} ? $p->{misctable} : 'misc',
         peerstatstable => defined $p->{peerstatstable} ? $p->{peerstatstable} : 'peerstats',
         metadatatable => defined $p->{metadatatable} ? $p->{metadatatable} : 'metadata',
         peerstats => defined $p->{peerstats} ? $p->{peerstats} : 1,
         sthandles => {},
               };

    $self->{sqlhandle} = DBI->connect ("dbi:mysql:" . $self->{dbname},
                                       $self->{dbuser}, $self->{dbpass}) 
                        or die $self->{sqlhandle}->errstr();

    my $st_handle = $self->{sqlhandle}->prepare 
                    ('show tables from ' . $self->{dbname})
                    or die $self->{sqlhandle}->errstr();
    my $result = $st_handle->execute () or die $self->{sqlhandle}->errstr();
    my $arrayref = $st_handle->fetchall_arrayref ()
                   or die $self->{sqlhandle}->errstr();

    my @rows = @$arrayref;
    $result = 0;
    foreach my $key (@rows) {
        if (@$key[0] eq $self->{statstable}) { $result++; }
    }
    
    if ($result eq 0) {
        my $st_handle = $self->{sqlhandle}->prepare 
           ('create table ' . $self->{statstable} . 
            '(info_hash CHAR(40) not null unique,
              time INT unsigned,
              transferred BIGINT unsigned,
              completed INT unsigned,
              duration INT unsigned,
              peercompletemin INT unsigned,
              peercompleteaverage INT unsigned,
              peercompletemax INT unsigned,
              peerconnectionaverage INT unsigned,
              peerconnectionmax INT unsigned,
              clients INT unsigned,
              seeds INT unsigned,
              speed FLOAT unsigned,
              peakclients INT unsigned,
              peakseeds INT unsigned,
              peakspeed FLOAT unsigned,       
              primary key (info_hash)
             )') or die $self->{sqlhandle}->errstr();
        my $result = $st_handle->execute () or die $self->{sqlhandle}->errstr();
        $st_handle = $self->{sqlhandle}->prepare 
           ('create table ' . $self->{misctable} . 
            '(name VARCHAR(255) not null unique,              
              value VARCHAR(255),
              primary key (name)
             )') or die $self->{sqlhandle}->errstr();
        $result = $st_handle->execute () or die $self->{sqlhandle}->errstr();
        $st_handle = $self->{sqlhandle}->prepare 
           ('create table ' . $self->{peerstatstable} . 
            '(info_hash CHAR(40) not null unique,
              peerstats mediumblob,
              primary key (info_hash)
             )') or die $self->{sqlhandle}->errstr();
        $result = $st_handle->execute () or die $self->{sqlhandle}->errstr();
        $st_handle = $self->{sqlhandle}->prepare 
           ('create table ' . $self->{metadatatable} . 
            '(info_hash CHAR(40) not null unique,
              filename VARCHAR(255),
              filesize BIGINT unsigned,
              tracker VARCHAR(255),
              piecesize INT unsigned,
              primary key (info_hash)
             )') or die $self->{sqlhandle}->errstr();
        $result = $st_handle->execute () or die $self->{sqlhandle}->errstr();

    }
 
    bless ($self, $class);
    return $self;
}

# returns filename for torrent-hash
sub getfilename ($) { 
    my $self = shift;
    my $info_hash = shift;

    unless (defined $self->{sthandles}->{getfn}) {
      $self->{sthandles}->{getfn} = $self->{sqlhandle}->prepare 
               ('select filename from ' . $self->{metadatatable} . 
                ' where info_hash=?') or die $self->{sqlhandle}->errstr();
    }
    my $result = $self->{sthandles}->{getfn}->execute ($info_hash)
                 or die $self->{sqlhandle}->errstr();
    my $arrayref = $self->{sthandles}->{getfn}->fetchall_arrayref () 
                   or die $self->{sqlhandle}->errstr();

    my @rows = @$arrayref;
    return scalar @rows != 0 ? ${$rows[0]}[0] : ''
}

# returns filesize for torrent-hash
sub getfilesize ($) { 
    my $self = shift;
    my $info_hash = shift;

    unless (defined $self->{sthandles}->{getfs}) {
      $self->{sthandles}->{getfs} = $self->{sqlhandle}->prepare 
               ('select filesize from ' . ${$self}{metadatatable} . 
                ' where info_hash=?') or die $self->{sqlhandle}->errstr();
    }
    my $result = $self->{sthandles}->{getfs}->execute ($info_hash) 
                 or die $self->{sqlhandle}->errstr();
    my $arrayref = $self->{sthandles}->{getfs}->fetchall_arrayref () 
                   or die $self->{sqlhandle}->errstr();

    my @rows = @$arrayref;
    return scalar @rows != 0 ? ${$rows[0]}[0] : ''
}

# saves data about one torrent
sub save ($) { 
    my $self = shift;
    my $rrecord = shift;
    my %r = %{$rrecord};

    unless (defined $self->{sthandles}->{save}) {
      $self->{sthandles}->{save} = $self->{sqlhandle}->prepare 
       ('insert ignore into ' . $self->{statstable} . 
        ' (info_hash, time, transferred, completed, duration, 
          peercompletemin, peercompleteaverage, peercompletemax,
          peerconnectionaverage, peerconnectionmax, clients, seeds,
          speed, peakclients, peakseeds, peakspeed) 
         values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
       ) or die $self->{sqlhandle}->errstr();
    }
   
    my $result = $self->{sthandles}->{save}->execute (
                  $r{hash}, $r{lastseen}, $r{up}, $r{completed}, $r{span},
                  $r{peercompletemin},$r{peercompleteavg}, $r{peercompletemax}, 
                  $r{peerconnavg}, $r{peerconnmax}, $r{clients}, $r{seeds},
                  $r{speed}, $r{peakclients}, $r{peakseeds}, $r{peakspeed}) 
                 or die $!;

    if ($result eq '0E0') {

        unless (defined $self->{sthandles}->{saveupdate}) {
          $self->{sthandles}->{saveupdate} = $self->{sqlhandle}->prepare 
           ('update ' . $self->{statstable} . 
            ' set time=?, transferred=?, completed=?, duration=?,
              peercompletemin=?, peercompleteaverage=?, peercompletemax=?,
              peerconnectionaverage=?, peerconnectionmax=?, clients=?,
              seeds=?, speed=?, peakclients=?, peakseeds=?, 
              peakspeed=? 
             where info_hash=?'
           ) or die $self->{sqlhandle}->errstr();
        }

        $result = $self->{sthandles}->{saveupdate}->execute (
                    $r{lastseen}, $r{up}, $r{completed}, $r{span}, 
                    $r{peercompletemin}, $r{peercompleteavg}, $r{peercompletemax},
                    $r{peerconnavg}, $r{peerconnmax}, $r{clients}, 
                    $r{seeds}, $r{speed}, $r{peakclients}, $r{peakseeds}, 
                    $r{peakspeed},
                    $r{hash})
                  or die $self->{sqlhandle}->errstr();
    }

    if ($self->dopeerstats ($r{hash})) {
        unless (defined $self->{sthandles}->{savepeerstats}) {
          $self->{sthandles}->{savepeerstats} = $self->{sqlhandle}->prepare        
             ('update ' . $self->{peerstatstable} . 
              ' set peerstats=? 
               where info_hash=?;') or die $self->{sqlhandle}->errstr();
        }
        $result = $self->{sthandles}->{savepeerstats}->execute 
                    ($r{peerstats}, $r{hash}) 
                  or die $self->{sqlhandle}->errstr();
    }
}

# saves miscellaneous data
sub savemisc ($) { 
    my $self = shift;
    my %r = @_;
    my $statusdir = ${$self}{statusdir};
    my $result;

    unless (defined $self->{sthandles}->{misc}) {
      $self->{sthandles}->{misc} = $self->{sqlhandle}->prepare 
         ('insert ignore into ' . $self->{misctable} . 
          ' (name, value) values (?, ?)') or die $self->{sqlhandle}->errstr();
    }
   
    $result = $self->{sthandles}->{misc}->execute 
               ('totalclients', $r{totalconnected}) 
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('totalmaxclients', $r{totalmaxconnected}) 
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('reannounce_interval_avg', $r{reannounce_interval_avg})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('totalleechers', $r{totalleechers})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('totalseeds', $r{totalseeds})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('totaltransfer', $r{totalupped})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute
               ('totalcompleted', $r{totalcompleted})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('totalgzip', $r{totalgzip})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('totalannounces', $r{totalannounces})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute
               ('totalannouncesize', $r{totalannouncesize})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('totalscrapes', $r{totalscrapes})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('totalscrapesize', $r{totalscrapesize})
              or die $self->{sqlhandle}->errstr();
    $result = $self->{sthandles}->{misc}->execute 
               ('totalothersize', $r{totalothersize})
              or die $self->{sqlhandle}->errstr();

    if ($result eq '0E0') {
        unless (defined $self->{sthandles}->{miscupdate}) {
          $self->{sthandles}->{miscupdate} = $self->{sqlhandle}->prepare 
            ('update ' . $self->{misctable} . 
             ' set value=? where name=?;') 
            or die $self->{sqlhandle}->errstr(); 
        }

        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalconnected}, 'totalclients') 
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalmaxconnected}, 'totalmaxclients')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{reannounce_interval_avg}, 'reannounce_interval_avg')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalleechers}, 'totalleechers')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalseeds}, 'totalseeds')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   (sprintf ("%.0f", $r{totalupped}), 'totaltransfer')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalcompleted}, 'totalcompleted')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalgzip}, 'totalgzip')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalannounces}, 'totalannounces')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalannouncesize}, 'totalannouncesize')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalscrapes}, 'totalscrapes')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalscrapesize}, 'totalscrapesize')
                  or die $self->{sqlhandle}->errstr();
        $result = $self->{sthandles}->{miscupdate}->execute 
                   ($r{totalothersize}, 'totalothersize')
                  or die $self->{sqlhandle}->errstr();
    }
}

# should peerstats be generated for file X ?
sub dopeerstats ($) { 
    my $self = shift;
    my $info_hash = shift;

    if ($self->{peerstats}) {
      unless (defined $self->{sthandles}->{peerstatsquery}) {
        $self->{sthandles}->{peerstatsquery} = $self->{sqlhandle}->prepare 
               ('select peerstats from ' . $self->{peerstatstable} . 
                ' where info_hash=?') or die $self->{sqlhandle}->errstr();
      }
      my $result = $self->{sthandles}->{peerstatsquery}->execute ($info_hash)
                   or die $self->{sqlhandle}->errstr();
      my $arrayref = $self->{sthandles}->{peerstatsquery}->fetchall_arrayref ()
                     or die $self->{sqlhandle}->errstr();

      my @rows = @$arrayref;
      return (scalar @rows != 0);
    }
}

sub destroy {
    my $self = shift;
    $self->{sqlhandle}->disconnect () or die $self->{sqlhandle}->errstr();
}


package trackalyze::IO::multiplex; 
# multiplexes two or more IO:: modules.

no strict 'refs';

sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self = { 
                 objects => \@_
               }; 

    bless ($self, $class);
    return $self;
}

# returns filename for torrent-hash; use first object.
sub getfilename ($) {
    my $self = shift;
    return ${${$self}{objects}}[0]->getfilename (@_);
}

# returns filesize for torrent-hash
sub getfilesize ($) {
    my $self = shift;
    return ${${$self}{objects}}[0]->getfilesize (@_);
}

# saves data about one torrent
sub save ($) {
    my $self = shift;
    foreach my $object (@{${$self}{objects}}) {
        $object->save (@_);
    }
}

# saves miscellaneous data
sub savemisc ($) {
    my $self = shift;
    foreach my $object (@{${$self}{objects}}) {
        $object->savemisc (@_);
    }
}

# should peerstats be generated for file X ?
sub dopeerstats ($) {
    my $self = shift;
    return ${${$self}{objects}}[0]->dopeerstats (@_);
}

sub destroy {
    my $self = shift;
    foreach my $object (@{${$self}{objects}}) {
        $object->destroy (@_);
    }
}

package trackalyze::IO::Dummy;
# Blackholes all IO requests

no strict 'refs';

sub new { 
    my $proto = shift;
    my $class = ref($proto) || $proto;


    my $self = {
               };

    bless ($self, $class);
    return $self;
}

# returns filename for torrent-hash
sub getfilename ($) { 
    return undef;
}

# returns filesize for torrent-hash
sub getfilesize ($) { 
    return undef;
}

# saves data about one torrent
sub save ($) { 
}

# saves miscellaneous data
sub savemisc ($) { 
}

# should peerstats be generated for file X ?
sub dopeerstats ($) { 
    return 0;
}


package trackalyze::Graph::rrdtool; 
# Provides Graphing via external rrdtool

use POSIX qw(strftime);

no strict 'refs';

sub new { 
    my $proto = shift;
    my $class = ref($proto) || $proto;

    my $p = shift;
    if (! defined $p) {
        $p = {};
    }

    my $self = {
         rrd         => defined $p->{rrd}         ? 
                        $p->{rrd}         : 'torrent.rrd',
         outputdir   => defined $p->{outputdir}   ? 
                        $p->{outputdir}   : 'graphs',
         useexternal => defined $p->{useexternal} ? 
                        $p->{useexternal} : undef,
         graphtitle  => defined $p->{graphtitle}  ? 
                        $p->{graphtitle}  : 'BitTorrent network traffic',
         };

    bless ($self, $class);
    return $self;
}

sub setup { 
    my $self = shift;
    my $ts = shift;

    if (! -e ${$self}{rrd}) {
        my $a  = `rrdtool create ${$self}{rrd} --start $ts \\
                    DS:torrent:COUNTER:600:0:U \\
                    DS:users:GAUGE:600:0:U \\
                    DS:seeds:GAUGE:600:0:U \\
                    --step 300 \\
                    RRA:AVERAGE:0.5:1:2000 \\
                    RRA:AVERAGE:0.5:6:2000 \\
                    RRA:AVERAGE:0.5:24:2000 \\
                    RRA:AVERAGE:0.5:288:2000 \\
                    RRA:MAX:0.5:1:2000 \\
                    RRA:MAX:0.5:6:2000 \\
                    RRA:MAX:0.5:24:2000 \\
                    RRA:MAX:0.5:288:2000`;
    }
}

sub update { 
    my $self = shift;
    my $ts = shift;
    my $upped = shift;
    my $users = shift;
    my $totalseeds = shift;
    my $uppedlong = sprintf ("%.0f", $upped);
    my $a = `rrdtool update ${$self}{rrd} $ts:$uppedlong:$users:$totalseeds`;
}

sub graph { 
    my $self = shift;
    my $endtime = shift;

    unless (defined $self->{useexternal} and $self->{useexternal}) {
 
        my $daystart = $endtime - 86400;
        my $weekstart = $endtime - 604800;
        my $monthstart = $endtime - 2592000;
        my $yearstart = $endtime - 31536000;
        my $dayend = $endtime - 300;
        my $weekend = $endtime - 1800;
        my $monthend = $endtime - 7200;
        my $yearend = $endtime - 86400;
        my $enddateday = strftime('%D %H:%M:%S', localtime($endtime-300));
        my $enddateweek = strftime('%D %H:%M:%S', localtime($endtime-1800));
        my $enddatemonth = strftime('%D %H:%M:%S', localtime($endtime-7200));
        my $enddateyear = strftime('%D %H:%M:%S', localtime($endtime-86400));

        my $title = $self->{graphtitle};
        my $date = strftime('%D %H:%M:%S', localtime(time));

        # Bandwidth graphs ...
        my $a = `rrdtool graph $self->{outputdir}/torrent_day.png \\
          --start $daystart \\
          -e $dayend \\
          DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE \\
          "CDEF:torrent_in_bits=torrent_in_bytes,8,*" \\
          "CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,86400,*" \\
          AREA:torrent_in_bits#00dd00:torrent \\
          COMMENT:"                                       +--------------------------\\n" \\
          COMMENT:"             maximum       average       current" \\
          COMMENT:" | graphed $date\\n" \\
          COMMENT:"in     " \\
          GPRINT:torrent_in_bits:MAX:"%7.2lf %sb/s" \\
          GPRINT:torrent_in_bits:AVERAGE:"%7.2lf %Sb/s" \\
          GPRINT:torrent_in_bits:LAST:"%7.2lf %Sb/s" \\
          COMMENT:"| endtime $enddateday \\n" \\
          COMMENT:"                                                   | Total in metric \(10^X\),\\n" \\
          GPRINT:torrent_bytes_in:AVERAGE:"ROUGHLY  %7.2lf %sB total" \\
          COMMENT:"                        | not binary \(2^X\) units." \\
          -v "bits/sec" \\
          -t "$title traffic \(day\) 5 min avg" \\
          -h 100 \\
          -w 392 \\
          -x "HOUR:1:HOUR:6:HOUR:2:0:%H" \\
          -l 0 \\
          -a "PNG"`;

        $a = `rrdtool graph $self->{outputdir}/torrent_week.png \\
          --start $weekstart \\
          -e $weekend \\
          DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE \\
          DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX \\
          "CDEF:torrent_in_bits=torrent_in_bytes,8,*" \\
          "CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,604800,*" \\
          "CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*" \\
          "CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,604800,*" \\
          "CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-" \\
          AREA:torrent_in_bits#00dd00:torrent \\
          STACK:torrent_in_bits_maxtop#cccccc:peak \\
          COMMENT:"                               +--------------------------\\n" \\
          COMMENT:"             maximum       average       current" \\
          COMMENT:" | graphed $date\\n" \\
          COMMENT:"in     " \\
          GPRINT:torrent_in_bits:MAX:"%7.2lf %sb/s" \\
          GPRINT:torrent_in_bits:AVERAGE:"%7.2lf %Sb/s" \\
          GPRINT:torrent_in_bits:LAST:"%7.2lf %Sb/s" \\
          COMMENT:"| endtime $enddateweek\\n" \\
          COMMENT:"5m-peak" \\
          GPRINT:torrent_in_bits_max:MAX:"%7.2lf %sb/s" \\
          COMMENT:"            " \\
          GPRINT:torrent_in_bits_max:LAST:"%7.2lf %Sb/s" \\
          COMMENT:"| Total in metric \(10^X\),\\n" \\
          GPRINT:torrent_bytes_in:AVERAGE:"ROUGHLY  %7.2lf %sB total" \\
          COMMENT:"                        | not binary \(2^X\) units." \\
          -v "bits/sec" \\
          -t "$title traffic \(week\) 30 min avg" \\
          -h 100 \\
          -w 392 \\
          -x "HOUR:6:DAY:1:DAY:1:0:%a" \\
          -l 0 \\
          -a "PNG"`;

        $a = `rrdtool graph $self->{outputdir}/torrent_month.png \\
          --start $monthstart \\
          -e $monthend \\
          DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE \\
          DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX \\
          "CDEF:torrent_in_bits=torrent_in_bytes,8,*" \\
          "CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,2592000,*" \\
          "CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*" \\
          "CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,2592000,*" \\
          "CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-" \\
          AREA:torrent_in_bits#00dd00:torrent \\
          STACK:torrent_in_bits_maxtop#cccccc:peak \\
          COMMENT:"                               +--------------------------\\n" \\
          COMMENT:"             maximum       average       current" \\
          COMMENT:" | graphed $date\\n" \\
          COMMENT:"in     " \\
          GPRINT:torrent_in_bits:MAX:"%7.2lf %sb/s" \\
          GPRINT:torrent_in_bits:AVERAGE:"%7.2lf %Sb/s" \\
          GPRINT:torrent_in_bits:LAST:"%7.2lf %Sb/s" \\
          COMMENT:"| endtime $enddatemonth\\n" \\
          COMMENT:"5m-peak" \\
          GPRINT:torrent_in_bits_max:MAX:"%7.2lf %sb/s" \\
          COMMENT:"            " \\
          GPRINT:torrent_in_bits_max:LAST:"%7.2lf %Sb/s" \\
          COMMENT:"| Total in metric \(10^X\),\\n" \\
          GPRINT:torrent_bytes_in:AVERAGE:"ROUGHLY  %7.2lf %sB total" \\
          COMMENT:"                        | not binary \(2^X\) units." \\
          -v "bits/sec" \\
          -t "$title traffic \(month\) 2 hour avg" \\
          -h 100 \\
          -w 392 \\
          -x "DAY:1:WEEK:1:WEEK:1:0:Week %W" \\
          -l 0 \\
          -a "PNG"`;

        $a = `rrdtool graph $self->{outputdir}/torrent_year.png \\
          --start $yearstart \\
          -e $yearend \\
          DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE \\
          DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX \\
          "CDEF:torrent_in_bits=torrent_in_bytes,8,*" \\
          "CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,31536000,*" \\
          "CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*" \\
          "CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,31536000,*" \\
          "CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-" \\
          AREA:torrent_in_bits#00dd00:torrent \\
          STACK:torrent_in_bits_maxtop#cccccc:peak \\
          COMMENT:"                               +--------------------------\\n" \\
          COMMENT:"             maximum       average       current" \\
          COMMENT:" | graphed $date\\n" \\
          COMMENT:"in     " \\
          GPRINT:torrent_in_bits:MAX:"%7.2lf %sb/s" \\
          GPRINT:torrent_in_bits:AVERAGE:"%7.2lf %Sb/s" \\
          GPRINT:torrent_in_bits:LAST:"%7.2lf %Sb/s" \\
          COMMENT:"| endtime $enddateyear\\n" \\
          COMMENT:"5m-peak" \\
          GPRINT:torrent_in_bits_max:MAX:"%7.2lf %sb/s" \\
          COMMENT:"            " \\
          GPRINT:torrent_in_bits_max:LAST:"%7.2lf %Sb/s" \\
          COMMENT:"| Total in metric \(10^X\),\\n" \\
          GPRINT:torrent_bytes_in:AVERAGE:"ROUGHLY  %7.2lf %sB total" \\
          COMMENT:"                        | not binary \(2^X\) units." \\
          -v "bits/sec" \\
          -t "$title traffic \(year\) 1 day avg" \\
          -h 100 \\
          -w 392 \\
          -x "MONTH:1:MONTH:1:MONTH:1:0:%b" \\
          -l 0 \\
          -a "PNG"`;

        $a = `rrdtool graph $self->{outputdir}/users_day.png \\
          --start $daystart \\
          -e $dayend \\
          DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE \\
          DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE \\
          "CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF" \\
          "CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF" \\
          "CDEF:tracker_leechers=tracker_users,tracker_seeds,-" \\
          "CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/" \\
          "CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF" \\
          AREA:tracker_seeds#0000ff:Seeds \\
          STACK:tracker_leechers#0000aa:Leechers \\
          COMMENT:"                             +--------------------------\\n" \\
          COMMENT:"             maximum       average       current" \\
          COMMENT:" | graphed $date\\n" \\
          COMMENT:"Seeds      " \\
          GPRINT:tracker_seeds:MAX:"%7.0lf" \\
          GPRINT:tracker_seeds:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_seeds:LAST:"     %7.0lf" \\
          COMMENT:" | endtime $enddateday\\n" \\
          COMMENT:"Leechers   " \\
          GPRINT:tracker_leechers:MAX:"%7.0lf" \\
          GPRINT:tracker_leechers:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_leechers:LAST:"     %7.0lf" \\
          COMMENT:" | Average Seeder to Leecher\\n" \\
          COMMENT:"All Clients" \\
          GPRINT:tracker_users:MAX:"%7.0lf" \\
          GPRINT:tracker_users:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_users:LAST:"     %7.0lf" \\
          GPRINT:tracker_ratio:AVERAGE:" | ratio is %3.2lf\\n" \\
          COMMENT:"                                                   |\\n" \\
          --alt-y-grid \\
          -v users \\
          -t "$title users (day) 5m avg" \\
          -h 100 \\
          -w 392 \\
          -x "HOUR:1:HOUR:6:HOUR:2:0:%H" \\
          -l 0 \\
          --units-exponent 0 \\
          -a "PNG"`;


        $a = `rrdtool graph $self->{outputdir}/users_week.png \\
          --start $weekstart \\
          -e $weekend \\
          DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE \\
          DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE \\
          DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX \\
          DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX \\
          "CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF" \\
          "CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF" \\
          "CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF" \\
          "CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF" \\
          "CDEF:tracker_leechers=tracker_users,tracker_seeds,-" \\
          "CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-" \\
          "CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-" \\
          "CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/" \\
          "CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF" \\
          AREA:tracker_seeds#0000ff:Seeds \\
          STACK:tracker_leechers#0000aa:Leechers \\
          STACK:tracker_users_maxtop#cccccc:peak \\
          COMMENT:"                    +--------------------------\\n" \\
          COMMENT:"             maximum       average       current" \\
          COMMENT:" | graphed $date\\n" \\
          COMMENT:"Seeds      " \\
          GPRINT:tracker_seeds:MAX:"%7.0lf" \\
          GPRINT:tracker_seeds:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_seeds:LAST:"     %7.0lf" \\
          COMMENT:" | endtime $enddateweek\\n" \\
          COMMENT:"Leechers   " \\
          GPRINT:tracker_leechers:MAX:"%7.0lf" \\
          GPRINT:tracker_leechers:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_leechers:LAST:"     %7.0lf" \\
          COMMENT:" | Average Seeder to Leecher\\n" \\
          COMMENT:"All Clients" \\
          GPRINT:tracker_users:MAX:"%7.0lf" \\
          GPRINT:tracker_users:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_users:LAST:"     %7.0lf" \\
          GPRINT:tracker_ratio:AVERAGE:" | ratio is %3.2lf\\n" \\
          COMMENT:"Peak       " \\
          GPRINT:tracker_users_max:MAX:"%7.0lf" \\
          COMMENT:"            " \\
          GPRINT:tracker_users_max:LAST:"     %7.0lf" \\
          COMMENT:" | 5m peak of all clients\\n" \\
          --alt-y-grid \\
          -v users \\
          -t "$title users (week) 30 min avg" \\
          -h 100 \\
          -w 392 \\
          -x "HOUR:6:DAY:1:DAY:1:0:%a" \\
          -l 0 \\
          --units-exponent 0 \\
          -a "PNG"`;

        $a = `rrdtool graph $self->{outputdir}/users_month.png \\
          --start $monthstart \\
          -e $monthend \\
          DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE \\
          DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE \\
          DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX \\
          DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX \\
          "CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF" \\
          "CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF" \\
          "CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF" \\
          "CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF" \\
          "CDEF:tracker_leechers=tracker_users,tracker_seeds,-" \\
          "CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-" \\
          "CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-" \\
          "CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/" \\
          "CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF" \\
          AREA:tracker_seeds#0000ff:Seeds \\
          STACK:tracker_leechers#0000aa:Leechers \\
          STACK:tracker_users_maxtop#cccccc:peak \\
          COMMENT:"                    +--------------------------\\n" \\
          COMMENT:"             maximum       average       current" \\
          COMMENT:" | graphed $date\\n" \\
          COMMENT:"Seeds      " \\
          GPRINT:tracker_seeds:MAX:"%7.0lf" \\
          GPRINT:tracker_seeds:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_seeds:LAST:"     %7.0lf" \\
          COMMENT:" | endtime $enddatemonth\\n" \\
          COMMENT:"Leechers   " \\
          GPRINT:tracker_leechers:MAX:"%7.0lf" \\
          GPRINT:tracker_leechers:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_leechers:LAST:"     %7.0lf" \\
          COMMENT:" | Average Seeder to Leecher\\n" \\
          COMMENT:"All Clients" \\
          GPRINT:tracker_users:MAX:"%7.0lf" \\
          GPRINT:tracker_users:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_users:LAST:"     %7.0lf" \\
          GPRINT:tracker_ratio:AVERAGE:" | ratio is %3.2lf\\n" \\
          COMMENT:"Peak       " \\
          GPRINT:tracker_users_max:MAX:"%7.0lf" \\
          COMMENT:"            " \\
          GPRINT:tracker_users_max:LAST:"     %7.0lf" \\
          COMMENT:" | 5m peak of all clients\\n" \\
          --alt-y-grid \\
          -v users \\
          -t "$title users (month) 2 hour avg" \\
          -h 100 \\
          -w 392 \\
          -x "DAY:1:WEEK:1:WEEK:1:0:Week %W" \\
          -l 0 \\
          --units-exponent 0 \\
          -a "PNG"`;

        $a = `rrdtool graph $self->{outputdir}/users_year.png \\
          --start $yearstart \\
          -e $yearend \\
          DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE \\
          DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE \\
          DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX \\
          DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX \\
          "CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF" \\
          "CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF" \\
          "CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF" \\
          "CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF" \\
          "CDEF:tracker_leechers=tracker_users,tracker_seeds,-" \\
          "CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-" \\
          "CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-" \\
          "CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/" \\
          "CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF" \\
          AREA:tracker_seeds#0000ff:Seeds \\
          STACK:tracker_leechers#0000aa:Leechers \\
          STACK:tracker_users_maxtop#cccccc:peak \\
          COMMENT:"                    +--------------------------\\n" \\
          COMMENT:"             maximum       average       current" \\
          COMMENT:" | graphed $date\\n" \\
          COMMENT:"Seeds      " \\
          GPRINT:tracker_seeds:MAX:"%7.0lf" \\
          GPRINT:tracker_seeds:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_seeds:LAST:"     %7.0lf" \\
          COMMENT:" | endtime $enddateyear\\n" \\
          COMMENT:"Leechers   " \\
          GPRINT:tracker_leechers:MAX:"%7.0lf" \\
          GPRINT:tracker_leechers:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_leechers:LAST:"     %7.0lf" \\
          COMMENT:" | Average Seeder to Leecher\\n" \\
          COMMENT:"All Clients" \\
          GPRINT:tracker_users:MAX:"%7.0lf" \\
          GPRINT:tracker_users:AVERAGE:"     %7.0lf" \\
          GPRINT:tracker_users:LAST:"     %7.0lf" \\
          GPRINT:tracker_ratio:AVERAGE:" | ratio is %3.2lf\\n" \\
          COMMENT:"Peak       " \\
          GPRINT:tracker_users_max:MAX:"%7.0lf" \\
          COMMENT:"            " \\
          GPRINT:tracker_users_max:LAST:"     %7.0lf" \\
          COMMENT:" | 5m peak of all clients\\n" \\
          --alt-y-grid \\
          -v users \\
          -t "$title users (year) 1 day avg" \\
          -h 100 \\
          -w 392 \\
          -x "MONTH:1:MONTH:1:MONTH:1:0:%b" \\
          -l 0 \\
          --units-exponent 0 \\
          -a "PNG"`;
    } else {
       my $a = `${$self}{useexternal}`;
    }
}

package trackalyze::Graph::RRDs; 
# Provides Graphing via shared rrdtool

use POSIX qw(strftime);

# Only try to load RRDs if it's actually available

BEGIN {
    eval('use RRDs;');
}

no strict 'refs';

sub new { 
    my $proto = shift;
    my $class = ref($proto) || $proto;

    my $p = shift;
    if (! defined $p) {
        $p = {};
    }

    if (not exists $INC{'RRDs.pm'}) {
         print "Sorry, RRDs module could not be loaded (is it installed ?).\n";
         print "Please switch to external rrdtool module.\n";
         exit 0;
    }
    
    my $self = {
         rrd         => defined $p->{rrd}         ? 
                        $p->{rrd}         : 'torrent.rrd',
         outputdir   => defined $p->{outputdir}   ? 
                        $p->{outputdir}   : 'graphs',
         useexternal => defined $p->{useexternal} ? 
                        $p->{useexternal} : undef,
         graphtitle  => defined $p->{graphtitle}  ? 
                        $p->{graphtitle}  : 'BitTorrent network traffic',
         };

    bless ($self, $class);
    return $self;
}

sub setup { 
    my $self = shift;
    my $ts = shift;

    if (! -e $self->{rrd}) {
        RRDs::create($self->{rrd},
           '--start' => $ts,
           'DS:torrent:COUNTER:600:0:U',
           'DS:users:GAUGE:600:0:U',
           'DS:seeds:GAUGE:600:0:U',
           '--step' => 300,
           'RRA:AVERAGE:0.5:1:2000',
           'RRA:AVERAGE:0.5:6:2000',
           'RRA:AVERAGE:0.5:24:2000',
           'RRA:AVERAGE:0.5:288:2000',
           'RRA:MAX:0.5:1:2000',
           'RRA:MAX:0.5:6:2000',
           'RRA:MAX:0.5:24:2000',
           'RRA:MAX:0.5:288:2000');
    }
}

sub update { 
    my $self = shift;
    my $ts = shift;
    my $upped = shift;
    my $users = shift;
    my $totalseeds = shift;
    RRDs::update($self->{rrd}, $ts.':'.sprintf ("%.0f", $upped).':'.$users.':'.
                               $totalseeds);
}

sub graph { 
    my $self = shift;
    my $endtime = shift;

    unless (defined $self->{useexternal} and $self->{useexternal}) {
 
        my $daystart = $endtime - 86400;
        my $weekstart = $endtime - 604800;
        my $monthstart = $endtime - 2592000;
        my $yearstart = $endtime - 31536000;

        my $title = $self->{graphtitle};
        my $date = strftime('%D %H:%M:%S', localtime(time));
        my $graphdate = strftime('%D %H:%M:%S', localtime($endtime));

        # Bandwidth graphs ...
        RRDs::graph(
          $self->{outputdir}.'/torrent_day.png',
          '--start' => $daystart,
          '-e' => $endtime-300,
          "DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE",
          'CDEF:torrent_in_bits=torrent_in_bytes,8,*',
          'CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,86400,*',
          'AREA:torrent_in_bits#00dd00:torrent',
          'COMMENT:                                        +--------------------------\n',
          'COMMENT:             maximum       average       current',
          'COMMENT: | graphed '.$date.'\n',
          'COMMENT:in     ',
          'GPRINT:torrent_in_bits:MAX:%7.2lf %sb/s',
          'GPRINT:torrent_in_bits:AVERAGE:%7.2lf %Sb/s',
          'GPRINT:torrent_in_bits:LAST:%7.2lf %Sb/s',
          'COMMENT:| endtime '.strftime('%D %H:%M:%S', localtime($endtime-300)).'\n',
          'COMMENT:                                                   | Total in metric (10^X),\n',
          'GPRINT:torrent_bytes_in:AVERAGE:ROUGHLY  %7.2lf %sB total',
          'COMMENT:                        | not binary (2^X) units.',
          '-v' => "bits/sec",
          '-t' => $title . ' traffic (day) 5 min avg',
          '-h' => 100,
          '-w' => 392,
          '-x' => 'HOUR:1:HOUR:6:HOUR:2:0:%H',
          '-l' => 0,
          '-a' => 'PNG');

        RRDs::graph(
          $self->{outputdir}.'/torrent_week.png',
          '--start' => $weekstart,
          '-e' => $endtime-1800,
          "DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE",
          "DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX",
          'CDEF:torrent_in_bits=torrent_in_bytes,8,*',
          'CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,604800,*',
          'CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*',
          'CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,604800,*',
          'CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-',
          'AREA:torrent_in_bits#00dd00:torrent',
          'STACK:torrent_in_bits_maxtop#cccccc:peak',
          'COMMENT:                               +--------------------------\n',
          'COMMENT:             maximum       average       current',
          'COMMENT: | graphed '.$date.'\n',
          'COMMENT:in     ',
          'GPRINT:torrent_in_bits:MAX:%7.2lf %sb/s',
          'GPRINT:torrent_in_bits:AVERAGE:%7.2lf %Sb/s',
          'GPRINT:torrent_in_bits:LAST:%7.2lf %Sb/s', 
          'COMMENT:| endtime '.strftime('%D %H:%M:%S', localtime($endtime-1800)).'\n',
          'COMMENT:5m-peak',
          'GPRINT:torrent_in_bits_max:MAX:%7.2lf %sb/s',
          'COMMENT:            ',
          'GPRINT:torrent_in_bits_max:LAST:%7.2lf %Sb/s', 
          'COMMENT:| Total in metric (10^X),\n',
          'GPRINT:torrent_bytes_in:AVERAGE:ROUGHLY  %7.2lf %sB total',
          'COMMENT:                        | not binary (2^X) units. ',
          '-v' => 'bits/sec',
          '-t' => $title . ' traffic (week) 30 min avg',
          '-h' => 100,
          '-w' => 392,
          '-x' => 'HOUR:6:DAY:1:DAY:1:0:%a',
          '-l' => 0,
          '-a' => 'PNG');

        RRDs::graph(
          $self->{outputdir}.'/torrent_month.png',
          '--start' => $monthstart,
          '-e' => $endtime-7200,
          "DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE",
          "DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX",
          'CDEF:torrent_in_bits=torrent_in_bytes,8,*',
          'CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,2592000,*',
          'CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*',
          'CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,2592000,*',
          'CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-',
          'AREA:torrent_in_bits#00dd00:torrent',
          'STACK:torrent_in_bits_maxtop#cccccc:peak',
          'COMMENT:                               +--------------------------\n',
          'COMMENT:             maximum       average       current',
          'COMMENT: | graphed '.$date.'\n', 
          'COMMENT:in     ',
          'GPRINT:torrent_in_bits:MAX:%7.2lf %sb/s',
          'GPRINT:torrent_in_bits:AVERAGE:%7.2lf %Sb/s',
          'GPRINT:torrent_in_bits:LAST:%7.2lf %Sb/s',
          'COMMENT:| endtime '.strftime('%D %H:%M:%S', localtime($endtime-7200)).'\n',
          'COMMENT:5m-peak',
          'GPRINT:torrent_in_bits_max:MAX:%7.2lf %sb/s',
          'COMMENT:            ',
          'GPRINT:torrent_in_bits_max:LAST:%7.2lf %Sb/s', 
          'COMMENT:| Total in metric (10^X),\n',
          'GPRINT:torrent_bytes_in:AVERAGE:ROUGHLY  %7.2lf %sB total',
          'COMMENT:                        | not binary (2^X) units.',
          '-v' => 'bits/sec',
          '-t' => $title .' traffic (month) 2 hour avg',
          '-h' => 100,
          '-w' => 392,
          '-x' => 'DAY:1:WEEK:1:WEEK:1:0:Week %W',
          '-l' => 0,
          '-a' => 'PNG');

        RRDs::graph(
          $self->{outputdir}.'/torrent_year.png',
          '--start' => $yearstart,
          '-e' => $endtime-86400,
          "DEF:torrent_in_bytes=$self->{rrd}:torrent:AVERAGE",
          "DEF:torrent_in_bytes_max=$self->{rrd}:torrent:MAX",
          'CDEF:torrent_in_bits=torrent_in_bytes,8,*',
          'CDEF:torrent_bytes_in=torrent_in_bytes,0,1250000000,LIMIT,UN,0,torrent_in_bytes,IF,31536000,*',
          'CDEF:torrent_in_bits_max=torrent_in_bytes_max,8,*',
          'CDEF:torrent_bytes_in_max=torrent_in_bytes_max,0,1250000000,LIMIT,UN,0,torrent_in_bytes_max,IF,31536000,*',
          'CDEF:torrent_in_bits_maxtop=torrent_in_bits_max,torrent_in_bits,-',
          'AREA:torrent_in_bits#00dd00:torrent',
          'STACK:torrent_in_bits_maxtop#cccccc:peak',
          'COMMENT:                               +--------------------------\n',
          'COMMENT:             maximum       average       current',
          'COMMENT: | graphed '.$date.'\n',
          'COMMENT:in     ',
          'GPRINT:torrent_in_bits:MAX:%7.2lf %sb/s',
          'GPRINT:torrent_in_bits:AVERAGE:%7.2lf %Sb/s',
          'GPRINT:torrent_in_bits:LAST:%7.2lf %Sb/s',
          'COMMENT:| endtime '.strftime('%D %H:%M:%S', localtime($endtime-86400)).'\n',
          'COMMENT:5m-peak',
          'GPRINT:torrent_in_bits_max:MAX:%7.2lf %sb/s',
          'COMMENT:            ',
          'GPRINT:torrent_in_bits_max:LAST:%7.2lf %Sb/s', 
          'COMMENT:| Total in metric (10^X),\n',
          'GPRINT:torrent_bytes_in:AVERAGE:ROUGHLY  %7.2lf %sB total',
          'COMMENT:                        | not binary (2^X) units.',
          '-v' => 'bits/sec',
          '-t' => $title . ' traffic (year) 1 day avg',
          '-h' => 100,
          '-w' => 392,
          '-x' => 'MONTH:1:MONTH:1:MONTH:1:0:%b',
          '-l' => 0,
          '-a' => 'PNG');

        # User Graphs ...
        RRDs::graph(
          $self->{outputdir}.'/users_day.png',
          '--start' => $daystart,
          '-e' => $endtime-300,
          "DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE",
          "DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE",
          'CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF',
          'CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF',
          'CDEF:tracker_leechers=tracker_users,tracker_seeds,-',
          'CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/',
          'CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF',
          'AREA:tracker_seeds#0000ff:Seeds',
          'STACK:tracker_leechers#0000aa:Leechers',
          'COMMENT:                             +--------------------------\n',
          'COMMENT:             maximum       average       current',
          'COMMENT: | graphed '.$date.'\n',
          'COMMENT:Seeds      ',
          'GPRINT:tracker_seeds:MAX:%7.0lf',
          'GPRINT:tracker_seeds:AVERAGE:     %7.0lf',
          'GPRINT:tracker_seeds:LAST:     %7.0lf',
          'COMMENT: | endtime '.strftime('%D %H:%M:%S', localtime($endtime-300)).'\n',
          'COMMENT:Leechers   ',
          'GPRINT:tracker_leechers:MAX:%7.0lf',
          'GPRINT:tracker_leechers:AVERAGE:     %7.0lf',
          'GPRINT:tracker_leechers:LAST:     %7.0lf',
          'COMMENT: | Average Seeder to Leecher\n',
          'COMMENT:All Clients',
          'GPRINT:tracker_users:MAX:%7.0lf',
          'GPRINT:tracker_users:AVERAGE:     %7.0lf',
          'GPRINT:tracker_users:LAST:     %7.0lf',
          'GPRINT:tracker_ratio:AVERAGE: | ratio is %3.2lf\n',
          'COMMENT:                                                   |\n',
          '--alt-y-grid',
          '-v' => 'users',
          '-t' => $title . ' users (day) 5m avg',
          '-h' => 100,
          '-w' => 392,
          '-x' => 'HOUR:1:HOUR:6:HOUR:2:0:%H',
          '-l' => 0,
          '--units-exponent' => 0,
          '-a' => 'PNG');

        RRDs::graph(
          $self->{outputdir}.'/users_week.png',
          '--start' => $weekstart,
          '-e' => $endtime-1800,
          "DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE",
          "DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE",
          "DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX",
          "DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX",
          'CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF',
          'CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF',
          'CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF',
          'CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF',
          'CDEF:tracker_leechers=tracker_users,tracker_seeds,-',
          'CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-',
          'CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-',
          'CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/',
          'CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF',
          'AREA:tracker_seeds#0000ff:Seeds',
          'STACK:tracker_leechers#0000aa:Leechers',
          'STACK:tracker_users_maxtop#cccccc:peak',
          'COMMENT:                     +--------------------------\n',
          'COMMENT:             maximum       average       current',
          'COMMENT: | graphed '.$date.'\n',
          'COMMENT:Seeds      ',
          'GPRINT:tracker_seeds:MAX:%7.0lf',
          'GPRINT:tracker_seeds:AVERAGE:     %7.0lf',
          'GPRINT:tracker_seeds:LAST:     %7.0lf',
          'COMMENT: | endtime '.strftime('%D %H:%M:%S', localtime($endtime-1800)).'\n',
          'COMMENT:Leechers   ',
          'GPRINT:tracker_leechers:MAX:%7.0lf',
          'GPRINT:tracker_leechers:AVERAGE:     %7.0lf',
          'GPRINT:tracker_leechers:LAST:     %7.0lf',
          'COMMENT: | Average Seeder to Leecher\n',
          'COMMENT:All Clients',
          'GPRINT:tracker_users:MAX:%7.0lf',
          'GPRINT:tracker_users:AVERAGE:     %7.0lf',
          'GPRINT:tracker_users:LAST:     %7.0lf',
          'GPRINT:tracker_ratio:AVERAGE: | ratio is %3.2lf\n',
          'COMMENT:Peak       ',
          'GPRINT:tracker_users_max:MAX:%7.0lf',
          'COMMENT:            ',
          'GPRINT:tracker_users_max:LAST:     %7.0lf',
          'COMMENT: | 5m peak of all clients\n',
          '--alt-y-grid',
          '-v' => 'users',
          '-t' => $title . ' users (week) 30 min avg',
          '-h' => 100,
          '-w' => 392,
          '-x' => 'HOUR:6:DAY:1:DAY:1:0:%a',
          '-l' => 0,
          '--units-exponent' => 0,
          '-a' => 'PNG');

        RRDs::graph(
          $self->{outputdir}.'/users_month.png',
          '--start' => $monthstart,
          '-e' => $endtime-7200,
          "DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE",
          "DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE",
          "DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX",
          "DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX",
          'CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF',
          'CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF',
          'CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF',
          'CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF',
          'CDEF:tracker_leechers=tracker_users,tracker_seeds,-',
          'CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-',
          'CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-',
          'CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/',
          'CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF',
          'AREA:tracker_seeds#0000ff:Seeds',
          'STACK:tracker_leechers#0000aa:Leechers',
          'STACK:tracker_users_maxtop#cccccc:peak',
          'COMMENT:                     +--------------------------\n',
          'COMMENT:             maximum       average       current',
          'COMMENT: | graphed '.$date.'\n',
          'COMMENT:Seeds      ',
          'GPRINT:tracker_seeds:MAX:%7.0lf',
          'GPRINT:tracker_seeds:AVERAGE:     %7.0lf',
          'GPRINT:tracker_seeds:LAST:     %7.0lf',
          'COMMENT: | endtime '.strftime('%D %H:%M:%S', localtime($endtime-7200)).'\n',
          'COMMENT:Leechers   ',
          'GPRINT:tracker_leechers:MAX:%7.0lf',
          'GPRINT:tracker_leechers:AVERAGE:     %7.0lf',
          'GPRINT:tracker_leechers:LAST:     %7.0lf',
          'COMMENT: | Average Seeder to Leecher\n',
          'COMMENT:All Clients',
          'GPRINT:tracker_users:MAX:%7.0lf',
          'GPRINT:tracker_users:AVERAGE:     %7.0lf',
          'GPRINT:tracker_users:LAST:     %7.0lf',
          'GPRINT:tracker_ratio:AVERAGE: | ratio is %3.2lf\n',
          'COMMENT:Peak       ',
          'GPRINT:tracker_users_max:MAX:%7.0lf',
          'COMMENT:            ',
          'GPRINT:tracker_users_max:LAST:     %7.0lf',
          'COMMENT: | 5m peak of all clients\n',
          '--alt-y-grid',
          '-v' => 'users',
          '-t' => $title . ' users (month) 2 hour avg',
          '-h' => 100,
          '-w' => 392,
          '-x' => 'DAY:1:WEEK:1:WEEK:1:0:Week %W',
          '-l' => 0,
          '--units-exponent' => 0,
          '-a' => 'PNG');

        RRDs::graph(
          $self->{outputdir}.'/users_year.png',
          '--start' => $yearstart,
          '-e' => $endtime-86400,
          "DEF:tracker_users_unchecked=$self->{rrd}:users:AVERAGE",
          "DEF:tracker_seeds_unchecked=$self->{rrd}:seeds:AVERAGE",
          "DEF:tracker_users_unchecked_max=$self->{rrd}:users:MAX",
          "DEF:tracker_seeds_unchecked_max=$self->{rrd}:seeds:MAX",
          'CDEF:tracker_users=tracker_users_unchecked,UN,0,tracker_users_unchecked,IF',
          'CDEF:tracker_seeds=tracker_seeds_unchecked,UN,0,tracker_seeds_unchecked,IF',
          'CDEF:tracker_users_max=tracker_users_unchecked_max,UN,0,tracker_users_unchecked_max,IF',
          'CDEF:tracker_seeds_max=tracker_seeds_unchecked_max,UN,0,tracker_seeds_unchecked_max,IF',
          'CDEF:tracker_leechers=tracker_users,tracker_seeds,-',
          'CDEF:tracker_leechers_max=tracker_users_max,tracker_seeds_max,-',
          'CDEF:tracker_users_maxtop=tracker_users_max,tracker_users,-',
          'CDEF:tracker_ratio_unchecked=tracker_seeds,tracker_leechers,/',
          'CDEF:tracker_ratio=tracker_ratio_unchecked,UN,0,tracker_ratio_unchecked,IF',
          'AREA:tracker_seeds#0000ff:Seeds',
          'STACK:tracker_leechers#0000aa:Leechers',
          'STACK:tracker_users_maxtop#cccccc:peak',
          'COMMENT:                     +--------------------------\n',
          'COMMENT:             maximum       average       current',
          'COMMENT: | graphed '.$date.'\n',
          'COMMENT:Seeds      ',
          'GPRINT:tracker_seeds:MAX:%7.0lf',
          'GPRINT:tracker_seeds:AVERAGE:     %7.0lf',
          'GPRINT:tracker_seeds:LAST:     %7.0lf',
          'COMMENT: | endtime '.strftime('%D %H:%M:%S', localtime($endtime-86400)).'\n',
          'COMMENT:Leechers   ',
          'GPRINT:tracker_leechers:MAX:%7.0lf',
          'GPRINT:tracker_leechers:AVERAGE:     %7.0lf',
          'GPRINT:tracker_leechers:LAST:     %7.0lf',
          'COMMENT: | Average Seeder to Leecher\n',
          'COMMENT:All Clients',
          'GPRINT:tracker_users:MAX:%7.0lf',
          'GPRINT:tracker_users:AVERAGE:     %7.0lf',
          'GPRINT:tracker_users:LAST:     %7.0lf',
          'GPRINT:tracker_ratio:AVERAGE: | ratio is %3.2lf\n',
          'COMMENT:Peak       ',
          'GPRINT:tracker_users_max:MAX:%7.0lf',
          'COMMENT:            ',
          'GPRINT:tracker_users_max:LAST:     %7.0lf',
          'COMMENT: | 5m peak of all clients\n',
          '--alt-y-grid',
          '-v' => 'users',
          '-t' => $title . ' users (year) 1 day avg',
          '-h' => 100,
          '-w' => 392,
          '-x' => 'MONTH:1:MONTH:1:MONTH:1:0:%b',
          '-l' => 0,
          '--units-exponent' => 0,
          '-a' => 'PNG');
    } else {
       my $a = `$self->{useexternal}`;
    }
}

package trackalyze::Graph::Dummy;
# Blackholes all Graphing requests

no strict 'refs';

sub new { 
    my $proto = shift;
    my $class = ref($proto) || $proto;

    my $self = {
         };

    bless ($self, $class);
    return $self;
}

sub setup { 
    my $self = shift;
}

sub update { 
}

sub graph { 
}
