[code-review] Module Bt::Tracker

belld96 <[email protected]> Mon, 09 Feb 2004 18:52:47 -0600
Newsgroups gmane.comp.lang.perl.code-review-ladder
Message-ID <[email protected]>
This is a module I'm working on. The namespace hasn't been registered (as far 
as I know), and it's in beta stages.

This module is an object-oriented interface to a BitTorrent tracker using DBI. 
I've tried to do some testing, but my client/server experience in Perl is 
nonexistant and CGI isn't a very good medium for testing.

The Bt::Bencode module is the same as Convert::Bencode (just trying to keep 
all the BitTorrent stuff under one namespace).

Any feedback, suggestions, comments, flames, problems, etc..., reply to 
[email protected] Don't worry, I have no ego to bruise.

i'd post to comp.lang.perl.misc or .modules but my school doesn't have a news 
server :(

Thanks in advance for putting up with my code. ;-)
-----------------------------------



{ package Bt::Tracker;

    use strict;
    use warnings;

    use Bt::Bencode qw(bencode);

    our $VERSION = 0.01;

    our $errstr;


    # Usage:
    # 	my $tracker = new Bt::Tracker(
    #		$dbh,			# REQUIRED: DBI object
    #		{ name => value } );	# OPTIONAL: Configuration
    # Returns:
    #	Tracker object
    sub new
    {
	my $class = shift;
	my ($dbh,$config) = @_;
	my $self = {};


	#$errstr = "First argument to constructor must be DBI object", return 0
	#	if !$dbh::ISA("DBI");

	$self->{dbh} 		= $dbh;


	$self->{config} 	= $config;
	$self->{config}->{interval} = $config->{interval} || 3600;
	$self->{config}->{max_peers} = $config->{max_peers} || 50;


	$self->{_file_columns}	= {
	    	info_hash	=> "VARCHAR(40) UNIQUE",
		complete 	=> "INT(9)",
		incomplete 	=> "INT(9)",
		};
	$self->{_peer_columns}	= {
	    	peer_id		=> "VARCHAR(40) UNIQUE",
		ip 		=> "VARCHAR(50)",
		port 		=> "INT(5)",
		last_update	=> "INT(15)",
		uploaded	=> "INT(18)",
		downloaded	=> "INT(18)",
		left		=> "INT(18)",
		};

	$self->{scrape_config}	= [];
	$self->{scrape_columns}	= [ "complete", "incomplete" ];

	$self->{peerlist_config} = [ "interval" ];
	$self->{peerlist_columns} = [ "ip", "port" ];

	$self->{peerlists} 	= [];



	bless $self, $class;

	# Make sure torrents table exists
	$self->create_filelist;

	return $self;
    }



    # Usage:
    #	$tracker->add_file_column(
    #		column	=> "sql prototype",	# REQUIRED: Hash of columns
    #		column	=> "sql prototype" );
    sub add_file_column
    {
	my $self	= shift;

	while (my $column = shift, my $proto = shift)
	{
	    $self->{_file_columns}->{$column} = $proto;


	    $self->{dbh}->do(qq/
		ALTER TABLE torrents ADD COLUMN $column $proto
		/ );
	}

	return 1;
    }



    # Usage:
    #	$tracker->add_peer_column(
    #		column	=> "sql prototype",	# REQUIRED: Hash of columns
    #		column	=> "sql prototype" );
    sub add_peer_column
    {
	my $self	= shift;

	while (my $column = shift, my $proto = shift)
	{
	    $self->{_peer_columns}->{$column} = $proto;


	    $self->{dbh}->do(qq/
		ALTER TABLE x$_ ADD COLUMN $column $proto/ )
		for $self->all_peerlists;
	}

	return 1;
    }



    # Usage:
    #	my @peerlists = $tracker->all_peerlists;
    sub all_peerlists
    {
	my $self	= shift;
	#my @peerlists;

	#for (@{$self->{dbh}->selectcol_arrayref('SHOW TABLES')})
	#{
	#    push @peerlists, $_ if /[0-9a-fA-F]{40}/;
	#}

	return @{$self->{peerlists}} if $self->{peerlists};
    }


    # Usage:
    #	$tracker->announce(
    #		"info_hash",			# REQUIRED: File info hash
    #		{ peer_id => value, 		# OPTIONAL: Peer data
    #		  event => value,
    #		  numwant => value, ... } );
    sub announce
    {
	my $self	= shift;
	my $info_hash	= shift;
	my $peer_data	= shift;

	$self->set_peer($info_hash,$peer_data->{peer_id},$peer_data);

	$self->delete_peer($info_hash,$peer_data->{peer_id})
		if $peer_data->{event} eq "stopped";

	return $self->peerlist(
		$info_hash,
		$peer_data->{numwant} || $self->config('max_peers') );
    }



    # Usage:
    # 	my %config = $tracker->config;
    # 	$tracker->config( interval => 500, max_peers => 50, ... );
    # 	my $interval = $tracker->config("interval");
    # Returns:
    # 	Scalar of last passed config value if odd number of parameters
    #	Hash of entire config list if even number of parameters
    sub config
    {
	my $self 	= shift;

	while (my $name = shift)
	{
	    my $value = shift;

	    $self->{config}->{$name} = $value if $value;

	    return $self->{config}->{$name} if !$value;
	}

	return %{$self->{config}};
    }



    # Usage:
    #	$tracker->create_filelist;
    sub create_filelist
    {
	my $self	= shift;
	my %columns 	= %{$self->{_file_columns}};

	$self->{dbh}->do(qq/
	    CREATE TABLE torrents (/ .
	    join(", ", map { qq/$_ $columns{$_}/ } keys(%columns)) .
	    qq/)/ );

	return 1;
    }



    # Usage:
    # 	$tracker->create_peerlist("info_hash");	# REQUIRED: File info hash
    sub create_peerlist
    {
	my $self 	= shift;
	my $info_hash	= shift;

	my %columns	= %{$self->{_peer_columns}};

	$self->{dbh}->do(
	    qq/CREATE TABLE x$info_hash ( / .
	    join ", ", map { qq/$_ $columns{$_}/ } keys(%columns) .
	    qq/) /);

	push @{$self->{peerlists}},$info_hash;

	return 1;
    }



    # Usage:
    #	$self->decode_hash($info_hash);		# REQUIRED: Packed info hash
    sub decode_hash
    {
	return (length $_[0] == 20) ? quotemeta unpack("H*",$_[0]) : $_[0];
    }



    # Usage:
    #	$tracker->delete_file("info_hash");	# REQUIRED: File info hash
    sub delete_file
    {
	my $self	= shift;
	my ($info_hash) = @_;


	$self->{dbh}->do(qq/
	    DELETE FROM torrents
	    WHERE info_hash= "$info_hash"
	    LIMIT 1/);

	return 1;
    }



    # Usage:
    #	$tracker->delete_peer(
    #		"info_hash",			# REQUIRED: File info hash
    #		"peer_id" );			# REQUIRED: Peer ID
    sub delete_peer
    {
	my $self	= shift;
	my ($info_hash,$peer_id) = @_;


	$self->{dbh}->do(qq/
	    DELETE FROM x$info_hash
	    WHERE peer_id= "$peer_id"
	    LIMIT 1
	    /);


	$self->drop_peerlist($info_hash) if !$self->find_peer($info_hash);

	return 1;
    }



    # Usage:
    #	$tracker->drop_file_column(
    #		"column",			# REQUIRED: List of columns
    #		"column" );
    sub drop_file_column
    {
	my $self	= shift;

	while (my $column = shift)
	{
	    delete $self->{_file_columns}->{$column};

	    $self->{dbh}->do( qq/ALTER TABLE torrents DROP COLUMN $column/ );
	}

	return 1;
    }



    # Usage:
    #	$tracker->drop_filelist
    sub drop_filelist
    {
	my $self	= shift;

	$self->{dbh}->do(qq/DROP TABLE torrents/);

	return 1;
    }



    # Usage:
    #	$tracker->drop_peer_column(
    #		"column",			# REQUIRED: List of columns
    #		"column" );
    sub drop_peer_column
    {
	my $self	= shift;

	while (my $column = shift)
	{
	    delete $self->{_file_columns}->{$column};


	    $self->{dbh}->do( qq/ALTER TABLE x$_ DROP COLUMN $column/ )
		for $self->all_peerlists;
	}

	return 1;
    }



    # Usage:
    #	$tracker->drop_peerlist("info_hash")	# REQUIRED: File info hash
    sub drop_peerlist
    {
	my $self	= shift;
	my ($info_hash)	= @_;

	$self->{dbh}->do( qq/DROP TABLE x$info_hash/ );

	$self->{peerlists} = [ grep(!/$info_hash/,$self->all_peerlists) ];

	return 1;
    }



    # Usage:
    #	$self->encode_hash($info_hash)		# REQUIRED: File info hash
    sub encode_hash
    {
	return (length $_[0] == 40) ? pack("H*",$_[0]) : $_[0];
    }



    # Usage:
    #	my @files = $tracker->find_file(
    #		{ where 	=> "column <= 'value' && column = 'value'",
    #		  order_by 	=> "columns [ASC|DESC]",
    #		  limit		=> 2 } ); 	# OPTIONAL: SQL info
    sub find_file
    {
	my $self 	= shift;
	my ($sql) 	= @_;

	my $query = qq/
	    SELECT info_hash
	    FROM torrents/;

	$query .= "\nWHERE $$sql{'where'}" if $$sql{'where'};
	$query .= "\nORDER BY $$sql{'order_by'}" if $$sql{'order_by'};
	$query .= "\nLIMIT $$sql{'limit'}" if $$sql{'limit'};

	my $files = $self->{dbh}->selectcol_arrayref($query);

	return @$files if $files;
    }



    # Usage:
    #	my @peers = $tracker->find_peer(
    #		"info_hash",			# REQUIRED: File info hash
    #		{ where 	=> "column <= 'value' && column = 'value'",
    #		  order_by 	=> "columns [ASC|DESC]",
    #		  limit		=> 2 } ); 	# OPTIONAL: SQL info
    sub find_peer
    {
	my $self 	= shift;
	my ($info_hash,$sql) = @_;

	my $query = qq/
	    SELECT peer_id
	    FROM x$info_hash/;

	$query .= "\nWHERE $$sql{'where'}" if $$sql{'where'};
	$query .= "\nORDER BY $$sql{'order_by'}" if $$sql{'order_by'};
	$query .= "\nLIMIT $$sql{'limit'}" if $$sql{'limit'};

	my $peers = $self->{dbh}->selectcol_arrayref($query);

	return @$peers if $peers;
    }



    # Usage:
    #	my %file = $tracker->get_file(
    #		"info_hash",			# REQUIRED: File info hash
    #		[ "column", "column" ] );	# OPTIONAL: Limit columns
    sub get_file
    {
	my $self 	= shift;
	my ($info_hash,$columns) = @_;

	my $peer = $self->{dbh}->selectrow_hashref(qq/
	    SELECT /.(($columns) ? join(",",@$columns) : "*").qq/
	    FROM torrents
	    WHERE info_hash="$info_hash"
	    LIMIT 1/);

	return %{$peer} if $peer;
    }



    # Usage:
    #	my %peer = $tracker->get_peer(
    #		"info_hash",			# REQUIRED: File info hash
    #		"peer_id",			# REQUIRED: Peer ID
    #		[ "column", "column" ] );	# OPTIONAL: Limit columns
    sub get_peer
    {
	my $self 	= shift;
	my ($info_hash,$peer_id,$columns) = @_;

	my $peer = $self->{dbh}->selectrow_hashref(qq/
	    SELECT /.(($columns) ? join(",",@$columns) : "*").qq/
	    FROM x$info_hash
	    WHERE peer_id="$peer_id"
	    LIMIT 1/);

	return %{$peer} if $peer;
    }



    # Usage:
    #	$tracker->load_config( from_file => $filename )
    # OR
    #	$tracker->load_config( from_table => $db_table )
    # OR
    #	$tracker->load_config( delete_table => $db_table )
    sub load_config
    {
	my $self	= shift;
	my $from	= shift;
	my $name	= shift;


	if ($from eq "from_file")
	{
	    open CONFIG,"<$name" or ($errstr = $! and return 0);

	    $self->{config}->{$1} = $2 while <CONFIG> =~ /^([^\t])\t(.+)$/;

	    close CONFIG or ($errstr = $! and return 0);
	}
	elsif ($from eq "from_table")
	{
	    # Select rows

	    my $AoA = $self->{dbh}->selectall_arrayref("SELECT * FROM $name");

	    $self->{config}->{$_->[0]} = $_->[1] for @{$AoA};
	}

	return 1;
    }


    # Usage:
    #	$bencoded_peerlist = $tracker->peerlist(
    #		$info_hash,			# REQUIRED: File info hash
    #		$max_peers)			# OPTIONAL: Maximum peers
    sub peerlist
    {
	my $self	= shift;
	my $info_hash	= shift;
	my $numwant	= shift;
	my $peerlist	= {};
	# Random start points
	my @rand	= rand($self->find_peer($info_hash)) x $numwant / 10;


	$peerlist->{$_} = $self->{config}->{$_} for @{$self->{peerlist_config}};


	for (0..9)
	{
	    my $start = $rand[$_];
	    my $limit = ($_ == 9) ? $numwant / 9 : $numwant % 9;
	    my @peers = $self->find_peer($info_hash,{limit => "$limit,$start"});

	    for my $peer_id (@peers)
	    {
		my %peer_data = $self->get_peer($info_hash,$peer_id);

		# Remove lost peers
		$self->delete_peer($info_hash,$peer_id) and next
			if ($peer_data{last_update} < time - $self->{config}->{interval} * 2);

		push @{$peerlist->{peers}}, {
		    "peer id"	=> $self->encode_hash($peer_id),
		    ip		=> $peer_data{ip},
		    port	=> $peer_data{port},
		    };
	    }

	}


	return bencode($peerlist);
    }



    # Usage:
    #	$tracker->save_config( to_file => $filename )
    # OR
    #	$tracker->save_config( to_table => $db_table )
    sub save_config
    {
	my $self	= shift;
	my $to		= shift;
	my $name	= shift;

	my %config 	= %{$self->{config}};

	if ($to eq "to_file")
	{
	    open CONFIG,"<$name" or ($errstr = $! and return 0);

	    print CONFIG "$_\t$config{$_}\n" for keys %config;

	    close CONFIG or ($errstr = $! and return 0);
	}
	elsif ($to eq "to_table")
	{
	    # Create table
	    $self->{dbh}->do( qq/CREATE TABLE IF NOT EXISTS $name (
		    name VARCHAR(255),
		    value TEXT
		)/ );

	    my $add_row = $self->{dbh}->prepare( qq/
			INSERT INTO $name
			(name,value)
			VALUES (?,?)
		    /);

	    # Insert rows
	    for (keys %{$self->{config}})
	    {
		my $config 	= $_;
		my $value 	= $self->{config}->{$_};

		$add_row->execute($config,$value);
	    }

	    $add_row->finish;
	}

	return 1;
    }



    # Usage:
    #	$tracker->scrape(@info_hashes)		# OPTIONAL: List of files
    sub scrape
    {
	my $self	= shift;
	my @files	= @_ || $self->all_peerlists;
	my $scrape	= {};


	for my $info_hash (@files)
	{
	    my $incomplete = $self->find_peer($info_hash,{where => "left > 0"});
	    my $complete = $self->find_peer($info_hash,{where => "left = 0"});

	    my $file_data = {complete => $complete, incomplete => $incomplete};

	    $self->set_file($info_hash,$file_data);

	    $scrape->{$self->encode_hash($info_hash)} = $file_data;
	}

	return bencode($scrape);
    }



    # Usage:
    #	$tracker->set_file(
    #		"info_hash",			# REQUIRED: File info hash
    #		{ column => value, ... } );	# REQUIRED: Columns to set
    sub set_file
    {
	my $self 	= shift;
	my ($info_hash,$file) = @_;
	my $query;

	if ($self->get_file($info_hash))
	{
	    $query = qq/UPDATE torrents SET / .
		    join(", ",map { qq/$_="$file->{$_}"/ } keys %$file) .
		    qq/ WHERE info_hash="$info_hash"/;
	}
	else
	{
	    $file->{info_hash} = $info_hash;

	    $query = qq/INSERT INTO torrents (/ .
		    join(", ", map { qq/$_/ } keys %$file) .
		    qq/) VALUES (/ .
		    join(", ", map { qq/"$file->{$_}"/ } keys %$file) .
		    qq/)/;
	}

	$self->{dbh}->do($query);

	return 1;
    }



    # Usage:
    #	$tracker->set_peer(
    #		"info_hash",			# REQUIRED: File info hash
    #		"peer_id",			# REQUIRED: Peer ID
    #		{ column => value, ... } );	# REQUIRED: Columns to set
    sub set_peer
    {
	my $self = shift;
	my ($info_hash,$peer_id,$peer) = @_;
	my $query;


	$self->create_peerlist($info_hash) if !$self->find_peer($info_hash);


	if ($self->get_peer($info_hash,$peer_id))
	{
	    $query = qq/UPDATE x$info_hash SET / .
		    join(",",map { qq/$_="$peer->{$_}"/ } keys %$peer) .
		    qq/ WHERE peer_id="$peer_id"/;
	}
	else
	{
	    $peer->{peer_id} = $peer_id;

	    $query = qq/INSERT INTO x$info_hash (/ .
		    join(",", map { qq/$_/ } keys %$peer) .
		    qq/) VALUES (/ .
		    join(",", map { qq/"$peer->{$_}"/ } keys %$peer) .
		    qq/)/;
	}

	$self->{dbh}->do($query);

	return 1;
    }


    1;
}




=pod

=head1 NAME

Bt::Tracker - Object-oriented interface for a BitTorrent tracker.



=head1 SYNOPSIS

use DBI; my $dbh = DBI->connect(...);
use Bt::Tracker


$tracker = new Bt::Tracker $dbh, \%config;

$info_hash = $tracker->decode_hash($packed_info_hash)

$bencoded_peerlist = $tracker->announce($info_hash, $peer_id, \%peer_data);
$bencoded_fileinfo = $tracker->scrape(@info_hashes);

$bencoded_peerlist = $tracker->peerlist($info_hash, $max_peers);


$tracker->config($column => $value);
$value = $tracker->config($column);
%config = $tracker->config;

$tracker->save_config(to_file => $config_file);
$tracker->save_config(to_table => $db_table);
$tracker->load_config(from_file => $config_file);
$tracker->load_config(from_table => $db_table);


$tracker->add_file_column($column, $sql_prototype);
$tracker->add_peer_column($column, $sql_prototype);

$tracker->drop_file_column($column);
$tracker->drop_peer_column($column);


%file_info = $tracker->get_file($info_hash);
$tracker->set_file($info_hash, \%file_info);
$tracker->delete_file($info_hash);


%peer_info = $tracker->get_peer($info_hash, $peer_id);
$tracker->set_peer($info_hash, $peer_id, \%peer_info);
$tracker->delete_peer($info_hash, $peer_id);


@info_hashes = $tracker->find_file({ where => $sql_where, limit => $sql_limit, 
order_by => $sql_order_by });

@peer_ids = $tracker->find_peer($info_hash,{ where => $sql_where, limit => 
$sql_limit, order_by => $sql_order_by });


$tracker->create_peerlist($info_hash);
$tracker->drop_peerlist($info_hash);
@active_info_hashes = $tracker->all_peerlists;



=head1 DESCRIPTION

This module is an object-oriented interface to a BitTorrent tracker. The 
BitTorrent tracker keeps account of all the users that want a file and sends 
out lists of sources to download the file from.

BitTorrent clients periodically announce themselves to the tracker, sending it 
a small amount of data. The tracker then sends the client a random list of 
peers.

The scrape interface allows remote users to get some statistics from the 
tracker.

For more information, read the method descriptions (especially C<announce>, 
C<scrape>, C<peerlist>, and C<config>), and look at the SEE ALSO section for 
links to information on the BitTorrent protocol.



=head1 METHODS

=head2 new

=over 4

$tracker = new Bt::Tracker($dbh,\%config);

=back

Creates a new tracker object. The first argument should be a DBI object, see 
perldoc DBI for more information. The second argument is an optional 
configuration hashref, see C<config> for more information.

If successful, C<new> returns the object. If the creation fails (somehow), it 
returns false and places a string describing the error in 
$Bt::Tracker::errstr.

=head2 add_file_column

=over 4

$tracker->add_file_column(
		"downloads" 	=> "INT(8) UNSIGNED",
		"name" 		=> "CHAR(30)",
		"description" 	=> "TEXT",
		);

=back

Adds a column to the file information table. The first argument is the name of 
the column, the second argument is the SQL column prototype. You can add more 
than one column at the same time to save typing (but not database queries).

For the SQL-illiterate, the most useful column prototypes are C<INT> for 
integer values, C<CHAR> for short character values, and C<TEXT> for character 
values longer than 255 characters.

LINK TO SQL DOCUMENTATION

=head2 add_peer_column

=over 4

$tracker->add_peer_column(
		"avg_speed" 	=> "INT(8) UNSIGNED",
		"user_agent" 	=> "CHAR(30)",
		);

=back

Adds a column to all peerlist tables. New peerlists will be created with the 
new columns, and existing tables will be altered to include them. The first 
argument is the name of the column, the second argument is the SQL column 
prototype. You can add more than one column at the same time to save typing 
(but not database queries).

For the SQL-illiterate, see add_file_column for some simple SQL column 
prototypes.

=head2 all_peerlists

=over 4

@active_files = $tracker->all_peerlists;

=back

Returns a list of all peerlist info hashes. These can be matched with info 
hashes from the file summary table.

=head2 announce

=over 4

$bencoded_peerlist = $tracker->announce($info_hash,\%peer_data);

=back

An announce is basically set_peer and peerlist. The first argument to announce 
should be an info hash, the second argument is a hashref of peerdata. Announce 
returns a bencoded peerlist (the same as peerlist).

Some keys in the peer data have special meaning to announce. The B<event> key 
can be one of the following values: "started" means this client is joining or 
rejoining the swarm, "completed" means this client is done downloading and is 
now seeding, "stopped" means this client is leaving the swarm, and a null 
value (the most common) is a normal announce. The module will automatically 
delete the peer from the peerlist when event is "stopped," but it will still 
return a peerlist. The original tracker still sends the client a peerlist when 
event=stopped, but it's probably not necessary. The B<numwant> key is how many 
peers the client wants. Not all clients send numwant, so it defaults to the 
max_peers config value. If numwant is greater than max_peers, we'll use 
max_peers.

=head2 config

=over 4

$value = $tracker->config("name");			# Get one config value

$tracker->config(name => "value", name => "value");	# Set config

%config = $tracker->config;				# Get all config values

=back

The config method can be used to set and get config values. If config is 
passed an odd number of parameters, it will return the value of the last named 
config key. If passed an even number of parameters, it will return the entire 
config hash.

Some necessary config values:

=over 4

=item interval

The amount of time, in seconds, that a client should wait before re-announcing

=item max_peers

The maximum amount of peers to send to a client.

=back

=head2 create_filelist

=over 4

$tracker->create_filelist

=back

Creates the file summary table. This function is called automatically when the 
tracker object is created.

=head2 create_peerlist

=over 4

$tracker->create_peerlist($info_hash);

=back

Creates a database table for a peerlist named $info_hash. Uses the current 
prototypes specified using add_file_column and drop_file_column.

Normally you shouldn't need to call this function, as the set_peer function 
will call it automatically if the peerlist doesn't exist.

=head2 decode_hash

=over 4

$tracker->decode_hash($info_hash);

=back

Unpacks the binary string that BitTorrent clients send into a 40-character 
hexidecimal string that's easier to use. Only a packed hash will be unpacked. 
If an unpacked hash is passed, will return the same value.

** QUESTION: Should the module call this function automatically, since it is 
necessary to do? or should we require the programmer to call it explicitly? **

=head2 delete_file

=over 4

$tracker->delete_file($info_hash);

=back

Removes a file from the file summary table.

=head2 delete_peer

=over 4

$tracker->delete_peer($info_hash,$peer_id);

=back

Removes a peer from a peerlist.

=head2 drop_file_column

=over 4

$tracker->drop_file_column(@column_names)

=back

Drops a list of columns from the file summary table. Any data in the columns 
will be deleted.

=head2 drop_filelist

=over 4

$tracker->drop_filelist

=back

Drops the file summary table. Any data in the table is deleted, so be very 
sure before you use this.

=head2 drop_peer_column

=over 4

$tracker->drop_peer_column(@column_names)

=back

Drops a list of columns from peerlist tables. Any data in the columns will be 
deleted.

=head2 drop_peerlist

=over 4

$tracker->drop_peerlist($info_hash)

=back

Drops a peerlist table. Any data in the table is deleted. This function is  
called automatically when the last peer in the list is deleted.

=head2 encode_hash

=over 4

$tracker->encode_hash($info_hash)

=back

Packs a 40-character hexidecimal string into a 20-byte binary structure to be 
sent to BitTorrent clients. C<announce>, C<peerlist>, and C<scrape> all call 
this function automatically, so you shouldn't need to.

=head2 find_file

=over 4

@files = $tracker->find_file(
	{ where 	=> "column <= 'value' && column = 'value'",
	  order_by 	=> "columns [ASC|DESC]",
	  limit		=> 2 } ); 	# OPTIONAL: SQL info

=back

Find a list of files from the file summary table. The first argument is a 
reference to a hash of SQL data with the following keys:

=over 4

=item where

An SQL C<where> clause of the form C<column OP value [CONJ column OP value]> 
where C<column> is the column name, C<OP> is an SQL operator, C<value> is the 
value to test, and C<CONJ> is an optional conjunction for multiple columns. 
See ** LINK TO DATA **

=item order_by

An SQL C<ORDER BY> clause of the form C<column [DESC|ASC] [, column 
[DESC|ASC], ...] where C<column> is the column name, C<DESC> means to sort 
descending, and C<ASC> means to sort ascending (which is the default action 
and redundant to specify). You can specify multiple columns by separating them 
with commas.

=item limit

An SQL C<LIMIT> clause of the form C<max_rows,start_row> where C<max_rows> is 
the maximum number of rows to return and C<start_row> is the row number to 
start from. SQL numbers rows beginning from 0. ** <-- VERIFY THIS!

=back

=head2 find_peer

=over 4

@files = $tracker->find_peer(
	$info_hash,			# REQUIRED: File info hash
	{ where 	=> "column <= 'value' && column = 'value'",
	  order_by 	=> "columns [ASC|DESC]",
	  limit		=> 2 } ); 	# OPTIONAL: SQL info

=back

Find a list of peers from a peerlist. The first argument is a file info_hash. 
The second argument is a reference to a hash of SQL data. See the find_file 
function (above) for details about this hash.

=head2 get_file

=over 4

%file_data = $tracker->get_file(
	$info_hash,			# REQUIRED: File info hash
	\@columns )			# OPTIONAL: Columns to return

=back

Get a row of data from the file summary table. The first argument is the file 
info hash to get. The optional second argument is a reference to an array of 
column names to return.

=head2 get_peer

=over 4

%peer_data = $tracker->get_peer(
	$info_hash,			# REQUIRED: File info hash
	$peer_id,			# REQUIRED: Peer ID
	\@columns )			# OPTIONAL: Columns to return

=back

Get a row of data from a peerlist. The first argument is the file info hash. 
The second argument is the peer id to get. The optional third argument is a 
reference to an array of columns names to return.

=head2 load_config

=over 4

$tracker->load_config( from_file => $filename );	# Read from a file

$tracker->load_config( from_table => $table_name );	# Read from a db table

=back

Loads config from a more permanent location. The first argument is either 
"from_file" or "from_table", specifying where to load the config values from. 
The second argument is the path/filename or the table name to load from. The 
config values can then be accessed with the C<config> method.

=head2 peerlist

=over 4

$bencoded_peerlist = $tracker->peerlist($info_hash, $num_peers);

=back

Peerlist gathers a random list of peers for a file. The first argument is an 
info_hash, the second (optional) argument is the number of peers to return. If 
the second argument is missing, it defaults to the max_peers config value.

The randomness is determined by picking ($num_peers)/10 random start points 
and taking 10 peers from each start point.

Peerlist also deletes any lost peers. A peer is considered "lost" if they 
haven't announced in C<interval*2> seconds.

=head2 save_config

=over 4

$tracker->save_config( to_file => $filename );		# Write to a file

$tracker->save_config( to_table => $table_name );	# Write to a db table

=back

Saves config to a more permanent location. The first argument is either 
"to_file" or "to_table", specifying where to save the config values to. The 
second argument is either the path/filename or the table name to save to.

=head2 scrape

=over 4

$bencoded_scrape = $tracker->scrape;			# Scrape all files

$bencoded_scrape = $tracker->scrape(@info_hashes);	# Scrape list of files

=back

Scrape gathers information about the files currently being tracked. The method 
accepts a list of info hashes as an optional argument.

Scrape automatically updates two columns in the file summary table: 
C<complete>, the number of completed peers, and C<incomplete>, the number of 
incomplete peers.

=head2 set_file

=over 4

$tracker->set_file($info_hash, \%file_data);

=back

Sets columns in the file summary table. The first argument is the info_hash of 
the file to be set. The second argument is a hash reference of data to set. 
Any keys in the hash that do not correspond to a column that hasn't been added 
with add_file_column will simply be disgarded.

=head2 set_peer

=over 4

$tracker->set_peer($info_hash, $peer_id, \%peer_data);

=back

Sets columns in the peerlist table. The first argument is the info_hash of the 
peerlist. The second argument is the peer_id of the peer to be set. The third 
argument is a hash reference of data to set. Any keys in the hash that do not 
correspond to a column that hasn't been added with add_peer_column will simply 
be disgarded.



=head1 TRACKER PROTOCOL

Here's a quick (and probably inaccurate) summary of the BitTorrent 
Client-to-Tracker protocol.

=head2 announce

Clients announce themselves at intervals, using HTTP GET and requesting the 
announce page.

 GET announce?query_string

The query_string has the following standard fields.

=over 4

=item info_hash

A 20-byte SHA1 hash of the info section of the .torrent file. This binary 
string will be unpacked by Bt::Tracker into a 40-character hexadecimal string 
if it's not unpacked already.

=item peer_id

A 20-byte binary string identifying the peer. This usually changes each time 
the client is started, but will remain constant throughout the session.

=item event

If this item doesn't exist, nothing special happens. Otherwise, it can be 
"started", "completed", or "stopped". "started" means this client is joining 
or rejoining the swarm, "completed" means this client is done downloading and 
is now seeding, "stopped" means this client is leaving the swarm.

=item uploaded

The number of bytes uploaded by the peer.

=item downloaded

The number of bytes downloaded by the peer. It is possible to download more 
data than the file, as some pieces may be corrupted.

=item left

The number of bytes that the peer needs to complete their download.

=back

So the client sends something like this:

 GET announce?info_hash=...&peer_id=...&...&event=started

The tracker will process this announce and send back a bencoded dictionary 
with the following structure (you can feed this structure to Bt::Bencode).

=over 4

=item interval

The amount of time (in seconds) the client should wait before announcing 
again.

=item

** PEERLIST DATA

=back

So the tracker sends this bencoded data back to the client and the client 
connects to the peers to begin downloading the file.

=head2 scrape

The scrape interface is used by websites and some clients to get data about 
some or all of the files a tracker is tracking.

Using HTTP GET, the client requests the scrape page.

 GET scrape?info_hash=...&info_hash=...

The info_hash part of the query string is optional and allows a client to help 
save the tracker from expensive scrape requests. A scrape with no query string 
will return every file being tracked, which could get to be quite lengthy.

The tracker sends back a bencoded dictionary with the following structure. 
(you can feed this structure to Bt::Bencode)

=over 4

** SCRAPE DATA

=back

For a more thorough explanation may I suggest the SEE ALSO section?
For information on bencoding, read perldoc Bt::Bencode

=head1 BUGS

None known. Report any to <doug-41bi/[email protected]>

=head1 TO DO

=head1 SEE ALSO

The BitTorrent protocol.

=head1 COPYRIGHT

Copyright (c)2004, Doug Bell. This module is free software and can be 
modified/distributed under the same terms as Perl itself.

=head1 AUTHORS

Doug Bell <doug-41bi/[email protected]>. Please e-mail with any problems, 
suggestions, bugs, flames, rants, raves, inaccuracies, ideosyncrasies, 
idiocies, wedding proposals, or other.

=head1 CHANGES

Only program-altering changes need apply.

=head1 HISTORY

v0.01 - 02/02/2004 - Released to comp.lang.perl for testing and feedback

=cut