- Add authentication support
[xmlrtorrent.git] / xmlrtorrent.pl
1 # control an rTorrent client via XMLRPC,
2 # and collect rtorrent files from IRC for later download
3 #
4 # (c) 2007-2008 by Ralf Ertzinger <ralf@camperquake.de>
5 # licensed under GNU GPL v2
6
7 use strict;
8 use Irssi 20020324 qw (command_bind command_runsub signal_add_first signal_add_last);
9 use vars qw($VERSION %IRSSI);
10 use XML::Simple;
11 use Data::Dumper;
12 use File::Spec;
13 use xmlrtorrent;
14
15 my $conf;
16 my $conffile = File::Spec->catfile(Irssi::get_irssi_dir(), 'xmlrtorrent.xml');
17 my $scriptdir = File::Spec->catfile(Irssi::get_irssi_dir(), 'scripts');
18 my %torrentlist = ();
19 my $torrentindex = 1;
20 my $rtorrent;
21
22 my @outputstack = (undef);
23
24 my $PARAMS = {
25     'XMLURL' => 'http://localhost/RPC2',
26     'USERNAME' => '',
27     'PASSWORD' => '',
28 };
29
30 # activate debug here
31 my $debug = 0;
32
33 # "message public", SERVER_REC, char *msg, char *nick, char *address, char *target
34 signal_add_last("message public" => sub {check_for_link(\@_,1,4,2,0);});
35 # "message own_public", SERVER_REC, char *msg, char *target
36 signal_add_last("message own_public" => sub {check_for_link(\@_,1,2,-1,0);});
37
38 # "message private", SERVER_REC, char *msg, char *nick, char *address
39 signal_add_last("message private" => sub {check_for_link(\@_,1,-1,2,0);});
40 # "message own_private", SERVER_REC, char *msg, char *target, char *orig_target
41 signal_add_last("message own_private" => sub {check_for_link(\@_,1,2,-1,0);});
42
43 # "message irc action", SERVER_REC, char *msg, char *nick, char *address, char *target
44 signal_add_last("message irc action" => sub {check_for_link(\@_,1,4,2,0);});
45 # "message irc own_action", SERVER_REC, char *msg, char *target
46 signal_add_last("message irc own_action" => sub {check_for_link(\@_,1,2,-1,0);});
47
48 # For tab completion
49 signal_add_first('complete word', \&sig_complete);
50
51 my $xmlrtorrent_commands = {
52     'save' => sub {
53         cmd_save();
54     },
55
56     'set' => sub {
57         cmd_set(@_);
58     },
59     
60     'show' => sub {
61         cmd_show(@_);
62     },
63
64     'help' => sub {
65         cmd_help(@_);
66     },
67
68     'queue' => sub {
69         cmd_queue(@_);
70     },
71
72     'remote' => sub {
73         cmd_remote(@_);
74     },
75
76     'debug' => sub {
77         $debug = 1;
78         write_irssi('Enabled debugging');
79     },
80
81     'nodebug' => sub {
82         $debug = 0;
83         write_irssi('Disabled debugging');
84     },
85 };
86
87 sub write_irssi {
88     my @text = @_;
89     my $output = $outputstack[0];
90
91     $text[0] = '%%mxmlrtorrent: %%n' . $text[0];
92
93     if (defined($output) and ref($output)) {
94         $output->print(sprintf(shift(@text), @text), MSGLEVEL_CLIENTCRAP);
95     } else {
96         Irssi::print(sprintf(shift(@text), @text));
97     }
98
99 }
100
101 sub push_output {
102     unshift(@outputstack, shift);
103 }
104
105 sub pop_output {
106     shift(@outputstack);
107 }
108
109 sub write_debug {
110     if ($debug) {
111         write_irssi(@_);
112     }
113 }
114
115 # This is shamelessly stolen from pythons urlgrabber
116 sub format_number {
117     my $number = shift;
118     my $SI = shift || 0;
119     my @symbols = ('', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y');
120     my $step = $SI?1000:1024;
121     my $thresh = 999;
122     my $depth = 0;
123     my $max_depth = $#symbols;
124     my $format;
125
126     while (($number > $thresh) and ($depth < $max_depth)) {
127         $depth += 1;
128         $number /= $step;
129     }
130
131     if ($number =~ /^[+-]?\d+$/) {
132         # Integer.
133         $format = '%i%s';
134     } elsif ($number < 9.95) {
135         $format = '%.1f%s';
136     } else {
137         $format = '%.0f%s';
138     }
139     return sprintf($format, $number, $symbols[$depth]);
140 }
141
142
143
144 sub check_for_link {
145     my ($signal,$parammessage,$paramchannel,$paramnick,$paramserver) = @_;
146     my $server = $signal->[$paramserver];
147     my $target = $signal->[$paramchannel];
148     my $message = ($parammessage == -1) ? '' : $signal->[$parammessage];
149     my $nick = ($paramnick == -1)?defined($server)?$server->{'nick'}:'':$signal->[$paramnick];
150     my $g;
151     my $m;
152     my $p;
153
154     my $witem;
155     if (defined $server) {
156         $witem = $server->window_item_find($target);
157     } else {
158         $witem = Irssi::window_item_find($target);
159     }
160
161     # Look if we should ignore this line
162     if ($message =~ m,(?:\s|^)/nosave(?:\s|$),) {
163         return;
164     }
165
166     push_output($witem);
167
168     # Look if there is a torrent link in there
169     $message =~ m,(http://\S*\.(?:torrent|penis)),;
170     $m = $1;
171     while (defined($m)) {
172         write_debug('Torrent-URL: %s', $m);
173         $torrentlist{$torrentindex++} = {'CHANNEL' => $target, 'NICK' => $nick, 'URL' => $m};
174
175         # Remove the matched part from the message and try again (there may be
176         # more!)
177         $message =~ s/$m//;
178
179         $message =~ m|(http://.*\.torrent)|;
180         $m = $1;
181     }
182
183     pop_output();
184 }
185
186 # Handle the queue of unhandled torrents
187 sub cmd_queue {
188     my ($subcmd, $id, @params) = @_;
189
190     if ('remove' eq $subcmd) {
191         if (defined($id)) {
192             delete($torrentlist{$id});
193         }
194     } elsif ('clear' eq $subcmd) {
195         %torrentlist = ();
196     } elsif ('confirm' eq $subcmd) {
197         my $u;
198         return unless(defined($id) and exists($torrentlist{$id}));
199
200         $u = $torrentlist{$id}->{'URL'};
201
202         write_debug('Sending %s to rtorrent', $u);
203         unless(defined($rtorrent->load_start($u))) {
204             write_irssi('%%RError sending URL %s: %s', $u, $rtorrent->errstr());
205         } else {
206             write_irssi('%s enqueued', $u);
207             delete($torrentlist{$id});
208         }
209     } elsif ('add' eq $subcmd) {
210         unless(defined($id)) {
211             return;
212         }
213         $torrentlist{$torrentindex++} = {'CHANNEL' => '', 'NICK' => '', 'URL' => $id};
214     } elsif (('list' eq $subcmd) or !defined($subcmd))  {
215         write_irssi('List of queued torrents');
216         foreach (sort(keys(%torrentlist))) {
217             write_irssi('  %3d: %s@%s: %s', $_,
218                     $torrentlist{$_}->{'NICK'},
219                     $torrentlist{$_}->{'CHANNEL'},
220                     $torrentlist{$_}->{'URL'});
221         }
222     } else {
223         write_irssi('Unknown subcommand: %s', $subcmd);
224     }
225 }
226
227 # Handle the remote rtorrent queue
228 sub cmd_remote {
229     my ($subcmd, $id, @params) = @_;
230     my $rqueue;
231
232     if (('list' eq $subcmd) or !defined($subcmd)) {
233         unless(defined($rqueue = $rtorrent->download_list())) {
234             write_irssi('Error getting list of downloads: %s', $rtorrent->errstr());
235             return;
236         }
237
238         write_irssi('List of rempote torrents');
239         foreach (@{$rqueue}) {
240             write_irssi('%s%s: %sB/%sB done (%d%%), %sB/s up, %sB/s down',
241                     $_->[6]?'*':' ',
242                     $_->[0],
243                     format_number($_->[2]),
244                     format_number($_->[1]),
245                     ($_->[2]*100)/$_->[1],
246                     format_number($_->[3]),
247                     format_number($_->[4]));
248         }
249     }
250 }
251
252
253 sub cmd_save {
254
255     eval {
256         open(CONF, '>'.$conffile) or die 'Could not open config file';
257         print CONF XML::Simple::XMLout($conf, KeepRoot => 1, KeyAttr => {'config' => 'module', 'option' => 'key'});
258         close(CONF);
259     };
260     if ($@) {
261         write_irssi('Could not save config to %s: %s', ($conffile, $@));
262     } else {
263         write_irssi('configuration saved to %s', $conffile);
264     }
265 }
266
267 sub cmd_set {
268     my $target = shift;
269     my $key = shift;
270     my $val = shift;
271
272     if ('global' eq $target) {
273         if(exists($PARAMS->{$key})) {
274             $conf->{'xmlrtorrent'}->{$key} = $val;
275             if ('XMLURL' eq $key) {
276                 unless(defined($rtorrent = xmlrtorrent->new(
277                         'XMLURL' => $conf->{'xmlrtorrent'}->{'XMLURL'},
278                         'USERNAME' => $conf->{'xmlrtorrent'}->{'USERNAME'},
279                         'USERNAME' => $conf->{'xmlrtorrent'}->{'PASSWORD'}))) {
280                     write_irssi('Could not initialize XMLRPC instance');
281                     return;
282                 }
283             }
284         } else {
285             write_irssi('Key %s does not exist', $key);
286         }
287     }
288 }
289
290
291 sub cmd_show {
292     my $target = shift;
293     my $p;
294     my $e;
295 }
296
297 sub cmd_help {
298     my $target = shift;
299     my $p;
300
301     write_irssi(<<'EOT');
302 Supported commands:
303  save: Save the current configuration
304  help: Display this help
305  debug: enable debugging messages
306  nodebug: disable debugging messages
307 EOT
308 }
309
310
311 # save on unload
312 sub sig_command_script_unload {
313     my $script = shift;
314     if ($script =~ /(.*\/)?xmlrtorrent(\.pl)?$/) {
315         cmd_save();
316     }
317 }
318
319 sub init_xmlrtorrent {
320
321     my $bindings = shift;
322     my $p;
323
324     unless(-r $conffile && defined($conf = XML::Simple::XMLin($conffile, ForceArray => ['config', 'option'], KeepRoot => 1, KeyAttr => {'config' => 'module', 'option' => 'key'}))) {
325         # No config, start with an empty one
326         write_debug('No config found, using defaults');
327         $conf = { 'xmlrtorrent' => { }};
328     }
329     foreach (keys(%{$PARAMS})) {
330         unless (exists($conf->{'xmlrtorrent'}->{$_})) {
331             $conf->{'xmlrtorrent'}->{$_} = $PARAMS->{$_};
332         }
333     }
334
335     unless(defined($rtorrent = xmlrtorrent->new(
336             'XMLURL' => $conf->{'xmlrtorrent'}->{'XMLURL'},
337             'USERNAME' => $conf->{'xmlrtorrent'}->{'USERNAME'},
338             'USERNAME' => $conf->{'xmlrtorrent'}->{'PASSWORD'}))) {
339         write_irssi('Could not initialize XMLRPC instance');
340         return;
341     }
342
343     if ($bindings) {
344
345         Irssi::signal_add_first('command script load', 'sig_command_script_unload');
346         Irssi::signal_add_first('command script unload', 'sig_command_script_unload');
347         Irssi::signal_add('setup saved', 'cmd_save');
348
349
350         Irssi::command_bind('torrent' => \&cmdhandler);
351     }
352
353     write_irssi('xmlrtorrent initialized');
354 }
355
356 sub sig_complete {
357     my ($complist, $window, $word, $linestart, $want_space) = @_;
358     my @matches;
359
360     if ($linestart !~ m|^/torrent\b|) {
361         return;
362     }
363
364     ${$want_space} = 0;
365
366     Irssi::signal_stop();
367 }
368
369 sub cmdhandler {
370     my ($data, $server, $witem) = @_;
371     my ($cmd, @params) = split(/\s+/, $data);
372
373     push_output($witem);
374
375     if (exists($xmlrtorrent_commands->{$cmd})) {
376         $xmlrtorrent_commands->{$cmd}->(@params);
377     } else {
378         write_irssi('Unknown command: %s', $cmd);
379     }
380
381     pop_output();
382 }
383
384 unshift(@INC, $scriptdir);
385 init_xmlrtorrent(1);