File Coverage

blib/lib/WWW/Newzbin.pm
Criterion Covered Total %
statement 35 194 18.0
branch 6 130 4.6
condition 0 55 0.0
subroutine 8 18 44.4
pod 7 7 100.0
total 56 404 13.8


line stmt bran cond sub pod time code
1             package WWW::Newzbin;
2              
3 1     1   32606 use 5.005;
  1         5  
  1         463  
4 1     1   16 use strict;
  1         3  
  1         46  
5 1     1   5 use warnings;
  1         8  
  1         184  
6              
7 1     1   7 use Carp qw(carp croak);
  1         2  
  1         289  
8 1     1   1157 use WWW::Newzbin::Constants qw(:all);
  1         3  
  1         406  
9              
10 1     1   1571 use LWP::UserAgent;
  1         108100  
  1         3074  
11              
12             our $VERSION = '0.07';
13              
14             # lwp::useragent object for communicating with newzbin
15             my $ua = LWP::UserAgent->new(
16             agent => "WWW::Newzbin/$VERSION"
17             );
18              
19             #=============================================================================#
20              
21             # internal carp() - gives us a chance to stop them if user has requested no warnings
22             sub _carp {
23 0     0   0 my $self = shift;
24 0         0 my $err = shift;
25            
26 0 0       0 carp("WARNING: WWW::Newzbin " . $err) unless $self->{param}->{nowarnings};
27             }
28              
29             sub _croak {
30 0     0   0 my $self = shift;
31 0         0 my $err = shift;
32            
33 0         0 croak("ERROR: WWW::Newzbin " . $err);
34             }
35              
36             sub new {
37 2     2 1 834 my $class = shift;
38 2         5 my $self = {};
39              
40 2         6 bless($self, $class);
41              
42 2 50       9 if (my $err = $self->_init(@_)) {
43 0         0 $self->_croak("new(): Cannot initialise object: $err");
44 0         0 return undef;
45             } else {
46 2         8 return $self;
47             }
48             }
49              
50             sub _init {
51 2     2   3 my $self = shift;
52            
53 2         4 (%{$self->{param}}) = @_;
  2         19  
54            
55             # dismiss unrecognised constructor parameters
56 2         4 foreach my $param (keys %{$self->{param}}) {
  2         19  
57 6 50       35 if ($param !~ /^(username|password|nowarnings|proxy)$/) {
58 0         0 $self->_carp("new(): Unknown constructor parameter '$param'");
59 0         0 delete $self->{param}->{"$param"};
60             }
61             }
62            
63             # check for existence of required parameters
64 2         6 foreach my $required (qw(username password)) {
65 4 50       16 if (!$self->{param}->{"$required"}) {
66 0         0 return "missing required parameter '$required'";
67             }
68             }
69              
70             # if a proxy has been specified, pass it to lwp::useragent
71 2 100       14 if (exists $self->{param}->{proxy}) {
72 1         3 eval { $ua->proxy("http", $self->{param}->{proxy}); };
  1         8  
73 1 50       162 if ($@) {
74 0           $self->_carp("new(): Cannot use proxy '$self->{param}->{proxy}'");
75 0           delete $self->{param}->{proxy};
76             }
77             }
78             }
79              
80             sub _set_error {
81 0     0     my $self = shift;
82 0           $self->{error}->{code} = shift;
83 0           $self->{error}->{message} = shift;
84             }
85              
86             sub error_code {
87 0     0 1   my $self = shift;
88 0   0       return $self->{error}->{code} || undef;
89             }
90              
91             sub error_message {
92 0     0 1   my $self = shift;
93 0   0       return $self->{error}->{message} || undef;
94             }
95              
96             sub lwp_useragent {
97 0     0 1   my $self = shift;
98 0           return $ua;
99             }
100              
101             #-----------------------------------------------------------------------------#
102              
103             # interface to v3's filefind api
104             sub search_files {
105 0     0 1   my $self = shift;
106 0           $self->_set_error(undef, undef);
107 0           $self->_set_search_files_total(undef);
108            
109 0           my (%criteria) = @_;
110            
111             # check for unrecognised parameters
112 0           foreach my $key (keys %criteria) {
113 0 0         if ($key !~ /^(query|category|group|retention|minsize|maxsize|filetype|resultoffset|resultlimit|sortfield|sortorder)$/) {
114 0           $self->_carp("search_files(): Unknown parameter '$key'");
115 0           delete $criteria{"$key"};
116             }
117             }
118            
119             # check for required parameters
120 0           foreach my $req (qw(query)) {
121 0 0         if (!$criteria{"$req"}) {
122 0           $self->_croak("search_files(): Missing required parameter '$req'");
123             }
124             }
125              
126             # build post request for filefind
127 0           my %post = (
128             username => $self->{param}->{username},
129             password => $self->{param}->{password},
130             query => $criteria{query},
131             filetype => $criteria{filetype},
132             );
133              
134             # validation for "category"
135 0 0         if (exists $criteria{category}) {
136 0 0         if (ref($criteria{category}) =~ /^ARRAY/) {
    0          
137 0           $post{category} = join(",", @{$criteria{category}});
  0            
138             } elsif (!ref($criteria{category})) {
139 0           $post{category} = $criteria{category};
140             } else {
141 0           $self->_carp("search_files(): 'category' parameter must be a string or arrayref of strings; parameter not included in search");
142             }
143             }
144              
145             # validation for "group"
146 0 0         if (exists $criteria{group}) {
147 0 0         if (ref($criteria{group}) =~ /^ARRAY/) {
    0          
148 0           $post{group} = join(",", @{$criteria{group}});
  0            
149             } elsif (!ref($criteria{group})) {
150 0           $post{group} = $criteria{group};
151             } else {
152 0           $self->_carp("search_files(): 'group' parameter must be a string or arrayref of strings; parameter not included in search");
153             }
154             }
155              
156             # validation for "retention"
157 0 0         if (exists $criteria{retention}) {
158 0 0 0       if (($criteria{retention} =~ /^\d+$/) && ($criteria{retention} > 0)) {
159 0           $post{retention} = $criteria{retention};
160             } else {
161 0           $self->_carp("search_files(): 'retention' parameter must be a positive integer; parameter not included in search");
162             }
163             }
164              
165             # validation for "minsize"
166 0 0         if (exists $criteria{minsize}) {
167 0 0 0       if (($criteria{minsize} =~ /\D/) || ($criteria{minsize} < 0)) {
    0 0        
168 0           $self->_carp("search_files(): 'minsize' parameter must be a positive integer; parameter not included in search");
169 0           delete $criteria{minsize};
170             } elsif ((exists $criteria{maxsize}) && ($criteria{minsize} > $criteria{maxsize})) {
171 0           $self->_carp("search_files(): 'minsize' parameter must be less than 'maxsize' parameter; both parameters not included in search");
172 0           delete $criteria{minsize};
173 0           delete $criteria{maxsize};
174             } else {
175 0           $post{bytesmin} = $criteria{minsize};
176             }
177             }
178            
179             # validation for "maxsize"
180 0 0         if (exists $criteria{maxsize}) {
181 0 0 0       if (($criteria{maxsize} =~ /\D/) || ($criteria{maxsize} < 0)) {
    0 0        
182 0           $self->_carp("search_files(): 'maxsize' parameter must be a positive integer; parameter not included in search");
183 0           delete $criteria{maxsize};
184             } elsif ((exists $criteria{minsize}) && ($criteria{minsize} > $criteria{maxsize})) {
185 0           $self->_carp("search_files(): 'minsize' parameter must be less than 'maxsize' parameter; both parameters not included in search");
186 0           delete $criteria{minsize};
187 0           delete $criteria{maxsize};
188             } else {
189 0           $post{bytesmax} = $criteria{maxsize};
190             }
191             }
192              
193             # validation for "resultoffset"
194 0 0         if (exists $criteria{resultoffset}) {
195 0 0 0       if (($criteria{resultoffset} =~ /\D/) || ($criteria{resultoffset} < -1)) {
196 0           $self->_carp("search_files(): 'resultoffset' parameter must be an integer >= 0; offset/limit parameters not included in search");
197 0           delete $criteria{resultlimit};
198             } else {
199 0           $post{offset} = $criteria{resultoffset};
200             }
201             }
202              
203             # validation for "resultlimit"
204 0 0         if (exists $criteria{resultlimit}) {
205 0 0 0       if (($criteria{resultlimit} =~ /\D/) || ($criteria{resultlimit} < 0)) {
206 0           $self->_carp("search_files(): 'resultlimit' parameter must be a positive integer; offset/limit parameters not included in search");
207 0           delete $post{offset};
208             } else {
209 0           $post{limit} = $criteria{resultlimit};
210             }
211             }
212              
213             # validation for "sortfield"
214 0 0         if (exists $criteria{sortfield}) {
215 0 0 0       if (
      0        
216             ($criteria{sortfield} ne NEWZBIN_SORTFIELD_DATE) &&
217             ($criteria{sortfield} ne NEWZBIN_SORTFIELD_SUBJECT) &&
218             ($criteria{sortfield} ne NEWZBIN_SORTFIELD_FILESIZE)
219             ) {
220 0           $self->_carp("search_files(): 'sortfield' parameter does not have an allowed value; sort parameters not included in search");
221 0           delete $criteria{sortorder};
222             } else {
223 0           $post{sortfield} = $criteria{sortfield};
224             }
225             }
226              
227             # validation for "sortorder"
228 0 0         if (exists $criteria{sortorder}) {
229 0 0 0       if (
230             ($criteria{sortorder} ne NEWZBIN_SORTORDER_ASC) &&
231             ($criteria{sortorder} ne NEWZBIN_SORTORDER_DESC)
232             ) {
233 0           $self->_carp("search_files(): 'sortorder' parameter does not have an allowed value; sort parameters not included in search");
234 0           delete $post{sortfield};
235             } else {
236 0           $post{sortorder} = $criteria{sortorder};
237             }
238             }
239              
240             # now we're ready to query newzbin
241 0 0 0       my $response = $ua->post(
242             "http://v3.newzbin.com/api/filefind3/",
243             \%post,
244             "Content-type" => "application/x-www-form-urlencoded"
245             ) or $self->_set_error(-1, "Could not send HTTP POST request to Newzbin's FileFind API") and return undef;
246            
247             # check for valid response
248 0 0         if (!$response->is_success) {
249 0 0         if ($response->code == 500) {
    0          
    0          
250 0           $self->_set_error(-1, "Newzbin's FileFind API is currently unavailable");
251             } elsif ($response->code == 403) {
252 0           $self->_set_error(-2, "Invalid Newzbin login credentials given");
253             } elsif ($response->code == 402) {
254 0           $self->_set_error(-3, "This is not a Newzbin Premium account");
255             } else {
256 0           $self->_set_error(-1, "Invalid response from Newzbin's FileFind API");
257             }
258            
259 0           return undef;
260             }
261            
262             # were there results for the given query?
263 0 0         if ($response->code == 204) {
264 0           $self->_set_error(-4, "No search results found");
265 0           return undef;
266             }
267            
268             # process results
269 0           my @raw_results = split(/\n/, $response->content);
270              
271             # first line holds the total number of results that would have been returned if offset/limit were not present
272 0           my $total_results = shift(@raw_results);
273 0           $total_results =~ s/^TOTAL=//i;
274 0           $self->_set_search_files_total($total_results);
275              
276 0           my @results;
277 0           foreach (@raw_results) {
278 0           my @result = split(/\t/, $_);
279              
280 0           push(@results, {
281             fileid => $result[0],
282             subject => $result[1],
283             posttime => $result[2],
284             filesize => $result[3],
285             author => $result[4],
286             groups => [ split(/,/, $result[5]) ],
287             });
288             }
289              
290 0           return @results;
291             }
292              
293             sub _set_search_files_total {
294 0     0     my $self = shift;
295 0           $self->{searchfiles}->{total} = shift;
296             }
297              
298             sub search_files_total {
299 0     0 1   my $self = shift;
300 0   0       return $self->{searchfiles}->{total} || undef;
301             }
302              
303             #-----------------------------------------------------------------------------#
304              
305             # interface to v3's directnzb api
306             sub get_nzb {
307 0     0 1   my $self = shift;
308 0           $self->_set_error(undef, undef);
309            
310 0           my (%request) = @_;
311            
312             # check for unrecognised parameters
313 0           foreach my $key (keys %request) {
314 0 0         if ($key !~ /^(reportid|fileid|nogzip|leavegzipped)$/) {
315 0           $self->_carp("get_nzb(): Unknown parameter '$key'");
316 0           delete $request{"$key"};
317             }
318             }
319            
320             # can only specify either reportid or fileid, not both...
321 0 0 0       if ((exists $request{reportid}) && (exists $request{fileid})) {
    0 0        
322 0           $self->_croak("get_nzb(): 'reportid' and 'fileid' cannot both be passed as parameters");
323             # ...but still must supply one of them
324             } elsif ((!exists $request{reportid}) && (!exists $request{fileid})) {
325 0           $self->_croak("get_nzb(): must supply either 'reportid' or 'fileid' as a parameter");
326             }
327            
328 0 0         if (exists $request{reportid}) {
    0          
329 0 0         if ($request{reportid} =~ /\D/) {
330 0           $self->_croak("get_nzb(): 'reportid' parameter must be an integer");
331             }
332             } elsif (exists $request{fileid}) {
333             # an integer or an arrayref of integers is acceptable here
334 0 0         if (ref($request{fileid}) =~ /^ARRAY/) {
335 0           foreach my $fid (@{$request{fileid}}) {
  0            
336 0 0         if ($fid =~ /\D/) {
337 0           $self->_croak("get_nzb(): 'fileid' parameter must be an integer or arrayref of integers");
338             }
339             }
340             } else {
341 0 0         if ($request{fileid} =~ /\D/) {
342 0           $self->_croak("get_nzb(): 'fileid' parameter must be an integer or arrayref of integers");
343             }
344             }
345             }
346            
347             # check for compress::zlib
348 0           eval { require Compress::Zlib; };
  0            
349 0 0         my $compress_zlib = ($Compress::Zlib::VERSION ? 1 : 0);
350            
351 0           my $response = $ua->post(
352             "http://v3.newzbin.com/api/dnzb/",
353             {
354             username => $self->{param}->{username},
355             password => $self->{param}->{password},
356             reportid => (exists $request{reportid} ? $request{reportid} : ""),
357 0 0 0       fileid => (exists $request{fileid} ? (ref($request{fileid}) =~ /^ARRAY/ ? join(",", @{$request{fileid}}) : $request{fileid}) : ""),
    0          
    0          
    0          
358             },
359             # if compress::zlib is installed and gzip compression hasn't been disabled, send a header that will result in a gzipped response
360             "Accept-Encoding" => (((!$request{nogzip}) && ($compress_zlib)) ? "gzip" : "")
361             );
362            
363             # examine http response code, filter out errors
364 0 0         if (!$response->is_success) {
365 0 0         if ($response->code == 500) {
    0          
    0          
    0          
    0          
    0          
    0          
366             # newzbin server error
367 0           $self->_set_error(-1, "Invalid response from Newzbin's DirectNZB API");
368             } elsif ($response->code == 503) {
369             # directnzb down for maintenance
370 0           $self->_set_error(-1, "Newzbin's DirectNZB API is currently unavailable");
371              
372             # examine newzbin-specific response code, filter out errors
373             } elsif ($response->header("X-DNZB-RCode") =~ /^400/) {
374             # missing parameters
375 0           $self->_croak("get_nzb(): must supply either 'reportid' or 'fileid' as a parameter; 'reportid' must be an integer, and 'fileid' must be an integer or arrayref of integers");
376             } elsif ($response->header("X-DNZB-RCode") =~ /^401/) {
377             # invalid credentials
378 0           $self->_set_error(-2, "Invalid Newzbin login credentials given");
379             } elsif ($response->header("X-DNZB-RCode") =~ /^402/) {
380             # no premium credit
381 0           $self->_set_error(-3, "This is not a Newzbin Premium account");
382             } elsif ($response->header("X-DNZB-RCode") =~ /^404/) {
383             # data unavailable
384 0           $self->_set_error(-4, "Data requested does not exist or is unavailable");
385             } elsif ($response->header("X-DNZB-RCode") =~ /^450/) {
386             # too many nzb download requests
387             # get number of seconds user has to wait
388 0           my $timeout = $response->header("X-DNZB-RText");
389 0           $timeout =~ s/.*Try Later, wait (\d+) second.*/$1/i;
390 0 0         $self->_set_error(($timeout =~ /\D/ ? 60 : $timeout), "Too many NZB download requests; try again in $timeout second" . ($timeout == 1 ? "" : "s"));
    0          
391             } else {
392 0           $self->_set_error(-1, "Newzbin's DirectNZB API is currently unavailable");
393             }
394              
395 0           return undef;
396             }
397            
398 0 0         if ($response->header("X-DNZB-RCode") =~ /^200/) {
399             # nzb contents are in body of document
400            
401             # if response headers indicate that content is compressed with gzip, uncompress it
402 0           my $nzb_file;
403 0 0 0       if (($response->header("Content-Encoding") =~ /gzip/) && (!$request{leavegzipped})) {
404 0           my $raw = $response->content;
405 0 0 0       $nzb_file = Compress::Zlib::memGunzip($raw) or $self->_carp("get_nzb(): could not decompress NZB file; try passing 'nogzip => 1' as a parameter") and $self->_set_error(-5, "Could not decompress NZB file") and return undef;
      0        
406             } else {
407 0           $nzb_file = $response->content;
408             }
409            
410 0 0         if ($request{reportid}) {
    0          
411             # if this is a report download, newzbin also supplies a name and category in the http headers
412 0           return ($nzb_file, $response->header("X-DNZB-Name"), $response->header("X-DNZB-Category"));
413             } elsif ($request{fileid}) {
414 0           return $nzb_file;
415             }
416             } else {
417 0           $self->_set_error(-1, "Invalid response from Newzbin's DirectNZB API");
418 0           return undef;
419             }
420             }
421              
422             #=============================================================================#
423              
424             1;
425              
426             __END__