File Coverage

lib/Transmission/Client.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             # ex:ts=4:sw=4:sts=4:et
2             package Transmission::Client;
3             # Copyright 2009-2013, Jan Henning Thorsen <jhthorsen@cpan.org>
4             # and contributors
5             #
6             # All rights reserved.
7             #
8             # This program is free software; you can redistribute it and/or
9             # modify it under the same terms as Perl itself.
10              
11             =head1 NAME
12              
13             Transmission::Client - Interface to Transmission
14              
15             =head1 VERSION
16              
17             0.0803
18              
19             =head1 DESCRIPTION
20              
21             L<Transmission::Client> is the main module in a collection of modules to
22             communicate with Transmission. Transmission is:
23              
24             Transmission is a cross-platform BitTorrent client that is:
25             * Easy
26             * Lean
27             * Native
28             * Powerful
29             * Free
30              
31             If you want to communicate with "transmission-daemon", this is a module
32             which can help you with that.
33              
34             The documentation is half copy/paste from the Transmission RPC spec:
35             L<https://trac.transmissionbt.com/browser/trunk/extras/rpc-spec.txt>
36              
37             This module differs from L<P2P::Transmission> in (at least) two ways:
38             This one use L<Moose> and it won't die. The latter is especially
39             annoying in the constructor.
40              
41             =head1 SYNOPSIS
42              
43             use Transmission::Client;
44              
45             my $client = Transmission::Client->new;
46             my $torrent_id = 2;
47             my $data = base64_encoded_data();
48              
49             $client->add(metainfo => $data) or confess $client->error;
50             $client->remove($torrent_id) or confess $client->error;
51              
52             for my $torrent (@{ $client->torrents }) {
53             print $torrent->name, "\n";
54             for my $file (@{ $torrent->files }) {
55             print "> ", $file->name, "\n";
56             }
57             }
58              
59             print $client->session->download_dir, "\n";
60              
61             =head1 FAULT HANDLING
62              
63             In C<0.06> L<Transmission::Client> can be constructed with "autodie" set
64             to true, to make this object confess instead of just setting L</error>.
65             Example:
66              
67             my $client = Transmission::Client->new(autodie => 1);
68              
69             eval {
70             $self->add(filename => 'foo.torrent');
71             } or do {
72             # add() failed...
73             };
74              
75             =head1 SEE ALSO
76              
77             L<Transmission::AttributeRole>
78             L<Transmission::Session>
79             L<Transmission::Torrent>
80             L<Transmission::Utils>
81              
82             =cut
83              
84 3     3   21429 use Moose;
  0            
  0            
85             use DateTime;
86             use DateTime::Duration;
87             use JSON::Any;
88             use LWP::UserAgent;
89             use MIME::Base64;
90             use Transmission::Torrent;
91             use Transmission::Session;
92             use constant RPC_DEBUG => $ENV{'TC_RPC_DEBUG'};
93              
94             our $VERSION = '0.0804';
95             our $SESSION_ID_HEADER_NAME = 'X-Transmission-Session-Id';
96             my $JSON = JSON::Any->new;
97              
98             with 'Transmission::AttributeRole';
99              
100             =head1 ATTRIBUTES
101              
102             =head2 url
103              
104             $str = $self->url;
105              
106             Returns an URL to where the Transmission rpc api is.
107             Default value is "http://localhost:9091/transmission/rpc";
108              
109             =cut
110              
111             has url => (
112             is => 'ro',
113             isa => 'Str',
114             default => 'http://localhost:9091/transmission/rpc',
115             );
116              
117             # this is subject for change!
118             has _url => (
119             is => 'ro',
120             isa => 'Str',
121             lazy_build => 1,
122             );
123              
124             sub _build__url {
125             my $self = shift;
126             my $url = $self->url;
127              
128             if($self->username or $self->password) {
129             my $auth = join ':', $self->username, $self->password;
130             $url =~ s,://,://$auth@,;
131             }
132              
133             return $url;
134             }
135              
136             =head2 error
137              
138             $str = $self->error;
139            
140             Returns the last error known to the object. All methods can return
141             empty list in addition to what specified. Check this attribute if so happens.
142              
143             Like L</autodie>? Create your object with C<autodie> set to true and this
144             module will throw exceptions in addition to setting this variable.
145              
146             =cut
147              
148             has error => (
149             is => 'rw',
150             isa => 'Str',
151             default => '',
152             clearer => '_clear_error',
153             trigger => sub { $_[0]->_autodie and confess $_[1] },
154             );
155              
156             has _autodie => (
157             is => 'ro',
158             init_arg => 'autodie',
159             isa => 'Bool',
160             default => 0,
161             );
162              
163             =head2 username
164              
165             $str = $self->username;
166              
167             Used to authenticate against Transmission.
168              
169             =cut
170              
171             has username => (
172             is => 'ro',
173             isa => 'Str',
174             default => '',
175             );
176              
177             =head2 password
178              
179             $str = $self->password;
180              
181             Used to authenticate against Transmission.
182              
183             =cut
184              
185             has password => (
186             is => 'ro',
187             isa => 'Str',
188             default => '',
189             );
190              
191             =head2 timeout
192              
193             $int = $self->timeout;
194              
195             Number of seconds to wait for RPC response.
196              
197             =cut
198              
199             has _ua => (
200             is => 'rw',
201             isa => 'LWP::UserAgent',
202             lazy => 1,
203             handles => [qw/timeout/],
204             default => sub {
205             LWP::UserAgent->new( agent => 'Transmission-Client' );
206             },
207             );
208              
209             =head2 session
210              
211             $session_obj = $self->session;
212             $stats_obj = $self->stats;
213              
214             Returns an instance of L<Transmission::Session>.
215             C<stats()> is a proxy method on L</session>.
216              
217             =cut
218              
219             has session => (
220             is => 'ro',
221             lazy => 1,
222             predicate => 'has_session',
223             handles => [qw/stats/],
224             default => sub {
225             Transmission::Session->new( client => $_[0] );
226             },
227             );
228              
229             =head2 torrents
230              
231             $array_ref = $self->torrents;
232             $self->clear_torrents;
233              
234             Returns an array-ref of L<Transmission::Torrent> objects. Default value
235             is a full list of all known torrents, with as little data as possible read
236             from Transmission. This means that each request on a attribute on an object
237             will require a new request to Transmission. See L</read_torrents> for more
238             information.
239              
240             =cut
241              
242             has torrents => (
243             is => 'rw',
244             traits => ['Array'],
245             lazy => 1,
246             clearer => "clear_torrents",
247             builder => "read_torrents",
248             predicate => 'has_torrents',
249             handles => {
250             torrent_list => 'elements',
251             },
252             );
253              
254             =head2 version
255              
256             $str = $self->version;
257              
258             Get Transmission version.
259              
260             =cut
261              
262             has version => (
263             is => 'ro',
264             isa => 'Str',
265             lazy_build => 1,
266             );
267              
268             sub _build_version {
269             my $self = shift;
270              
271             if(my $data = $self->rpc('session-get')) {
272             return $data->{'version'} || q();
273             }
274              
275             return q();
276             }
277              
278             =head2 session_id
279              
280             $self->session_id($str);
281             $str = $self->session_id;
282              
283             The session ID used to communicate with Transmission.
284              
285             =cut
286              
287             has session_id => (
288             is => 'rw',
289             isa => 'Str',
290             default => '',
291             trigger => sub {
292             $_[0]->_ua->default_header($SESSION_ID_HEADER_NAME => $_[1]);
293             },
294             );
295              
296             =head1 METHODS
297              
298             =head2 add
299              
300             $bool = $self->add(%args);
301              
302             key | value type & description
303             -----------------+-------------------------------------------------
304             download_dir | string path to download the torrent to
305             filename | string filename or URL of the .torrent file
306             metainfo | string torrent content
307             paused | boolean if true, don't start the torrent
308             peer_limit | number maximum number of peers
309              
310             Either "filename" or "metainfo" MUST be included. All other arguments are
311             optional.
312              
313             See "3.4 Adding a torrent" from
314             L<https://trac.transmissionbt.com/browser/trunk/extras/rpc-spec.txt>
315              
316             =cut
317              
318             sub add {
319             my $self = shift;
320             my %args = @_;
321              
322             if($args{'filename'} and $args{'metainfo'}) {
323             $self->error("Filename and metainfo argument crash");
324             return;
325             }
326             elsif($args{'filename'}) {
327             return $self->rpc('torrent-add', %args);
328             }
329             elsif($args{'metainfo'}) {
330             $args{'metainfo'} = encode_base64($args{'metainfo'});
331             return $self->rpc('torrent-add', %args);
332             }
333             else {
334             $self->error("Need either filename or metainfo argument");
335             return;
336             }
337             }
338              
339             =head2 remove
340              
341             $bool = $self->remove(%args);
342              
343             key | value type & description
344             -------------------+-------------------------------------------------
345             ids | array torrent list, as described in 3.1
346             delete_local_data | boolean delete local data. (default: false)
347              
348             C<ids> can also be the string "all". C<ids> is required.
349              
350             See "3.4 Removing a torrent" from
351             L<https://trac.transmissionbt.com/browser/trunk/extras/rpc-spec.txt>
352              
353             =cut
354              
355             sub remove {
356             my $self = shift;
357              
358             if($self->_do_ids_action('torrent-remove' => @_)) {
359             $self->clear_torrents; # torrent list might be out of sync
360             return 1;
361             }
362             else {
363             return 0;
364             }
365             }
366              
367             =head2 move
368              
369             $bool = $self->move(%args);
370              
371              
372             string | value type & description
373             ------------+-------------------------------------------------
374             ids | array torrent list, as described in 3.1
375             location | string the new torrent location
376             move | boolean if true, move from previous location.
377             | otherwise, search "location" for files
378              
379             C<ids> can also be the string "all". C<ids> and C<location> is required.
380              
381             See "3.5 moving a torrent" from
382             L<https://trac.transmissionbt.com/browser/trunk/extras/rpc-spec.txt>
383              
384             =cut
385              
386             sub move {
387             my $self = shift;
388             my %args = @_;
389              
390             if(!defined $args{'location'}) {
391             $self->error("location argument is required");
392             return;
393             }
394              
395             return $self->_do_ids_action('torrent-set-location' => %args);
396             }
397              
398             =head2 start
399              
400             $bool = $self->start($ids);
401              
402             Will start one or more torrents.
403             C<$ids> can be a single int, an array of ints or the string "all".
404              
405             =head2 stop
406              
407             $bool = $self->stop($ids);
408              
409             Will stop one or more torrents.
410             C<$ids> can be a single int, an array of ints or the string "all".
411              
412             =head2 verify
413              
414             $bool = $self->stop($ids);
415              
416             Will verify one or more torrents.
417             C<$ids> can be a single int, an array of ints or the string "all".
418              
419             =cut
420              
421             sub start {
422             return shift->_do_ids_action('torrent-start' => @_);
423             }
424              
425             sub stop {
426             return shift->_do_ids_action('torrent-stop' => @_);
427             }
428              
429             sub verify {
430             return shift->_do_ids_action('torrent-verify' => @_);
431             }
432              
433             sub _do_ids_action {
434             my $self = shift;
435             my $method = shift;
436             my %args = @_ == 1 ? (ids => $_[0]) : @_;
437             my $ids;
438              
439             unless(defined $args{'ids'}) {
440             $self->error('ids is required as argument');
441             return;
442             }
443              
444             unless(ref $args{'ids'} eq 'ARRAY') {
445             if($args{'ids'} eq 'all') {
446             delete $args{'ids'};
447             }
448             else {
449             $args{'ids'} = [$args{'ids'}];
450             }
451             }
452              
453             return $self->rpc($method, %args) ? 1 : 0;
454             }
455              
456             =head2 read_torrents
457              
458             @list = $self->read_torrents(%args);
459             $array_ref = $self->read_torrents(%args);
460              
461             key | value type & description
462             ------------+-------------------------------------------------
463             ids | array torrent list, as described in 3.1
464             | this is optional
465             lazy_read | will create objects with as little data as possible.
466              
467             =over 4
468              
469             =item List context
470              
471             Returns a list of L<Transmission::Torrent> objects and sets the L</torrents>
472             attribute.
473              
474             =item Scalar context
475              
476             Returns an array-ref of L<Transmission::Torrent>.
477              
478             =back
479              
480             =cut
481              
482             sub read_torrents {
483             my $self = shift;
484             my %args = @_ == 1 ? (ids => $_[0]) : @_;
485             my $list;
486              
487             # set fields...
488             if(exists $args{'fields'}) { # ... based on user input
489             # We should always request id
490             push @{$args{'fields'}}, 'id' unless
491             grep {'id' eq $_} @{$args{'fields'}};
492             }
493             elsif($args{'lazy_read'}) { # ... as few fields as possible
494             $args{'fields'} = ['id'];
495             }
496             else { # ... all fields
497             $args{'fields'} = [
498             keys %Transmission::Torrent::READ,
499             keys %Transmission::Torrent::BOTH,
500             ];
501             }
502              
503             # set ids
504             if($args{'ids'}) {
505             if($args{'ids'} eq 'all') {
506             delete $args{'ids'};
507             }
508             elsif(ref $args{'ids'} eq "") {
509             $args{'ids'} = [ $args{'ids'} ];
510             }
511             }
512              
513             if(my $data = $self->rpc('torrent-get' => %args)) {
514             $list = $data->{'torrents'};
515             }
516             else {
517             $list = [];
518             }
519              
520             for my $torrent (@$list) {
521             $torrent = Transmission::Torrent->new(
522             client => $self,
523             id => $torrent->{'id'},
524             %$torrent,
525             );
526             }
527              
528             if(wantarray) {
529             $self->torrents($list);
530             return @$list;
531             }
532             else {
533             return $list;
534             }
535             }
536              
537             =head2 rpc
538              
539             $any = $self->rpc($method, %args);
540              
541             Communicate with backend. This methods is meant for internal use.
542              
543             =cut
544              
545             sub rpc {
546             my $self = shift;
547             my $method = shift or return;
548             my %args = @_;
549             my $nested = delete $args{'_nested'}; # internal flag
550             my($tag, $res, $post);
551              
552             $method = $self->_normal2Camel($method);
553              
554             # The keys need to be dashes as well
555             # _normal2Camel modifies a hashref in places
556             $self->_normal2Camel( \%args );
557              
558             # make sure ids are numeric
559             if(ref $args{'ids'} eq 'ARRAY') {
560             for my $id (@{ $args{'ids'} }) {
561             # Need to convert string integer to "real" integer
562             # FLAGS = (IOK,POK,pIOK,pPOK)
563             # IV = 42
564             # ...to...
565             # FLAGS = (PADTMP,IOK,pIOK)
566             # IV = 42
567             $id += 0 if($id =~ /^\d+$/);
568             }
569             }
570              
571             $tag = int rand 2*16 - 1;
572             $post = $JSON->encode({
573             method => $method,
574             tag => $tag,
575             arguments => \%args,
576             });
577              
578             $res = $self->_ua->post($self->_url, Content => $post);
579              
580             if(RPC_DEBUG) {
581             print "post: $post\n";
582             print "status_line: ", $res->status_line, "\n";
583             }
584              
585             unless($res->is_success) {
586             if($res->code == 409 and !$nested) {
587             $self->session_id($res->header($SESSION_ID_HEADER_NAME));
588             return $self->rpc($method => %args, _nested => 1);
589             }
590             else {
591             $self->error($res->status_line);
592             return;
593             }
594             }
595              
596             $res = $JSON->decode($res->content);
597              
598             unless($res->{'tag'} == $tag) {
599             $self->error("Tag mismatch");
600             return;
601             }
602             unless($res->{'result'} eq 'success') {
603             $self->error($res->{'result'});
604             return;
605             }
606              
607             $self->_clear_error;
608              
609             return $res->{'arguments'};
610             }
611              
612             =head2 read_all
613              
614             1 == $self->read_all;
615              
616             This method will try to populate ALL torrent, session and stats information,
617             using three requests.
618              
619             =cut
620              
621             sub read_all {
622             my $self = shift;
623              
624             $self->session->read_all;
625             $self->stats->read_all;
626             () = $self->read_torrents;
627              
628             return 1;
629             }
630              
631             =head1 LICENSE
632              
633             This library is free software; you can redistribute it and/or modify it under
634             the same terms as Perl itself.
635              
636             =head1 COPYRIGHT AND AUTHORS
637              
638             Copyright 2009-2013, Jan Henning Thorsen <jhthorsen@cpan.org> and contributors
639              
640             Current maintainer: Olof Johansson - C<olof@cpan.org>
641              
642             =head2 CONTRIBUTORS
643              
644             =over
645              
646             =item Andrew Fresh
647              
648             =back
649              
650             =cut
651              
652             no MIME::Base64;
653             no Moose;
654             1;