File Coverage

lib/Net/BitTorrent/Tracker.pm
Criterion Covered Total %
statement 20 48 41.6
branch 0 10 0.0
condition 0 11 0.0
subroutine 7 9 77.7
pod n/a
total 27 78 34.6


line stmt bran cond sub pod time code
1 20     20   270 use v5.40;
  20         77  
2 20     20   153 use feature 'class', 'try';
  20         34  
  20         3282  
3 20     20   121 no warnings 'experimental::class', 'experimental::try';
  20         37  
  20         1326  
4             #
5 20     20   106 use Net::BitTorrent::Emitter;
  20         32  
  20         1755  
6             class Net::BitTorrent::Tracker v2.0.0 : isa(Net::BitTorrent::Emitter) {
7 20     20   10392 use Net::BitTorrent::Tracker::HTTP;
  20         70  
  20         1102  
8 20     20   11498 use Net::BitTorrent::Tracker::UDP;
  20         63  
  20         1162  
9 20     20   151 use List::Util qw[shuffle];
  20         95  
  20         53980  
10             field $tiers_raw : param; # [ [url1, url2], [url3] ]
11             field $debug : param = 0;
12             field @tiers; # [ [ { obj, last_announce, interval, ... }, ... ], ... ]
13             ADJUST {
14             for my $tier_list (@$tiers_raw) {
15             my @tier;
16             for my $url (@$tier_list) {
17             push @tier,
18             {
19             obj => $self->_create_tracker($url),
20             last_announce => 0,
21             interval => 0,
22             min_interval => 0,
23             tracker_id => undef,
24             consecutive_failures => 0
25             };
26             }
27              
28             # BEP 12: shuffle within tier
29             @tier = shuffle @tier;
30             push @tiers, \@tier;
31             }
32             }
33              
34             method _create_tracker ($url) {
35             if ( $url =~ /^udp:/ ) { return Net::BitTorrent::Tracker::UDP->new( url => $url ) }
36             elsif ( $url =~ /^https?:/ ) { return Net::BitTorrent::Tracker::HTTP->new( url => $url ) }
37             $self->_emit( log => "Unsupported tracker protocol: $url", level => 'fatal' );
38             return undef;
39             }
40              
41             method announce_all ( $params, $cb = undef ) {
42             my %unique_peers;
43             my $now = time();
44              
45             # If we have multiple infohashes (hybrid), we should ideally announce all.
46             my @ihs = ref( $params->{infohash} ) eq 'ARRAY' ? @{ $params->{infohash} } : ( $params->{infohash} );
47             for my $tier (@tiers) {
48             my $tier_success = 0;
49             for ( my $i = 0; $i < scalar @$tier; $i++ ) {
50             my $entry = $tier->[$i];
51              
52             # Check interval (unless event is set like 'started', 'stopped')
53             if ( !$params->{event} && $entry->{last_announce} + ( $entry->{interval} || 60 ) > $now ) {
54             $tier_success = 1 if $i == 0 && $entry->{last_announce} > 0;
55             next;
56             }
57             my $pending_ihs = scalar @ihs;
58             for my $ih (@ihs) {
59             my $ih_params = { %$params, infohash => $ih };
60             $ih_params->{trackerid} = $entry->{tracker_id} if $entry->{tracker_id};
61 0     0     my $on_res = sub ($res) {
  0            
  0            
62 0           $entry->{last_announce} = time();
63 0   0       $entry->{interval} = $res->{interval} // 1800;
64 0   0       $entry->{min_interval} = $res->{'min interval'} // 0;
65 0 0         $entry->{tracker_id} = $res->{trackerid} if $res->{trackerid};
66 0           $entry->{consecutive_failures} = 0;
67 0   0       for my $peer ( @{ $res->{peers} // [] } ) {
  0            
68 0           my $key = "$peer->{ip}:$peer->{port}";
69 0           $unique_peers{$key} = $peer;
70             }
71              
72             # Promote successful tracker to front of tier
73 0 0         if ( $i > 0 ) {
74 0           splice( @$tier, $i, 1 );
75 0           unshift( @$tier, $entry );
76             }
77 0           $pending_ihs--;
78 0 0 0       if ( $pending_ihs <= 0 && $cb ) {
79 0           $cb->( [ values %unique_peers ] );
80             }
81             };
82             try {
83             $entry->{obj}->perform_announce( $ih_params, $on_res );
84             $tier_success = 1;
85             }
86             catch ($e) {
87             $self->_emit( log => ' [DEBUG] Announce to ' . $entry->{obj}->url . " failed: $e\n", level => 'debug' ) if $debug;
88             $entry->{consecutive_failures}++;
89             $pending_ihs--;
90             }
91             }
92             last if $tier_success;
93             }
94             }
95             return [ values %unique_peers ];
96             }
97              
98             method scrape_all ( $infohashes, $cb = undef ) {
99             my %results;
100             for my $tier (@tiers) {
101             for my $entry (@$tier) {
102 0     0     my $on_res = sub ($res) {
  0            
  0            
103 0 0         if ( ref $res->{files} eq 'ARRAY' ) {
104 0           for ( my $j = 0; $j < scalar @$infohashes; $j++ ) {
105 0           $self->_merge_scrape_stats( \%results, $infohashes->[$j], $res->{files}[$j] );
106             }
107             }
108             else {
109 0   0       for my $ih ( keys %{ $res->{files} // {} } ) {
  0            
110 0           $self->_merge_scrape_stats( \%results, $ih, $res->{files}{$ih} );
111             }
112             }
113 0 0         $cb->( \%results ) if $cb;
114             };
115             try {
116             $entry->{obj}->perform_scrape( $infohashes, $on_res );
117             }
118             catch ($e) { }
119             }
120             }
121             return \%results;
122             }
123              
124             method _merge_scrape_stats ( $results, $ih, $stats ) {
125             $results->{$ih} //= { complete => 0, downloaded => 0, incomplete => 0 };
126             $results->{$ih}{complete} = $stats->{complete} if ( $stats->{complete} // 0 ) > $results->{$ih}{complete};
127             $results->{$ih}{incomplete} = $stats->{incomplete} if ( $stats->{incomplete} // 0 ) > $results->{$ih}{incomplete};
128             $results->{$ih}{downloaded} = $stats->{downloaded} if ( $stats->{downloaded} // 0 ) > $results->{$ih}{downloaded};
129             }
130              
131             method trackers () {
132             return [
133             map {
134             map { $_->{obj}->url }
135             @$_
136             } @tiers
137             ];
138             }
139              
140             method tick ($delta) {
141             for my $tier (@tiers) {
142             for my $entry (@$tier) {
143             if ( $entry->{obj}->can('tick') ) {
144             $entry->{obj}->tick($delta);
145             }
146             }
147             }
148             }
149              
150             method add_tracker ($url) {
151              
152             # Check if already present
153             for my $tier (@tiers) {
154             for my $entry (@$tier) {
155             return if $entry->{obj}->url eq $url;
156             }
157             }
158             push @tiers,
159             [
160             { obj => $self->_create_tracker($url),
161             last_announce => 0,
162             interval => 0,
163             min_interval => 0,
164             tracker_id => undef,
165             consecutive_failures => 0
166             }
167             ];
168             }
169             };
170             #
171             1;