Update perldocs in general plugins to point to current website/mailing list.
[blosxom-plugins.git] / general / allconsuming
blob421194c6315164944679d28cbc11a095d4880ef4
1 # Blosxom Plugin: allconsuming                                    -*- cperl -*-
2 # Author: Todd Larason (jtl@molehill.org)
3 # Version: 0+4i
4 # Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
5 # Netflix plugin Home/Docs/Licensing:
6 # http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/AllConsuming/
7 package allconsuming;
9 # http://allconsuming.net/news/000012.html
11 # -------------- Configuration Variables --------------
12 # AllConsuming username
13 $username = undef
14   unless defined $username;
16 # Amazon Associate ID; feel free to leave this =)
17 $associate_id = 'mtmolel-20'
18   unless defined $associate_id;
20 # undef == "list all"
21 #     0 == "don't list at all"
22 #    >0 == list first N (or all, if < N)
23 #    <0 == list random N (or all in random order, if < N)
24 %num = (
25         purchased => 5,          # most recent 5
26         reading   => undef,      # all
27         rereading => undef,      # all
28         favorite  => -5,         # random 5
29         completed => 5,          # most recent 5
30         nofinish  => 0           # none
31        ) unless scalar keys %num > 0;
33 # one of: SOAP::Lite, LWP, wget (or a pathname to wget), curl (or a pathname)
34 # SOAP::Lite should be fastest and most likely to stay working long-term,
35 # but is the hardest to get installed
36 $networking = 'LWP'
37   unless defined $networking;
39 # Whether to try to use caching; default is yes, and caching is very
40 # strongly recommended
41 $use_caching = 1
42   unless defined $use_caching;
44 # how long to go between re-fetching the data, in seconds
45 # default value is 1 week
46 $max_cache_data_age = 60 * 60 * 24 * 7
47   unless defined $max_cache_data_age;
49 # how long to go between re-formatting the lists, in seconds
50 # default is 5 minutes
51 $max_cache_layout_age = 60 * 5
52   unless defined $max_cache_layout_age;
54 $debug_level = 1
55   unless defined $debug_level;
56 # -----------------------------------------------------
58 $purchased = '';
59 $reading   = '';
60 $rereading = '';
61 $favorite  = '';
62 $completed = '';
63 $nofinish  = '';
65 use CGI qw/param/;
66 use strict;
67 use vars qw/$username $associate_id $max_cache_data_age $max_cache_layout_age
68   %num $networking $use_caching $debug_level
69   $purchased $reading $rereading $favorite $completed $nofinish/;
71 my $cache;
72 my $package = "allconsuming";
73 my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
74 my $save_cache = 0;
76 # General utility functions
78 sub debug {
79     my ($level, @msg) = @_;
81     if ($debug_level >= $level) {
82         print STDERR "$package debug $level: @msg\n";
83     }
86 sub load_template {
87     my ($bit) = @_;
88     return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
91 sub report {
92     my ($bit, $listname, $title, $author, $asin, $image, $allconsuming_url, $amazon_url) = @_;
93     my $f   = load_template("$listname.$bit") || load_template($bit);
94     $title  = encode_entities($title);
95     $author = encode_entities($author);
96     $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
97     return $f;
100 sub encode_entities {
101     my ($text) = @_;
102     eval "require HTML::Entities";
103     if ($@) {
104         my %map = ('<' => 'lt', '&' => 'amp', '>' => 'gt');
105         my $keys = join '',keys %map;
106         $text =~ s:([$keys]):&$map{$1};:g;
107         return $text;
108     }
109     return HTML::Entities::encode_entities($text);
112 # General networking
114 sub GET {
115     my ($url) = @_;
117     if ($networking =~ m:curl:) {
118         return `$networking -m 30 -s "$url"`;
119     } elsif ($networking =~ m:wget:) {
120         return `$networking --quiet -O - "$url"`;
121     } elsif ($networking eq 'LWP') {
122         foreach (qw/LWP::UserAgent HTTP::Request::Common/) {
123             eval "require $_";
124             if ($@) {
125                 debug(0, "Can't load $_, can't use LWP networking: $@");
126                 return undef;
127             }
128         }
129         my $ua  = LWP::UserAgent->new;
130         my $res = $ua->request(HTTP::Request::Common::GET $url);
131         if (!$res->is_success) {
132             my $error = $res->status_line;
133             debug(0, "HTTP GET error: $error");
134             return undef;
135         }
136         return $res->content;
137     } else {
138         debug(0, "ERROR: invalid \$networking $networking");
139     }
142 # AllConsuming-specific networking
144 sub allconsuming_handle {
145     if ($networking eq 'SOAP::Lite') {
146         eval "require SOAP::Lite;";
147         if ($@) {
148             debug(0, "SOAP::Lite couldn't be loaded");
149             return undef;
150         }
151         my @now = localtime;
152         my $soap = SOAP::Lite
153           -> uri('http://www.allconsuming.net/AllConsumingAPI')
154             -> proxy('http://www.allconsuming.net/soap.cgi');
155         my $obj = $soap
156               -> call(new => $now[2], $now[3], $now[4] + 1, $now[5] + 1900)
157                 -> result;
158         return {soap => $soap,
159                 obj  => $obj,
160                 map  => {purchased => 'GetPurchasedBooksList',
161                          reading   => 'GetCurrentlyReadingList',
162                          rereading => 'GetRereadingBooksList',
163                          favorite  => 'GetFavoriteBooksList',
164                          completed => 'GetCompletedBooksList',
165                          nofinish  => 'GetNeverFinishedBooksList'}
166                };
167     } else {
168         return {
169                 map => {purchased => 'purchased_books',
170                         reading   => 'currently_reading',
171                         rereading => 'rereading_books',
172                         favorite  => 'favorite_books',
173                         completed => 'completed_books',
174                         nofinish  => 'never_finished_books'}
175                };
176     }
179 sub allconsuming_lookup {
180     my ($handle, $username, $list) = @_;
182     return undef unless defined $handle;
184     if ($networking eq 'SOAP::Lite') {
185         return undef unless defined $handle->{map}{$list};
186         return $handle->{soap}
187           -> call($handle->{map}{$list} => $handle->{obj}, $username)
188             -> result;
189     } else {
190         my $data = GET('http://allconsuming.net/soap-client.cgi?' .
191                        "$handle->{map}{$list}=1&username=$username");
192         $data =~ s:\A\<pre>\$VAR1 =(.*)</pre>\Z:\1:ms;
193         return eval $data;
194     }
197 sub get_data {
198     if (defined $cache->{data} and
199         $^T - $cache->{data_timestamp} < $max_cache_data_age) {
200         return;
201     }
202     debug(1, "cache miss data");
203     $cache->{data_timestamp} = $^T;
204     my $obj = allconsuming_handle();
206     foreach (keys %num) {
207         next if defined($num{$_}) && $num{$_} == 0;
208         $cache->{data}{$_} = allconsuming_lookup($obj, $username, $_);
209     }
210     $save_cache = 1;
213 # Cache handling
215 sub prime_cache {
216     return if (!$use_caching);
217     eval "require Storable";
218     if ($@) {
219         debug(1, "cache disabled, Storable not available");
220         $use_caching = 0;
221         return 0;
222     }
223     if (!Storable->can('lock_retrieve')) {
224         debug(1, "cache disabled, Storable::lock_retrieve not available");
225         $use_caching = 0;
226         return 0;
227     }
228     $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : {});
229     if (defined(param('allconsuming'))) {
230         if (param('allconsuming') eq 'refresh_data') {
231             $cache = {};
232         } elsif (param('allconsuming') eq 'refresh_layout') {
233             $cache->{layout} = {};
234         }
235     }
238 sub save_cache {
239     return if (!$use_caching || !$save_cache);
240     debug(1, "Saving cache");
241     -d $blosxom::plugin_state_dir
242         or mkdir $blosxom::plugin_state_dir 
243         or (debug(0, "State dir $blosxom::plugin_state_dir nonexistant and noncreatable: $!") and return);
244     Storable::lock_store($cache, $cachefile);
247 sub build_list {
248     my ($listname, $num, $list) = @_;
249     my $count = 0;
250     my $results;
252     return '' if (defined $num and $num == 0);
253     $list = [$list] if (ref $list eq 'HASH');
254     if (defined $list and defined $num and $num < 0) {
255         # algorithm from Algorithm::Numerical::Shuffle by Abigail
256         for (my $i = @$list; -- $i;) {
257             my $r = int rand ($i + 1);
258             ($list -> [$i], $list -> [$r]) = ($list -> [$r], $list -> [$i]);
259         }
260         $num = -$num;
261     }
262     $results = report('head', $listname);
263     foreach (@$list) {
264         $results .= report('item', $listname,
265                            @{$_}{qw/title author asin image allconsuming_url
266                                    amazon_url/});
267         $count++;
268         last if (defined $num and $count == $num);
269     }
270     $results .= report('foot', $listname);
272     return $results;
275 # Blosxom plugin interface
277 sub head {
278     prime_cache();
279     get_data();
280     save_cache();
282     foreach (keys %num) {
283         next if defined($num{$_}) && $num{$_} == 0;
284         no strict;
285         $$_ = $cache->{layout}{$_}{$blosxom::flavour};
286         next if (defined $$_ &&
287                  ($^T - $cache->{layout_timestamp}{$_}{$blosxom::flavour}
288                   < $max_cache_layout_age));
289         debug(1, "cache miss layout $_ $blosxom::flavour");
290         $$_ = build_list($_, $num{$_}, $cache->{data}{$_}{asins});
291         $cache->{layout}{$_}{$blosxom::flavour} = $$_;
292         $cache->{layout_timestamp}{$_}{$blosxom::flavour} = $^T;
293         $save_cache = 1;
294         use strict;
295     }
296     save_cache();
297     
298     1;
301 sub start {
302     return 0 unless defined $username;
303     while (<DATA>) {
304         last if /^(__END__)?$/;
305         chomp;
306         my ($flavour, $comp, $txt) = split ' ',$_,3;
307         $txt =~ s:\\n:\n:g;
308         $blosxom::template{$flavour}{"$package.$comp"} = $txt;
309     }
310     return 1;
314 __DATA__
315 error head <table class="allconsuming $listname">\n
316 error item <tr><td><a href="http://www.amazon.com/exec/obidos/ASIN/$asin/$associate_id/ref=nosim"><img border="0" src="$image" alt="$title Book cover"></a></td><td><a href="http://www.amazon.com/exec/obidos/ASIN/$asin/$associate_id/ref=nosim"><i>$title</i></a>, $author</td></tr>\n
317 error foot </table>
318 __END__
320 =head1 NAME
322 Blosxom Plug-in: allconsuming
324 =head1 SYNOPSIS
326 Purpose: Lets you easily share your AllConsuming information
328   * $allconsuming::purchased -- list of books you've purchased
329   * $allconsuming::reading -- list of books you're reading
330   * $allconsuming::rereading -- list of books you're re-reading
331   * $allconsuming::favorite -- list of your favorite books
332   * $allconsuming::completed -- list of books you've completed
333   * $allconsuming::nofinish -- list of books you never finished
335 =head1 VERSION
337 0+3i
339 2nd test release
341 =head1 AUTHOR
343 Todd Larason  <jtl@molehill.org>, http://molelog.molehill.org/
345 This plugin is now maintained by the Blosxom Sourceforge Team,
346 <blosxom-devel@lists.sourceforge.net>.
348 =head1 BUGS
350 None known; please send bug reports and feedback to the Blosxom
351 development mailing list <blosxom-devel@lists.sourceforge.net>.
353 =head1 Customization
355 =head2 Configuration variables
357 C<$username> is your AllConsuming username.  Until it's defined, this plugin does nothing. 
359 C<$associate_id> is an Amazon Associate ID.  By default, it's mine;
360  change it to yours if you have one.
362 C<%num> sets how many items to include in each list.  Each of C<purchased>,
363 C<reading>, C<rereading>, C<favorite>, C<completed> and C<nofinish> can be
364 set separately.  Setting C<$num{foo}> to undef means to include the whole
365 list; setting it to 0 means to not build the list at all (or retrieve the
366 data from AllConsuming); setting it to a positive number N means to list the
367 first N items (or the whole list, if there aren't that many items) in order;
368 setting it to a negative number -N means to list a randomly selected set of
369 N items (or the whole list, in a random order, if there are fewer than N
370 items).
372 C<$networking> controls which networking implemenentation to use.  If set to
373 'SOAP::Lite', then the SOAP::Lite module will be used to communicate with
374 AllConsuming's official SOAP interface; this method is preferable for both
375 speed and reliability, but requires by far the most work to get working if
376 you don't already have the modules installed.  If set to 'LWP', then the
377 LWP family of modules will be used to communicate with a demonstration CGI
378 script.  If set to 'wget' or 'curl' (or a pathname that includes one of
379 those), then the respective external utility is used to communicate with
380 the demonstration CGI script.
382 C<$use_caching> is a boolean controlling whether to use caching at all.
383 Caching is very strongly recommended -- AllConsuming is rather slow.
385 C<$max_cache_data_age> sets how long to cache the downloaded AllConsuming
386 information for.  Fetching the data is pretty slow, so this defaults to a high
387 value -- 1 week.
389 C<$max_cache_layout_age> sets how long to cache the formatted data.
390 Formatting the data is relatively fast, so this defaults to a small value -- 5
391 minutes.  If you aren't modifying templates and aren't using randomized lists,
392 this can be set to the same as $max_cache_data_age without ill effects.
394 C<$debug_level> can be set to a value between 0 and 5; 0 will output
395 no debug information, while 5 will be very verbose.  The default is 1,
396 and should be changed after you've verified the plugin is working
397 correctly.
399 =head2 Classes for CSS control
401 There's are some classes used, available for CSS customization.
403   * C<allconsuming> -- all lists are in the netflix class
404   * C<purchased>, etc -- each list is also in a class named for the list
406 =head2 Flavour-style files
408 If you want a format change that can't be made by CSS, you can
409 override the HTML generated by creating files similar to Blosxom's
410 flavour files.  They should be named allconsuming.I<bit>.I<flavour>; for
411 available I<bit>s and their default meanings, see the C<__DATA__>
412 section in the plugin.
414 =head1 Caching
416 Because fetching the queue information is relatively slow, caching is very
417 strongly recommended.  Caching requires a version of the Storable module
418 that supports the 'lock_save' and 'lock_retrieve' functions.
420 Since the data reload is so slow, you may wish to raise the $max_cache_data_age
421 even higher, and use explicit cache reloading.  The cache can be reloaded
422 either by deleting the cache file $plugin_state_dir/.allconsuming.cache, or
423 by passing an C<allconsuming=refresh_data> parameter to the blosxom script;
424 the latter is preferable, as you can insure that you take the time hit, not
425 a random visitor.
427 =head1 LICENSE
429 this Blosxom Plug-in
430 Copyright 2003, Todd Larason
432 (This license is the same as Blosxom's)
434 Permission is hereby granted, free of charge, to any person obtaining a
435 copy of this software and associated documentation files (the "Software"),
436 to deal in the Software without restriction, including without limitation
437 the rights to use, copy, modify, merge, publish, distribute, sublicense,
438 and/or sell copies of the Software, and to permit persons to whom the
439 Software is furnished to do so, subject to the following conditions:
441 The above copyright notice and this permission notice shall be included
442 in all copies or substantial portions of the Software.
444 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
445 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
446 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
447 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
448 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
449 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
450 OTHER DEALINGS IN THE SOFTWARE.
452 =cut