[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