Rename videosite.pl to videosite-irssi.pl
[videosite.git] / videosite-irssi.pl
1 # autodownload flash videos
2 #
3 # (c) 2007-2008 by Ralf Ertzinger <ralf@camperquake.de>
4 # licensed under GNU GPL v2
5 #
6 # Based on youtube.pl by Christian Garbs <mitch@cgarbs.de>
7 # which in turn is
8 # based on trigger.pl by Wouter Coekaerts <wouter@coekaerts.be>
9
10
11 BEGIN {
12     # Get rid of a (possibly old) version of BettIrssi
13     # This is a hack to prevent having to reload irssi just
14     # because BettIrssi.pm changed
15
16     delete($INC{'BettIrssi.pm'});
17 }
18
19 use strict;
20 use Irssi 20020324 qw (command_bind command_runsub signal_add_first signal_add_last);
21 use vars qw($VERSION %IRSSI);
22 use XML::Simple;
23 use Data::Dumper;
24 use File::Spec;
25 use File::Temp qw(tempfile);
26 use BettIrssi 101 qw(_bcb _bcs);
27 use LWP::UserAgent;
28 use JSON -support_by_pp;
29
30 my @grabbers;
31 my @getters;
32 my $getter;
33 my $conf;
34 my $xmlconffile = File::Spec->catfile(Irssi::get_irssi_dir(), 'videosite.xml');
35 my $conffile = File::Spec->catfile(Irssi::get_irssi_dir(), 'videosite.json');
36 my $scriptdir = File::Spec->catfile(Irssi::get_irssi_dir(), 'scripts');
37 my $plugindir = File::Spec->catfile($scriptdir, 'videosite');
38 my @outputstack = (undef);
39
40 my $PARAMS = {
41     'getter' => '',
42     'mode' => 'download',
43     'connectorlist' => ['direct'],
44     'connectors' => {},
45 };
46
47
48 # activate debug here
49 my $debug = 0;
50
51 # "message public", SERVER_REC, char *msg, char *nick, char *address, char *target
52 signal_add_last(_bcs("message public" => sub {check_for_link(@_)}));
53 # "message own_public", SERVER_REC, char *msg, char *target
54 signal_add_last(_bcs("message own_public" => sub {check_for_link(@_)}));
55
56 # "message private", SERVER_REC, char *msg, char *nick, char *address
57 signal_add_last(_bcs("message private" => sub {check_for_link(@_)}));
58 # "message own_private", SERVER_REC, char *msg, char *target, char *orig_target
59 signal_add_last(_bcs("message own_private" => sub {check_for_link(@_)}));
60
61 # "message irc action", SERVER_REC, char *msg, char *nick, char *address, char *target
62 signal_add_last(_bcs("message irc action" => sub {check_for_link(@_)}));
63 # "message irc own_action", SERVER_REC, char *msg, char *target
64 signal_add_last(_bcs("message irc own_action" => sub {check_for_link(@_)}));
65
66 # For tab completion
67 # This does not use BettIrssi (yet)
68 signal_add_first('complete word', \&sig_complete);
69
70 sub push_output {
71     unshift(@outputstack, shift);
72 }
73
74 sub pop_output {
75     shift(@outputstack);
76
77     @outputstack = (undef) unless (@outputstack);
78 }
79
80 my $videosite_commands = {
81     'save' => sub {
82         cmd_save();
83     },
84
85     'set' => sub {
86         cmd_set(@_);
87     },
88     
89     'show' => sub {
90         cmd_show(@_);
91     },
92
93     'help' => sub {
94         cmd_help(@_);
95     },
96
97     'getter' => sub {
98         cmd_getter(@_);
99     },
100
101     'enable' => sub {
102         cmd_enable(@_);
103     },
104
105     'disable' => sub {
106         cmd_disable(@_);
107     },
108
109     'reload' => sub {
110         init_videosite(0);
111     },
112
113     'mode' => sub {
114         cmd_mode(@_);
115     },
116
117     'connector' => sub {
118         cmd_connector(@_);
119     },
120
121     'debug' => sub {
122         $debug = 1;
123         foreach (@grabbers, @getters) {
124             $_->setdebug(1);
125         }
126         write_irssi('Enabled debugging');
127     },
128
129     'nodebug' => sub {
130         $debug = 0;
131         foreach (@grabbers, @getters) {
132             $_->setdebug(0);
133         }
134         write_irssi('Disabled debugging');
135     },
136 };
137
138 sub write_irssi {
139     my @text = @_;
140     my $output = $outputstack[0];
141
142     my $format = "%%mvideosite: %%n" . shift(@text);
143
144     # escape % in parameters from irssi
145     s/%/%%/g foreach @text;
146
147     if (defined $output) {
148         $output->(sprintf($format, @text), MSGLEVEL_CLIENTCRAP);
149     } else {
150         Irssi::print(sprintf($format, @text));
151     }
152
153 }
154
155 sub write_debug {
156     if ($debug) {
157         write_irssi(@_);
158     }
159 }
160
161 sub expand_url_shortener {
162     my $s = shift;
163     my $os = '';
164     my @urlshortener = (
165         'is\.gd/[[:alnum:]]+',
166         'otf\.me/[[:alnum:]]+',
167         'hel\.me/[[:alnum:]]+',
168         '7ax\.de/[[:alnum:]]+',
169         'ow\.ly/[[:alnum:]]+',
170         'j\.mp/[[:alnum:]]+',
171         'bit\.ly/[[:alnum:]]+',
172         'tinyurl\.com/[[:alnum:]]+',
173         'pop\.is/[[:alnum:]]+',
174         'post\.ly/[[:alnum:]]+',
175         '1\.ly/[[:alnum:]]+',
176         '2\.ly/[[:alnum:]]+',
177         't\.co/[[:alnum:]]+',
178         'shar\.es/[[:alnum:]]+',
179         'goo\.gl/[[:alnum:]]+',
180         );
181     my $ua = LWP::UserAgent->new(agent => 'Mozilla', max_redirect => 0, timeout => 5);
182     my $i = 10;
183
184     OUTER: while (($os ne $s) and ($i > 0)) {
185         study($s);
186         $os = $s;
187         $i--;
188
189         foreach my $pattern (@urlshortener) {
190             my $p = "https?:\/\/" . $pattern;
191
192             write_debug("Matching %s against %s", $p, $s);
193             if ($s =~ m|($p)|) {
194                 my $matched = $1;
195                 my $res;
196
197                 write_debug("Found %s", $matched);
198                 $res = $ua->head($matched);
199                 if ($res->is_redirect()) {
200                     my $new = $res->headers()->header("Location");
201
202                     write_debug("Replacing %s with %s", $matched, $new);
203                     $s =~ s/$matched/$new/;
204                     next OUTER;
205                 } else {
206                     write_debug("Error resolving %s", $matched);
207                 }
208             }
209         }
210     }
211
212     if ($i == 0) {
213         write_debug("Loop terminated by counter");
214     }
215
216     write_debug("Final string: %s", $s);
217
218     return $s;
219 }
220
221 sub connectorlist {
222     my @c;
223
224     foreach (@{$conf->{'videosite'}->{'connectorlist'}}) {
225         push(@c, $conf->{'videosite'}->{'connectors'}->{$_});
226     }
227
228     return @c;
229 }
230
231
232 sub check_for_link {
233     my $event = shift;
234     my $message = $event->message();
235     my $witem = $event->channel();
236     my $g;
237     my $m;
238     my $p;
239
240
241     # Look if we should ignore this line
242     if ($message =~ m,(?:\s|^)/nosave(?:\s|$),) {
243         return;
244     }
245
246     push_output($event->ewpf);
247     $message = expand_url_shortener($message);
248
249     study($message);
250
251     # Offer the message to all Grabbers in turn
252     GRABBER: foreach $g (@grabbers) {
253         ($m, $p) = $g->get($message);
254         while (defined($m)) {
255             write_debug('Metadata: %s', Dumper($m));
256             if ('download' eq ($conf->{'videosite'}->{'mode'})) {
257                 write_irssi('%%R>>> %%NSaving %%Y%s%%N %%G%s', $m->{'SOURCE'}, $m->{'TITLE'});
258                 unless($getter->get($m)) {
259                     write_irssi('%%R>>> FAILED');
260                 }
261             } elsif ('display' eq ($conf->{'videosite'}->{'mode'})) {
262                 write_irssi('%%M>>> %%NSaw %%Y%s%%N %%G%s', $m->{'SOURCE'}, $m->{'TITLE'});
263             } else {
264                 write_irssi('%%R>>> Invalid operation mode');
265             }
266
267             # Remove the matched part from the message and try again (there may be
268             # more!)
269             $message =~ s/$p//;
270             study($message);
271             last GRABBER if ($message =~ /^\s*$/);
272
273             ($m, $p) = $g->get($message);
274         }
275     }
276
277     pop_output();
278 }
279
280 sub cmd_save {
281
282     eval {
283         my ($tempfile, $tempfn) = tempfile("videosite.json.XXXXXX", dir => Irssi::get_irssi_dir());
284         print $tempfile JSON->new->pretty->utf8->encode($conf);
285         close($tempfile);
286         rename($tempfn, $conffile);
287     };
288     if ($@) {
289         write_irssi('Could not save config to %s: %s', ($conffile, $@));
290     } else {
291         write_irssi('configuration saved to %s', $conffile);
292     }
293 }
294
295 sub cmd_set {
296     my $target = shift;
297     my $key = shift;
298     my $val = shift;
299     my $p;
300
301     foreach $p (@getters, @grabbers) {
302         if ($p->{'NAME'} eq $target) {
303             $p->setval($key, $val);
304             return;
305         }
306     }
307     write_irssi('No such module');
308 }
309
310
311 sub cmd_enable {
312     my $target = shift;
313     my $p;
314
315     foreach $p (@grabbers) {
316         if ($p->{'NAME'} eq $target) {
317             $p->enable();
318             return;
319         }
320     }
321     write_irssi('No such module');
322 }
323
324
325 sub cmd_disable {
326     my $target = shift;
327     my $p;
328
329     foreach $p (@grabbers) {
330         if ($p->{'NAME'} eq $target) {
331             $p->disable();
332             return;
333         }
334     }
335     write_irssi('No such module');
336 }
337
338
339 sub cmd_show {
340     my $target = shift;
341     my $p;
342     my $e;
343
344     if (defined($target)) {
345         foreach $p (@getters, @grabbers) {
346             if ($p->{'NAME'} eq $target) {
347                 write_irssi($p->getconfstr());
348                 return;
349             }
350         }
351         write_irssi('No such module');
352     } else {
353         write_irssi('Loaded grabbers (* denotes enabled modules):');
354         foreach $p (@grabbers) {
355             $e = $p->_getval('enabled');
356             write_irssi(' %s%s', $p->{'NAME'}, $e?'*':'');
357         };
358
359         write_irssi('Loaded getters:');
360         foreach $p (@getters) {
361             write_irssi(' %s', $p->{'NAME'});
362         };
363     }
364 }
365
366 sub cmd_help {
367     my $target = shift;
368     my $p;
369
370     if (defined($target)) {
371         foreach $p (@getters, @grabbers) {
372             if ($p->{'NAME'} eq $target) {
373                 write_irssi($p->gethelpstr());
374                 return;
375             }
376         }
377         write_irssi('No such module');
378     } else {
379         write_irssi(<<'EOT');
380 Supported commands:
381  save: save the current configuration
382  help [modulename]: display this help, or module specific help
383  show [modulename]: show loaded modules, or the current parameters of a module
384  set modulename parameter value: set a module parameter to a new value
385  getter [modulename]: display or set the getter to use
386  enable [modulename]: enable the usage of this module (grabbers only)
387  disable [modulename]: disable the usage of this module (grabbers only)
388  reload: reload all modules (this is somewhat experimental)
389  mode [modename]: display or set the operation mode (download/display)
390  connector [subcommand]: manage connectors (proxies)
391  debug: enable debugging messages
392  nodebug: disable debugging messages
393 EOT
394     }
395 }
396
397 sub cmd_getter {
398     my $target = shift;
399     my $p;
400
401     if (defined($target)) {
402         foreach $p (@getters) {
403             if ($p->{'NAME'} eq $target) {
404                 $getter = $p;
405                 $conf->{'videosite'}->{'getter'} = $target;
406                 write_irssi("Getter changed to %s", $target);
407                 return;
408             }
409         }
410         write_irssi('No such getter');
411     } else {
412         write_irssi('Current getter: %s', $conf->{'videosite'}->{'getter'});
413     }
414 }
415
416 sub cmd_mode {
417     my $mode = shift;
418
419     if (defined($mode)) {
420         $mode = lc($mode);
421         if (('download' eq $mode) or ('display' eq $mode)) {
422             $conf->{'videosite'}->{'mode'} = $mode;
423             write_irssi('Now using %s mode', $mode);
424         } else {
425             write_irssi('Invalid mode: %s', $mode);
426         }
427     } else {
428         write_irssi('Current mode: %s', $conf->{'videosite'}->{'mode'});
429     }
430 }
431
432 sub cmd_connector {
433     my $subcmd = shift;
434     my $connconf = $conf->{'videosite'}->{'connectors'};
435
436     unless(defined($subcmd)) {
437         $subcmd = "help";
438     }
439
440     $subcmd = lc($subcmd);
441
442     if ($subcmd eq 'list') {
443         write_irssi("Defined connectors");
444         foreach (keys(%{$connconf})) {
445             write_irssi($_);
446             my $schemas = $connconf->{$_}->{'schemas'};
447             if (scalar(keys(%{$schemas})) == 0) {
448                 write_irssi(" No schemas defined");
449             } else {
450                 foreach (keys(%{$schemas})) {
451                     write_irssi(' %s: %s', $_, $schemas->{$_});
452                 }
453             }
454         }
455
456         write_irssi();
457         write_irssi("Selected connectors: %s", join(", ", @{$conf->{'videosite'}->{'connectorlist'}}));
458     } elsif ($subcmd eq 'add') {
459         my ($name) = @_;
460
461         unless(defined($name)) {
462             write_irssi("No name given");
463             return;
464         }
465
466         $name = lc($name);
467
468         if (exists($connconf->{$_})) {
469             write_irssi("Connector already exists");
470             return;
471         }
472
473         $connconf->{$name} = {'name' => $name, 'schemas' => {}};
474     } elsif ($subcmd eq 'del') {
475         my ($name) = @_;
476
477         unless(defined($name)) {
478             write_irssi("No name given");
479             return;
480         }
481
482         $name = lc($name);
483
484         unless (exists($connconf->{$name})) {
485             write_irssi("Connector does not exist");
486             return;
487         }
488
489         if (exists($connconf->{$name}->{'_immutable'})) {
490             write_irssi("Connector cannot be removed");
491             return;
492         }
493
494         delete($connconf->{$name});
495
496         # Remove from list of active connectors
497         $conf->{'videosite'}->{'connectorlist'} =
498             [ grep { $_ ne $name } @{$conf->{'videosite'}->{'connectorlist'}} ];
499
500         if (scalar(@{$conf->{'videosite'}->{'connectorlist'}}) == 0) {
501             write_irssi("List of selected connectors is empty, resetting to direct");
502             $conf->{'videosite'}->{'connectorlist'} = [ 'direct' ];
503         }
504     } elsif ($subcmd eq 'addschema') {
505         my ($conn, $schema, $proxy) = @_;
506
507         unless(defined($conn)) {
508             write_irssi("No connector name given");
509             return;
510         }
511
512         $conn = lc($conn);
513
514         if (exists($connconf->{$conn}->{'_immutable'})) {
515             write_irssi("Connector cannot be modified");
516             return;
517         }
518
519         unless(defined($schema)) {
520             write_irssi("No schema given");
521             return;
522         }
523
524         $schema = lc($schema);
525
526         unless(defined($proxy)) {
527             write_irssi("No proxy given");
528             return;
529         }
530
531         unless(exists($connconf->{$conn})) {
532             write_irssi("Connector does not exist");
533             return;
534         }
535
536         $connconf->{$conn}->{'schemas'}->{$schema} = $proxy;
537     } elsif ($subcmd eq 'delschema') {
538         my ($conn, $schema) = @_;
539
540         unless(defined($conn)) {
541             write_irssi("No connector name given");
542             return;
543         }
544
545         $conn = lc($conn);
546
547         if (exists($connconf->{$conn}->{'_immutable'})) {
548             write_irssi("Connector cannot be modified");
549             return;
550         }
551
552         unless(defined($schema)) {
553             write_irssi("No schema given");
554             return;
555         }
556
557         $schema = lc($schema);
558
559         unless(exists($connconf->{$conn})) {
560             write_irssi("Connector does not exist");
561             return;
562         }
563
564         delete($connconf->{$conn}->{'schemas'}->{$schema});
565     } elsif ($subcmd eq 'select') {
566         my @connlist = map { lc } @_;
567
568         if (scalar(@connlist) == 0) {
569             write_irssi("No connectors given");
570             return;
571         }
572
573         foreach (@connlist) {
574             unless(exists($connconf->{$_})) {
575                 write_irssi("Connector %s does not exist", $_);
576                 return;
577             }
578         }
579
580         $conf->{'videosite'}->{'connectorlist'} = [ @connlist ];
581     } else {
582         write_irssi("connector [list|add|del|addschema|delschema|help] <options>");
583         write_irssi(" help: Show this help");
584         write_irssi(" list: List the defined connectors");
585         write_irssi(" add <name>: Add a connector with name <name>");
586         write_irssi(" del <name>: Delete the connector with name <name>");
587         write_irssi(" addschema <name> <schema> <proxy>: Add proxy to connector for the given schema");
588         write_irssi(" delschema <name> <schema>: Remove the schema from the connector");
589         write_irssi(" select <name> [<name>...]: Select the connectors to use");
590     }
591 }
592
593
594
595
596 # save on unload
597 sub sig_command_script_unload {
598     my $script = shift;
599     if ($script =~ /(.*\/)?videosite(\.pl)?$/) {
600         cmd_save();
601     }
602 }
603
604 sub ploader {
605
606     my $dir = shift;
607     my $pattern = shift;
608     my $type = shift;
609     my @list;
610     my $p;
611     my $g;
612     my @g = ();
613
614     opendir(D, $dir) || return ();
615     @list = grep {/$pattern/ && -f File::Spec->catfile($dir, $_) } readdir(D);
616     closedir(D);
617
618     foreach $p (@list) {
619         write_debug("Trying to load $p:");
620         $p =~ s/\.pm$//;
621         eval qq{ require videosite::$p; };
622         if ($@) {
623             write_irssi("Failed to load plugin: $@");
624             next;
625         }
626
627         $g = eval qq{ videosite::$p->new(); };
628         if ($@) {
629             write_irssi("Failed to instanciate: $@");
630             delete($INC{$p});
631             next;
632         }
633
634         write_debug("found $g->{'TYPE'} $g->{'NAME'}");
635         if ($type eq $g->{'TYPE'}) {
636             push(@g, $g);
637             $g->setio(\&write_irssi);
638             $g->setconn(\&connectorlist);
639         } else {
640             write_irssi('%s has wrong type (got %s, expected %s)', $p, $g->{'TYPE'}, $type);
641             delete($INC{$p});
642         }
643     }
644
645     write_debug("Loaded %d plugins", $#g+1);
646     
647     return @g;
648 }
649
650 sub _load_modules($) {
651
652     my $path = shift;
653
654     foreach (keys(%INC)) {
655         if ($INC{$_} =~ m|^$path|) {
656             write_debug("Removing %s from \$INC", $_);
657             delete($INC{$_});
658         }
659     }
660     @grabbers = ploader($path, '.*Grabber\.pm$', 'grabber');
661     @getters = ploader($path, '.*Getter\.pm$', 'getter');
662 }
663
664
665 sub init_videosite {
666
667     my $bindings = shift;
668     my $p;
669
670     if (-r $conffile) {
671         write_debug("Attempting JSON config load from %s", $conffile);
672         eval {
673             local $/;
674             open(CONF, '<', $conffile);
675             $conf = JSON->new->utf8->decode(<CONF>);
676             close(CONF);
677         };
678     } elsif (-r $xmlconffile) {
679         write_debug("Attempting XML config load from %s", $xmlconffile);
680         $conf = XML::Simple::XMLin($xmlconffile, ForceArray => ['config', 'option', 'connectorlist'], KeepRoot => 1, KeyAttr => {'connector' => '+name', 'config' => 'module', 'option' => 'key'});
681     }
682
683     unless(defined($conf)) {
684         # No config, start with an empty one
685         write_debug('No config found, using defaults');
686         $conf = { 'videosite' => { }};
687     }
688
689     foreach (keys(%{$PARAMS})) {
690         unless (exists($conf->{'videosite'}->{$_})) {
691             $conf->{'videosite'}->{$_} = $PARAMS->{$_};
692         }
693     }
694
695     # Make sure there is a connector called 'direct', which defines no
696     # proxies
697     unless (exists($conf->{'videosite'}->{'connectors'}->{'direct'})) {
698         $conf->{'videosite'}->{'connectors'}->{'direct'} = {
699                 'name' => 'direct',
700                 '_immutable' => '1',
701                 'schemas' => {},
702         };
703     }
704
705     _load_modules($plugindir);
706
707     unless (defined(@grabbers) && defined(@getters)) {
708         write_irssi('No grabbers or no getters found, can not proceed.');
709         return;
710     }
711
712     $getter = $getters[0];
713     foreach $p (@getters) {
714         if ($conf->{'videosite'}->{'getter'} eq $p->{'NAME'}) {
715             $getter = $p;
716         }
717     }
718     write_debug('Selected %s as getter', $getter->{'NAME'});
719     $conf->{'videosite'}->{'getter'} = $getter->{'NAME'};
720
721     # Loop through all plugins and load the config
722     foreach $p (@grabbers, @getters) {
723         $conf->{'videosite'}->{'config'}->{$p->{'NAME'}} = $p->mergeconfig($conf->{'videosite'}->{'config'}->{$p->{'NAME'}});
724     }
725
726     if ($bindings) {
727
728         Irssi::signal_add_first('command script load', 'sig_command_script_unload');
729         Irssi::signal_add_first('command script unload', 'sig_command_script_unload');
730         Irssi::signal_add('setup saved', 'cmd_save');
731
732
733         Irssi::command_bind(_bcb('videosite' => \&cmdhandler));
734     }
735
736     write_irssi('initialized successfully');
737 }
738
739 sub sig_complete {
740     my ($complist, $window, $word, $linestart, $want_space) = @_;
741     my @matches;
742
743     if ($linestart !~ m|^/videosite\b|) {
744         return;
745     }
746
747     if ('/videosite' eq $linestart) {
748         # No command enterd so far. Produce a list of possible follow-ups
749         @matches = grep {/^$word/} keys (%{$videosite_commands});
750     } elsif ('/videosite set' eq $linestart) {
751         # 'set' command entered. Produce a list of modules
752         foreach (@grabbers, @getters) {
753             push(@matches, $_->{'NAME'}) if $_->{'NAME'} =~ m|^$word|;
754         };
755     } elsif ($linestart =~ m|^/videosite set (\w+)$|) {
756         my $module = $1;
757
758         foreach my $p (@getters, @grabbers) {
759             if ($p->{'NAME'} eq $module) {
760                 @matches = $p->getparamlist($word);
761                 last;
762             }
763         }
764     } elsif ($linestart =~ m|/videosite set (\w+) (\w+)$|) {
765         my $module = $1;
766         my $param = $2;
767
768         foreach my $p (@getters, @grabbers) {
769             if ($p->{'NAME'} eq $module) {
770                 @matches = $p->getparamvalues($param, $word);
771                 last;
772             }
773         }
774     }
775
776
777     push(@{$complist}, sort @matches);
778     ${$want_space} = 0;
779
780     Irssi::signal_stop();
781 }
782
783 sub cmdhandler {
784     my $event = shift;
785     my ($cmd, @params) = split(/\s+/, $event->message());
786
787     push_output($event->ewpf);
788
789     if (exists($videosite_commands->{$cmd})) {
790         $videosite_commands->{$cmd}->(@params);
791     }
792
793     pop_output();
794 }
795
796 unshift(@INC, $scriptdir);
797 init_videosite(1);