Youtube: add new URL variant
[videosite.git] / libvideosite.pm
1 # library to 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 package libvideosite;
11 require Exporter;
12
13 use vars qw(@ISA @EXPORT_OK);
14 use File::Spec;
15 use Module::Load;
16 use LWP::UserAgent;
17 use Data::Dumper;
18 use File::Basename;
19 use Cwd qw(realpath);
20 use JSON -support_by_pp;
21 use File::Temp qw(tempfile);
22 use strict;
23
24 @ISA = qw(Exporter);
25 @EXPORT_OK = qw(init register_api check_for_link);
26
27 my @outputstack;
28 my $outputprefix;
29 my $debug = 0;
30 my %debugwindows = ();
31 my @grabbers;
32 my @getters;
33 my $getter;
34 my %builtin_config = ();
35 my $builtin_config_path;
36 my $builtin_config_default;
37 my $config_cache = 1;
38 my %config_cache = ();
39 our $error;
40
41 #
42 # The default config. These values will be set in the config
43 # if they do not exist already.
44 #
45 my $defaultconfig = {
46     'getter' => 'filegetter',
47     'mode' => 'download',
48     'active-connectors' => 'direct',
49     'defined-connectors' => 'direct',
50     'connectors' => {
51         'direct' => {
52             'name' => 'direct',
53             '_immutable' => '1',
54             'schemas' => {},
55         },
56         'environment' => {
57             'name' => 'environment',
58             '_immutable' => '1',
59             'schemas' => {},
60         },
61     },
62     'config-version' => '2',
63 };
64
65 #
66 # This is a list of default values for the remote API. These
67 # are used if the values are not registered by the library user.
68 #
69 my $remote_api = {
70     io => sub { print @_, "\n" },
71     config_init => \&_builtin_config_init,
72     config_get => \&_builtin_config_get,
73     config_set => \&_builtin_config_set,
74     config_has => \&_builtin_config_has,
75     config_save => \&_builtin_config_save,
76     config_del => \&_builtin_config_del,
77     color => sub { return '' },
78     module_path => sub { return dirname(realpath($0)) },
79     quote => sub { return $_ },
80     reload => sub {},
81 };
82
83 #
84 # List of known commands and handlers
85 #
86 my $videosite_commands = {
87     'save' => sub {
88         _cmd_save();
89     },
90
91     'set' => sub {
92         _cmd_set(@_);
93     },
94
95     'show' => sub {
96         _cmd_show(@_);
97     },
98
99     'help' => sub {
100         _cmd_help(@_);
101     },
102
103     'getter' => sub {
104         _cmd_getter(@_);
105     },
106
107     'enable' => sub {
108         _cmd_enable(@_);
109     },
110
111     'disable' => sub {
112         _cmd_disable(@_);
113     },
114
115     'reload' => sub {
116         $remote_api->{reload}->();
117     },
118
119     'mode' => sub {
120         _cmd_mode(@_);
121     },
122
123     'connector' => sub {
124         _cmd_connector(@_);
125     },
126
127     'debug' => sub {
128         _cmd_debug(@_);
129     },
130
131     'nodebug' => sub {
132         _cmd_nodebug(@_);
133     },
134
135     'service' => sub {
136         _cmd_service(@_);
137     },
138 };
139
140 #
141 # Output a string on the client.
142 # Works like (s)printf in that it takes a format string and a list of
143 # values to be replaced. Undefined values will be printed as '(undef)'
144 #
145 # All parameters (except for the format string itself) will be quoted
146 # using the client specific quote function
147 #
148 sub _io {
149     my @text = @_;
150     my $format;
151
152     @text = ('') unless(@text);
153
154     # This will define the outputprefix once, so we don't have
155     # do do this every time.
156     $outputprefix = sprintf("%svideosite: %s",
157         _colorpair('magenta'),
158         _colorpair()) unless(defined($outputprefix));
159     $format = $outputprefix . shift(@text);
160
161     #
162     # The format string is assumed to be appropriately quoted.
163     # Quote the rest of the text, replacing undefined strings by (undef)
164     #
165     @text = map { defined($_)?$remote_api->{quote}->($_):'(undef)' } @text;
166
167     $outputstack[0]->{io}->(sprintf($format, @text));
168 }
169
170 #
171 # Recursively walk through a hash-of-hashes, calling the given function
172 # for each found leaf with the path to the leaf
173 #
174 sub _recursive_hash_walk {
175     my $hash = shift;
176     my $callback = shift;
177     my @path = @_;
178
179     foreach (keys(%{$hash})) {
180         if (ref($hash->{$_}) eq 'HASH') {
181             _recursive_hash_walk($hash->{$_}, $callback, @path, $_);
182         } else {
183             $callback->([@path, $_], $hash->{$_});
184         }
185     }
186 }
187
188 #
189 # Return the color code for the given foreground/background color
190 # pair. Both can be undef, which means "default"
191 #
192 sub _colorpair {
193     my ($fg, $bg) = @_;
194
195     $fg = defined($fg)?$fg:'default';
196     $bg = defined($bg)?$bg:'default';
197
198     return $remote_api->{color}->($fg, $bg);
199 }
200
201 #
202 # Sets the given config item if it is not set already
203 #
204 sub _init_config_item {
205     my $path = shift;
206     my $value = shift;
207
208     unless(_config_has($path)) {
209         _config_set($path, $value);
210     }
211 }
212
213 #
214 # Print a message if debug is enabled
215 #
216 sub _debug {
217     my @data = @_;
218
219     $data[0] = "DEBUG: " . $data[0];
220
221     # Check for global debug
222     if ($debug) {
223         _io(@data);
224     } else {
225         # Check if current window is in the per-window-debug list
226         if (exists($debugwindows{$outputstack[0]->{window}})) {
227             _io(@data);
228         }
229     }
230 }
231
232 #
233 # Load a list of modules matching a pattern from a given directory.
234 #
235 sub _ploader {
236
237     my $dir = shift;
238     my $pattern = shift;
239     my $type = shift;
240     my @list;
241     my $p;
242     my $g;
243     my @g = ();
244
245     opendir(D, $dir) || return ();
246     @list = grep {/$pattern/ && -f File::Spec->catfile($dir, $_) } readdir(D);
247     closedir(D);
248
249     foreach $p (@list) {
250         _debug("Trying to load $p:");
251         $p =~ s/\.pm$//;
252         eval {
253             load "videosite::$p";
254         };
255         if ($@) {
256             _io("Failed to load plugin: $@");
257             next;
258         }
259
260         eval {
261             $g = "videosite::$p"->new();
262         };
263         if ($@) {
264             _io("Failed to instanciate: $@");
265             delete($INC{$p});
266             next;
267         }
268
269         _debug("found $g->{'TYPE'} $g->{'NAME'}");
270         if ($type eq $g->{'TYPE'}) {
271             push(@g, $g);
272             $g->register_api({
273                 io => \&_io,
274                 io_debug => \&_debug,
275                 connectors => sub { return _connectorlist('active-connectors') },
276                 config_get => \&_config_get,
277                 config_set => \&_config_set,
278                 config_has => \&_config_has,
279             });
280         } else {
281             _io('%s has wrong type (got %s, expected %s)', $p, $g->{'TYPE'}, $type);
282             delete($INC{$p});
283         }
284     }
285
286     _debug("Loaded %d plugins", $#g+1);
287
288     return @g;
289 }
290
291 #
292 # Populate the @grabbers and @getters lists from the given
293 # directory
294 #
295 sub _load_modules($) {
296
297     my $path = shift;
298
299     foreach (keys(%INC)) {
300         if ($INC{$_} =~ m|^$path|) {
301             _debug("Removing %s from \$INC", $_);
302             delete($INC{$_});
303         }
304     }
305     @grabbers = _ploader($path, '.*Grabber\.pm$', 'grabber');
306     @getters = _ploader($path, '.*Getter\.pm$', 'getter');
307 }
308
309 #
310 # Wrapper functions for config management to put in
311 # debugging
312 #
313 sub _config_get {
314     my $path = shift;
315     my $dotpath = join('.', @{$path});
316     my $value;
317
318     if ($config_cache && exists($config_cache{$dotpath}) && exists($config_cache{$dotpath}->{value})) {
319         $value = $config_cache{$dotpath}->{value};
320     } else {
321         $value = $remote_api->{config_get}->($path);
322         $config_cache{$dotpath} = {value => $value, has => 1};
323
324     }
325
326     _debug("config: getting %s=%s", $dotpath, $value);
327     return $value;
328 }
329
330 sub _config_set {
331     my $path = shift;
332     my $dotpath = join('.', @{$path});
333     my $value = shift;
334
335     _debug("config: setting %s=%s", $dotpath, $value);
336     $config_cache{$dotpath} = {value => $value, has => 1};
337     return $remote_api->{config_set}->($path, $value);
338 }
339
340 sub _config_has {
341     my $path = shift;
342     my $dotpath = join('.', @{$path});
343     my $b;
344
345     if ($config_cache && exists($config_cache{$dotpath}) && exists($config_cache{$dotpath}->{has})) {
346         $b = $config_cache{$dotpath}->{has};
347     } else {
348         $b = $remote_api->{config_has}->($path);
349         $config_cache{$dotpath}->{has} = $b;
350     }
351
352     _debug("config: testing %s (%s)", $dotpath, $b?'true':'false');
353     return $b;
354 }
355
356 sub _config_del {
357     my $path = shift;
358     my $dotpath = join('.', @{$path});
359
360     _debug("config: removing %s", $dotpath);
361     delete($config_cache{$dotpath});
362     $remote_api->{config_del}->($path);
363 }
364
365 #
366 # The _config_list_* are helper functions taking a path to a comma separated
367 # string. The string is interpreted as a list and the action performed
368 # on it, storing back the modified version
369 #
370
371 #
372 # Add an item to the list, checking for duplicates
373 #
374 sub _config_list_add {
375     my $path = shift;
376     my $item = shift;
377     my @c;
378
379     if (_config_has($path)) {
380         @c = split(/\s*,\s*/, _config_get($path));
381     } else {
382         @c = ();
383     }
384
385     _debug("Adding %s to list %s", $item, join(".", @{$path}));
386     unless(grep { $_ eq $item } @c) {
387         push(@c, $item);
388     };
389
390     _config_set($path, join(',', @c));
391 }
392
393 #
394 # Remove an item from the list
395 #
396 sub _config_list_del {
397     my $path = shift;
398     my $item = shift;
399     my @c;
400
401     unless(_config_has($path)) {
402         return;
403     }
404
405     _debug("Removing %s from list %s", $item, join('.', @{$path}));
406     @c = grep { $item ne $_ } split(/\s*,\s*/, _config_get($path));
407
408     _config_set($path, join(',', @c));
409 }
410
411 #
412 # Return true if the item contains the given list, false otherwise
413 #
414 sub _config_list_has {
415     my $path = shift;
416     my $item = shift;
417
418     unless(_config_has($path)) {
419         return 0;
420     }
421
422     _debug("Checking for %s in list %s",  $item, join('.', @{$path}));
423
424     return grep { $item eq $_ } split(/\s*,\s*/, _config_get($path));
425 }
426
427 #
428 # Replace a list with the given items
429 #
430 sub _config_list_set {
431     my $path = shift;
432
433     _debug("Replacing %s with (%s)", join('.', @{$path}), join(",", @_));
434
435     _config_set($path, join(',', @_));
436 }
437
438 #
439 # Return the list of currently active connectors, in the configured
440 # order
441 #
442 sub _connectorlist {
443     my $key = shift;
444     my @c;
445
446     foreach(split(/,/, _config_get([$key]))) {
447         push(@c, _unserialize_connector_hash($_));
448     }
449
450     return @c;
451 }
452
453 #
454 # Convert a connector hash from it's config structure back to a perl
455 # hash
456 #
457 sub _unserialize_connector_hash {
458     my $name = shift;
459     my $connector = {};
460
461     if (_config_has(['connectors', $name, 'name'])) {
462         $connector->{name} = _config_get(['connectors', $name, 'name']);
463         $connector->{schemas} = {};
464         foreach ('http', 'https') {
465             if (_config_has(['connectors', $name, 'schemas', $_])) {
466                 $connector->{schemas}->{$_} = _config_get(['connectors', $name, 'schemas', $_]);
467             }
468         }
469     }
470
471     _debug("Returning connector %s: %s", $name, Dumper($connector));
472
473     return $connector;
474 }
475
476 #
477 # Push a new output function on the IO stack.
478 #
479 sub _push_output {
480     unshift(@outputstack, shift);
481 }
482
483 #
484 # Pop the topmost output function from the stack, leaving
485 # at least one function on it.
486 #
487 sub _pop_output {
488     if (scalar(@outputstack) > 0) {
489         shift(@outputstack);
490     }
491 }
492
493 #
494 # Takes a string and replaces commonly used URL shorteners recursively,
495 # up to 10 levels deep
496 #
497 sub _expand_url_shortener {
498     my $s = shift;
499     my $os = '';
500     my @urlshortener = (
501         'is\.gd/[[:alnum:]]+',
502         'otf\.me/[[:alnum:]]+',
503         'hel\.me/[[:alnum:]]+',
504         '7ax\.de/[[:alnum:]]+',
505         'ow\.ly/[[:alnum:]]+',
506         'j\.mp/[[:alnum:]]+',
507         'bit\.ly/[[:alnum:]]+',
508         'tinyurl\.com/[[:alnum:]]+',
509         'pop\.is/[[:alnum:]]+',
510         'post\.ly/[[:alnum:]]+',
511         '1\.ly/[[:alnum:]]+',
512         '2\.ly/[[:alnum:]]+',
513         't\.co/[[:alnum:]]+',
514         'shar\.es/[[:alnum:]]+',
515         'goo\.gl/[[:alnum:]]+',
516         );
517     my $ua = LWP::UserAgent->new(agent => 'Mozilla', max_redirect => 0, timeout => 5);
518     my $i = 10;
519
520     OUTER: while (($os ne $s) and ($i > 0)) {
521         study($s);
522         $os = $s;
523         $i--;
524
525         foreach my $pattern (@urlshortener) {
526             my $p = "https?:\/\/" . $pattern;
527
528             _debug("Matching %s against %s", $p, $s);
529             if ($s =~ m|($p)|) {
530                 my $matched = $1;
531                 my $res;
532
533                 _debug("Found %s", $matched);
534                 $res = $ua->head($matched);
535                 if ($res->is_redirect()) {
536                     my $new = $res->headers()->header("Location");
537
538                     _debug("Replacing %s with %s", $matched, $new);
539                     $s =~ s/$matched/$new/;
540                     next OUTER;
541                 } else {
542                     _debug("Error resolving %s", $matched);
543                 }
544             }
545         }
546     }
547
548     if ($i == 0) {
549         _debug("Loop terminated by counter");
550     }
551
552     _debug("Final string: %s", $s);
553
554     return $s;
555 }
556
557 #
558 # Save the config to durable storage
559 #
560 sub _cmd_save {
561     my $event = shift;
562
563     if ($remote_api->{config_save}->()) {
564         _io("Config saved");
565     } else {
566         _io(sprintf("%sConfig save failed%s", _colorpair("*red"), _colorpair()));
567     }
568 }
569
570 #
571 # Set a configuration element
572 #
573 sub _cmd_set {
574     my $event = shift;
575     my $target = shift;
576     my $key = shift;
577     my $val = shift;
578     my $p;
579
580     foreach $p (@getters, @grabbers) {
581         if ($p->{'NAME'} eq $target) {
582             $p->setval($key, $val);
583             return;
584         }
585     }
586     _io('No such module');
587 }
588
589
590 #
591 # Enable a given module
592 #
593 sub _cmd_enable {
594     my $event = shift;
595     my $target = shift;
596     my $p;
597
598     foreach $p (@grabbers) {
599         if ($p->{'NAME'} eq $target) {
600             $p->enable();
601             return;
602         }
603     }
604     _io('No such module');
605 }
606
607 #
608 # Disable given module
609 #
610 sub _cmd_disable {
611     my $event = shift;
612     my $target = shift;
613     my $p;
614
615     foreach $p (@grabbers) {
616         if ($p->{'NAME'} eq $target) {
617             $p->disable();
618             return;
619         }
620     }
621     _io('No such module');
622 }
623
624 #
625 # Show settings for modules
626 #
627 sub _cmd_show {
628     my $event = shift;
629     my $target = shift;
630     my $p;
631     my $e;
632
633     if (defined($target)) {
634         foreach $p (@getters, @grabbers) {
635             if ($p->{'NAME'} eq $target) {
636                 _io($p->getconfstr());
637                 return;
638             }
639         }
640         _io('No such module');
641     } else {
642         _io('Loaded grabbers (* denotes enabled modules):');
643         foreach $p (@grabbers) {
644             $e = $p->_getval('enabled');
645             _io(' %s%s', $p->{'NAME'}, $e?'*':'');
646         };
647
648         _io('Loaded getters:');
649         foreach $p (@getters) {
650             _io(' %s', $p->{'NAME'});
651         };
652     }
653 }
654
655 #
656 # Show help for the commands
657 #
658 sub _cmd_help {
659     my $event = shift;
660     my $target = shift;
661     my $p;
662
663     if (defined($target)) {
664         foreach $p (@getters, @grabbers) {
665             if ($p->{'NAME'} eq $target) {
666                 _io($p->gethelpstr());
667                 return;
668             }
669         }
670         _io('No such module');
671     } else {
672         _io(<<'EOT');
673 Supported commands:
674  save: save the current configuration
675  help [modulename]: display this help, or module specific help
676  show [modulename]: show loaded modules, or the current parameters of a module
677  set modulename parameter value: set a module parameter to a new value
678  getter [modulename]: display or set the getter to use
679  enable [modulename]: enable the usage of this module (grabbers only)
680  disable [modulename]: disable the usage of this module (grabbers only)
681  reload: reload all modules (this is somewhat experimental)
682  mode [modename]: display or set the operation mode (download/display)
683  connector [subcommand]: manage connectors (proxies)
684  debug: enable debugging messages
685  nodebug: disable debugging messages
686 EOT
687     }
688 }
689
690 #
691 # Set the getter to use
692 #
693 sub _cmd_getter {
694     my $event = shift;
695     my $target = shift;
696     my $p;
697
698     if (defined($target)) {
699         foreach $p (@getters) {
700             if ($p->{'NAME'} eq $target) {
701                 $getter = $p;
702                 _config_set(['getter'], $target);
703                 _io("Getter changed to %s", $target);
704                 return;
705             }
706         }
707         _io('No such getter');
708     } else {
709         _io('Current getter: %s', _config_get(['getter']));
710     }
711 }
712
713 #
714 # Show/set the working mode
715 #
716 sub _cmd_mode {
717     my $event = shift;
718     my $mode = shift;
719
720     if (defined($mode)) {
721         $mode = lc($mode);
722         if (('download' eq $mode) or ('display' eq $mode)) {
723             _config_set(['mode'], $mode);
724             _io('Now using %s mode', $mode);
725         } else {
726             _io('Invalid mode: %s', $mode);
727         }
728     } else {
729         _io('Current mode: %s', _config_get(['mode']));
730     }
731 }
732
733
734 #
735 # Manage the connectors
736 #
737 sub _cmd_connector {
738     my $event = shift;
739     my $subcmd = shift;
740     my $c;
741
742     unless(defined($subcmd)) {
743         $subcmd = "help";
744     }
745
746     $subcmd = lc($subcmd);
747
748     if ($subcmd eq 'list') {
749         _io("Defined connectors");
750         foreach $c (_connectorlist('defined-connectors')) {
751             _io($c->{name});
752             my $schemas = $c->{schemas};
753             if (scalar(keys(%{$schemas})) == 0) {
754                 _io(" No schemas defined");
755             } else {
756                 foreach (keys(%{$schemas})) {
757                     _io(' %s: %s', $_, $schemas->{$_});
758                 }
759             }
760         }
761
762         _io();
763         _io("Selected connectors: %s", _config_get(['active-connectors']));
764     } elsif ($subcmd eq 'add') {
765         my ($name) = @_;
766
767         unless(defined($name)) {
768             _io("No name given");
769             return;
770         }
771
772         $name = lc($name);
773
774         unless($name =~ m|^[a-z]+$|) {
775             _io("%s is not a valid connector name (only letters are allowed)", $name);
776             return;
777         }
778
779         if (_config_list_has(['defined-connectors'], $name)) {
780             _io("Connector already exists");
781             return;
782         }
783
784         _config_set(['connectors', $name, 'name'], $name);
785         _config_list_add(['defined-connectors'], $name);
786     } elsif ($subcmd eq 'del') {
787         my ($name) = @_;
788         my @dcon;
789
790         unless(defined($name)) {
791             _io("No name given");
792             return;
793         }
794
795         unless (_config_list_has(['defined-connectors'], $name)) {
796             _io("Connector does not exist");
797             return;
798         }
799
800         if (_config_has(['connectors', $name, '_immutable'])) {
801             _io("Connector cannot be removed");
802             return;
803         }
804
805         # Remove from list of active connectors
806         _config_list_del(['defined-connectors'], $name);
807         _config_list_del(['active-connectors'], $name);
808
809         _config_del(['connectors', $name, 'name']);
810         _config_del(['connectors', $name, '_immutable']);
811         _config_del(['connectors', $name, 'schemas', 'http']);
812         _config_del(['connectors', $name, 'schemas', 'https']);
813
814         @dcon = split(/,/, _config_get(['active-connectors']));
815
816         if (scalar(@dcon) == 0) {
817             _io("List of selected connectors is empty, resetting to direct");
818             _config_list_add(['active-connectors', 'direct']);
819         }
820     } elsif ($subcmd eq 'addschema') {
821         my ($conn, $schema, $proxy) = @_;
822
823         unless(defined($conn)) {
824             _io("No connector name given");
825             return;
826         }
827
828         unless(defined($schema)) {
829             _io("No schema given");
830             return;
831         }
832
833         unless(defined($proxy)) {
834             _io("No proxy given");
835             return;
836         }
837
838         $conn = lc($conn);
839         unless(_config_list_has(['defined-connectors'], $conn)) {
840             _io("Connector does not exist");
841             return;
842         }
843
844         if (_config_has(['connectors', $conn, '_immutable'])) {
845             _io("Connector cannot be modified");
846             return;
847         }
848
849         $schema = lc($schema);
850         _config_set(['connectors', $conn, 'schemas', $schema], $proxy);
851     } elsif ($subcmd eq 'delschema') {
852         my ($conn, $schema) = @_;
853
854         unless(defined($conn)) {
855             _io("No connector name given");
856             return;
857         }
858
859         unless(defined($schema)) {
860             _io("No schema given");
861             return;
862         }
863
864         $conn = lc($conn);
865         unless(_config_list_has(['defined-connectors'], $conn)) {
866             _io("Connector does not exist");
867             return;
868         }
869
870         $schema = lc($schema);
871         _config_del(['connectors', $conn, 'schemas', $schema]);
872     } elsif ($subcmd eq 'select') {
873         my @connlist = map { lc } @_;
874
875         if (scalar(@connlist) == 0) {
876             _io("No connectors given");
877             return;
878         }
879
880         foreach (@connlist) {
881             unless(_config_list_has(['defined-connectors'], $_)) {
882                 _io("Connector %s does not exist", $_);
883                 return;
884             }
885         }
886
887         _config_list_set(['active-connectors'], @connlist);
888     } else {
889         _io("connector [list|add|del|addschema|delschema|help] <options>");
890         _io(" help: Show this help");
891         _io(" list: List the defined connectors");
892         _io(" add <name>: Add a connector with name <name>");
893         _io(" del <name>: Delete the connector with name <name>");
894         _io(" addschema <name> <schema> <proxy>: Add proxy to connector for the given schema");
895         _io(" delschema <name> <schema>: Remove the schema from the connector");
896         _io(" select <name> [<name>...]: Select the connectors to use");
897     }
898 }
899
900 #
901 # Enable debug.
902 # Global debug if the keyword "all" is given, or just for the
903 # current window otherwise
904 #
905 sub _cmd_debug {
906     my $event = shift;
907     my $scope = shift;
908
909     if (defined($scope) and (lc($scope) eq 'all')) {
910         _io("Global debug enabled");
911         $debug = 1;
912     } else {
913         _io("Debug for this window enabled");
914         $debugwindows{$event->{window}} = 1;
915     }
916 }
917
918 #
919 # Disable debug
920 # Disable global debug if the keyword "all" is given (this will
921 # also disable all per-window debugs) or just for the current
922 # window
923 #
924 sub _cmd_nodebug {
925     my $event = shift;
926     my $scope = shift;
927
928     if (defined($scope) and (lc($scope) eq 'all')) {
929         $debug = 0;
930         %debugwindows = ();
931         _io("Global debug disabled");
932     } else {
933         delete($debugwindows{$event->{window}});
934         _io("Debug for this window disabled");
935     }
936 }
937
938 #
939 # Handle generic service commands
940 #
941 sub _cmd_service {
942     my $event = shift;
943     my $subcmd = shift || '';
944
945     $subcmd = lc($subcmd);
946
947     if ($subcmd eq 'cache') {
948         _cmd_service_cache($event, @_);
949     }
950 }
951
952
953 #
954 # Display or clear the content of the config cache
955 #
956 sub _cmd_service_cache {
957     my $event = shift;
958     my $subcmd = shift;
959
960     $subcmd = 'list' unless defined($subcmd);
961     $subcmd = lc($subcmd);
962
963     if ($subcmd eq 'list') {
964         _io("Content of config cache:");
965         foreach (sort(keys(%config_cache))) {
966             if (exists($config_cache{$_}->{value})) {
967                 _io(" %s => %s", $_, $config_cache{$_}->{value});
968             } else {
969                 _io(" %s present", $_);
970             }
971         }
972     } elsif ($subcmd eq 'clear') {
973         %config_cache = ();
974         _io("Cache cleared");
975     }
976 }
977
978
979 #
980 # Return the list of loaded grabbers.
981 # This is used by the test programs, and is not meant to be
982 # used in general.
983 #
984 sub _grabbers {
985     return @grabbers;
986 }
987
988 #
989 # ==============================================
990 # Builtin config handling functions
991 # These are used if the library used does not
992 # register it's own config_* handlers
993 # ==============================================
994 #
995 sub _builtin_config_init {
996
997     if (defined($builtin_config_path)) {
998         my $filename = File::Spec->catfile($builtin_config_path, 'videosite.json');
999
1000         _debug("Trying to load configuration from %s", $filename);
1001
1002         if (-r $filename) {
1003             eval {
1004                 local $/;
1005                 open(CONF, '<', $filename);
1006                 %builtin_config = %{JSON->new->utf8->decode(<CONF>)};
1007                 close(CONF);
1008             } or do {
1009                 _io("Error loading configuration: %s", $@);
1010             }
1011         };
1012     } elsif (defined($builtin_config_default)) {
1013         _debug("Initializing builtin config from external default");
1014         foreach (keys(%{$builtin_config_default})) {
1015             _debug("Setting %s=%s", $_, $builtin_config_default->{$_});
1016             $builtin_config{$_} = $builtin_config_default->{$_};
1017         }
1018     }
1019 }
1020
1021 sub _builtin_config_get {
1022     return $builtin_config{join(".", @{$_[0]})};
1023 }
1024
1025 sub _builtin_config_set {
1026     $builtin_config{join(".", @{$_[0]})} = $_[1];
1027 }
1028
1029 sub _builtin_config_has {
1030     return exists($builtin_config{join(".", @{$_[0]})});
1031 }
1032
1033 sub _builtin_config_save {
1034
1035     if (defined($builtin_config_path)) {
1036         my $filename = File::Spec->catfile($builtin_config_path, 'videosite.json');
1037
1038         _debug("Attempting to save config to %s", $filename);
1039
1040         eval {
1041             my ($tempfile, $tempfn) = tempfile("videosite.json.XXXXXX", dir => $builtin_config_path);
1042             print $tempfile JSON->new->pretty->utf8->encode(\%builtin_config);
1043             close($tempfile);
1044             rename($tempfn, $filename);
1045         } or do {
1046             return 0;
1047         }
1048     }
1049
1050     return 1;
1051 }
1052
1053 sub _builtin_config_del {
1054     delete($builtin_config{join(".", @{$_[0]})});
1055 }
1056
1057 #
1058 # ==============================================
1059 # From this point on publicly callable functions
1060 # ==============================================
1061 #
1062
1063
1064 #
1065 # Initialization function for the library
1066 # Actually not the first thing to be called, it expects an API
1067 # has (register_api) to be registered first
1068 #
1069 sub init {
1070     unless($remote_api) {
1071         $error = "No API set";
1072         return 0;
1073     }
1074
1075     # Initialize configuration data
1076     $remote_api->{config_init}->();
1077
1078     # Check/create default values, if they do not exist
1079     _recursive_hash_walk($defaultconfig, \&_init_config_item);
1080
1081     # Load modules
1082     _load_modules(File::Spec->catfile($remote_api->{module_path}->(), 'videosite'));
1083
1084     unless (@grabbers && @getters) {
1085         _io('No grabbers or no getters found, can not proceed.');
1086         return 0;
1087     }
1088
1089     # Set the getter
1090     $getter = $getters[0];
1091     foreach my $p (@getters) {
1092         if (_config_get(['getter']) eq $p->{'NAME'}) {
1093             $getter = $p;
1094         }
1095     }
1096     _debug('Selected %s as getter', $getter->{'NAME'});
1097     _config_set(['getter'], $getter->{'NAME'});
1098
1099     # Done.
1100     _io('initialized successfully');
1101     return 1;
1102 }
1103
1104 #
1105 # Register a remote API. This API contains a basic output function (used
1106 # when no window specific function is available), some config functions
1107 # and a color code function.
1108 #
1109 sub register_api {
1110     my $a = shift;
1111     my @config_functions = qw(config_init config_set config_get config_has config_save config_del);
1112     my $c;
1113     my @missing;
1114
1115     unless(defined($a)) {
1116         die("No API given");
1117     }
1118
1119     #
1120     # The config_* handlers are special in that they either all have
1121     # provided by the user, or none. In the latter case builtin
1122     # handlers will be used, but the config will not persist.
1123     #
1124     $c = 0;
1125     foreach (@config_functions) {
1126         if (exists($a->{$_})) {
1127             $c++;
1128         } else {
1129             push(@missing, $_);
1130         }
1131     }
1132
1133     unless (($c == 0) or ($c == scalar(@config_functions))) {
1134         $error = sprintf("Missing config function: %s", $missing[0]);
1135         return 0;
1136     }
1137
1138     foreach (keys(%{$a})) {
1139         if (ref($a->{$_}) ne 'CODE') {
1140             $error = sprintf("API handler %s is not a subroutine reference", $_);
1141         }
1142         $remote_api->{$_} = $a->{$_};
1143     }
1144
1145     if (exists($a->{_debug})) {
1146         $debug = $a->{_debug}->();
1147     }
1148
1149     if (exists($a->{_config_path})) {
1150         $builtin_config_path = $a->{_config_path}->();
1151     }
1152
1153     if (exists($a->{_config_default})) {
1154         $builtin_config_default = $a->{_config_default}->();
1155     }
1156
1157     if (exists($a->{_config_cache})) {
1158         $config_cache = $a->{_config_cache}->();
1159     }
1160
1161     @outputstack = ({io => $remote_api->{'io'}, window => ""});
1162
1163     return 1;
1164 }
1165
1166 #
1167 # Check a message for useable links
1168 #
1169 sub check_for_link {
1170     my $event = shift;
1171     my $message = $event->{message};
1172     my $g;
1173     my $m;
1174     my $p;
1175     my $skip;
1176     my $mode = _config_get(['mode']);
1177
1178
1179     #
1180     # If /nosave is present in the message switch to display mode, regardless
1181     # of config setting
1182     #
1183     if ($message =~ m,(?:\s|^)/nosave(?:\s|$),) {
1184         $mode = 'display';
1185     }
1186
1187     _push_output($event);
1188     $message = _expand_url_shortener($message);
1189
1190     study($message);
1191
1192     # Offer the message to all Grabbers in turn
1193     GRABBER: foreach $g (@grabbers) {
1194         ($m, $p) = $g->get($message);
1195         while (defined($m)) {
1196             _debug('Metadata: %s', Dumper($m));
1197             $skip = 0;
1198             if (exists($remote_api->{link_callback})) {
1199                 $skip = $remote_api->{link_callback}->($m);
1200             }
1201             unless($skip) {
1202                 if ('download' eq $mode) {
1203                     _io(
1204                         sprintf('%s>>> %sSaving %s%%s%s %s%%s',
1205                             _colorpair('*red'),
1206                             _colorpair(),
1207                             _colorpair('*yellow'),
1208                             _colorpair(),
1209                             _colorpair('*green'),
1210                         ),
1211                         $m->{'SOURCE'},
1212                         $m->{'TITLE'}
1213                     );
1214                     unless($getter->get($m)) {
1215                         _io(sprintf('%s>>> FAILED', _colorpair('*red')));
1216                     }
1217                 } elsif ('display' eq $mode) {
1218                     _io(
1219                         sprintf('%s>>> %sSaw %s%%s%s %s%%s',
1220                             _colorpair('*magenta'),
1221                             _colorpair(),
1222                             _colorpair('*yellow'),
1223                             _colorpair(),
1224                             _colorpair('*green')
1225                         ),
1226                         $m->{'SOURCE'},
1227                         $m->{'TITLE'}
1228                     );
1229                 } else {
1230                     _io(sprintf('%s>>> Invalid operation mode', _colorpair('*red')));
1231                 }
1232             }
1233
1234             # Remove the matched part from the message and try again (there may be
1235             # more!)
1236             $message =~ s/$p//;
1237             study($message);
1238             last GRABBER if ($message =~ /^\s*$/);
1239
1240             ($m, $p) = $g->get($message);
1241         }
1242     }
1243
1244     _pop_output();
1245 }
1246
1247 #
1248 # Handle a videosite command (/videosite ...) entered in the client
1249 #
1250 sub handle_command {
1251     my $event = shift;
1252     my ($cmd, @params) = split(/\s+/, $event->{message});
1253
1254     _push_output($event);
1255
1256     if (exists($videosite_commands->{$cmd})) {
1257         $videosite_commands->{$cmd}->($event, @params);
1258     }
1259
1260     _pop_output();
1261 }
1262
1263 1;