File Coverage

blib/lib/XML/RSS/FromHTML.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package XML::RSS::FromHTML;
2 12     12   459311 use base Class::Accessor::Fast;
  12         32  
  12         12877  
3 12     12   82440 use strict;
  12         30  
  12         351  
4 12     12   74 use Carp;
  12         30  
  12         1036  
5 12     12   24757 use XML::RSS ();
  0            
  0            
6             use LWP::UserAgent ();
7             use HTTP::Cookies ();
8             use Data::Dumper ();
9             use bytes ();
10             use File::Basename ();
11             our $VERSION = '0.06';
12              
13             __PACKAGE__->mk_accessors(qw(
14             name
15             url
16             cacheDir
17             feedDir
18             rssObj
19             minInterval
20             passthru
21             debug
22             unicodeDowngrade
23             maxItemCount
24             outFileName
25             updateStatus
26             newItems
27             ));
28              
29             sub new {
30             my $self = shift;
31             my $p = bless({},$self);
32             # set default values
33             $p->name('myrss');
34             $p->cacheDir('.');
35             $p->feedDir('.');
36             $p->minInterval(300); # in seconds
37             $p->maxItemCount(30);
38             $p->passthru({});
39             $p->updateStatus('update not executed yet');
40             # initialize properties (for sub-classes)
41             $p->init(@_);
42             return $p;
43             }
44              
45             sub update {
46             my $self = shift;
47             ### define output files for debug
48             my $dbg = {
49             interval => $self->cacheDir.'/'.$self->name.'.intv',
50             html => $self->cacheDir.'/'.$self->name.'.html',
51             list => $self->cacheDir.'/'.$self->name.'.list',
52             update => $self->cacheDir.'/'.$self->name.'.update',
53             newcount => $self->cacheDir.'/'.$self->name.'.new.count',
54             };
55             if($self->debug){
56             unlink $dbg->{$_} foreach(keys %{$dbg});
57             }
58             ### check minimum interval
59             my ($getOk,$okTime,$nowTime) = $self->checkInterval();
60             unless($getOk){
61             # debug
62             if($self->debug){
63             open(OUT,'>',$dbg->{interval}) or confess $!;
64             print OUT "now : ${nowTime}\nok : ${okTime}";
65             }
66             $self->updateStatus("still under check interval time period");
67             return 0;
68             }
69             ### retrieve html
70             my $html = $self->getHTML( $self->url );
71             # debug
72             if($self->debug){
73             open(OUT,'>',$dbg->{html}) or confess $!;
74             print OUT $html."\n\n";
75             print OUT $self->url."\n";
76             }
77             ### html parsing
78             my $list = $self->makeItemList($html);
79             if(scalar @{$list} < 1){
80             $self->updateStatus("makeItemList returned with 0 item - html parse failure");
81             return 0;
82             }
83             # debug
84             if($self->debug){
85             open(OUT,'>',$dbg->{list}) or confess $!;
86             require 'Dumpvalue.pm';
87             select(OUT);
88             print Dumpvalue->new->dumpValue($list);
89             select(STDOUT);
90             }
91             ### caching
92             my ($update,$old_list,$size_new,$size_old) = $self->cache($list);
93             # debug
94             if($self->debug){
95             if($update){
96             open(OUT,'>',$dbg->{update}) or confess $!;
97             print OUT "new: $size_new\nold: $size_old\n";
98             }
99             }
100             ### read & parse old rss file
101             my $rss_old = $self->_loadOldRss();
102             ### remake RSS if update
103             if($update){
104             my ($rss_new,$new_count) = $self->remakeRSS($list,$old_list,$rss_old);
105             $self->rssObj($rss_new);
106             # debug
107             if($self->debug){
108             open(OUT,'>',$dbg->{newcount}) or confess $!;
109             print OUT "$new_count\n";
110             }
111             $self->updateStatus("updated with $new_count new items");
112             return 1;
113             }else{
114             $self->rssObj($rss_old);
115             $self->updateStatus("there was no new item");
116             return 0;
117             }
118             }
119              
120             sub checkInterval {
121             my $self = shift;
122             my $cache_file = $self->_getCacheFilePath();
123             return 1 if(!-f $cache_file);
124             return 1 if(!$self->minInterval);
125             my $okTime = ( stat($cache_file) )[9] + $self->minInterval;
126             my $nowTime = time();
127             return (1,$okTime,$nowTime) if($nowTime > $okTime);
128             return (0,$okTime,$nowTime);
129             }
130              
131             sub getHTML {
132             my $self = shift;
133             my $url = shift;
134             my $ua = LWP::UserAgent->new;
135             $ua->cookie_jar({ file => $self->cacheDir.'/'.$self->name.'.cookie' });
136             my $res = $ua->get($url);
137             confess q(couldn't retrieve html from ) . $url if(!$res->content);
138             return $res->content;
139             }
140              
141             sub cache {
142             my $self = shift;
143             my $list = shift;
144             my $cache_file = $self->_getCacheFilePath();
145             my $dump = Data::Dumper::Dumper($list);
146             my $len_new = bytes::length($dump);
147             my $len_old = -s $cache_file || 0;
148             # if there's an update
149             if($len_new != $len_old){
150             my $fh;
151             # read old cache file
152             my $old_data;
153             if(-f $cache_file){
154             open($fh,'<',$cache_file)
155             or confess "failed to open $cache_file - $!";
156             {
157             local ($/) = undef;
158             my $x = <$fh>;
159             ($x) = ($x =~ /(.+)/ms); # untaint
160             my $VAR1;
161             $old_data = eval($x);
162             }
163             close($fh);
164             }
165             # make new cache file
166             if($self->outFileName){
167             my $n = $self->outFileName;
168             $cache_file =~ s|[^/]+(\..+?)$|$n$1|o;
169             }
170             open($fh,'>',$cache_file)
171             or confess "failed to write-open $cache_file - $!";
172             print $fh Data::Dumper::Dumper($list);
173             return (1,$old_data,$len_new,$len_old);
174             }
175             # else then there's no update
176             return undef;
177             }
178              
179             sub remakeRSS {
180             my $self = shift;
181             my ($newlist,$oldlist,$oldrss) = @_;
182             my $rss_new = new XML::RSS(%{ $self->passthru });
183             # if old rss hold no items, which means the file was broken or removed,
184             # then we should reset the old list too, to remake all items again
185             if( scalar @{$oldrss->{items} || []} == 0 ){
186             $oldlist = [];
187             }
188             # find which item's new
189             my (@new,%chk,%chkInOldRss);
190             # making check hash
191             my $i=0;
192             foreach (@{ $oldlist }){
193             $chk{ $_->{link} } = $i;
194             $i++;
195             }
196             # making check hash - for items only exist in rss file, and not in cache
197             $i=0;
198             foreach (@{ $oldrss->{items} }){
199             next if($chk{ $_->{link} }); # ignore those in cache
200             $chkInOldRss{ $_->{link} } = $i;
201             $i++;
202             }
203             foreach my $p (@{ $newlist }){
204             # check for any content updates, compared to cache list
205             if(exists $chk{ $p->{link} }){
206             my $o = $oldlist->[ $chk{ $p->{link} } ];
207             my $oldlen = bytes::length(Data::Dumper::Dumper($o));
208             my $newlen = bytes::length(Data::Dumper::Dumper($p));
209             if($newlen != $oldlen){
210             # delete that old item from rss
211             my @tmp;
212             my $qr = qr/\Q$p->{link}\E/;
213             foreach my $old (@{ $oldrss->{items} }){
214             push(@tmp,$old) unless($old->{link} =~ /$qr/);
215             }
216             $oldrss->{items} = \@tmp;
217             push(@new,$p);
218             }
219             # else, check for duplicates
220             }elsif(exists $chkInOldRss{ $p->{link} }){
221             my @tmp;
222             my $qr = qr/\Q$p->{link}\E/;
223             foreach my $itm (@{ $oldrss->{items} }){
224             push(@tmp,$itm) unless($itm->{link} =~ /$qr/);
225             }
226             $oldrss->{items} = \@tmp;
227             push(@new,$p);
228             # if it's a brand new item
229             }else{
230             push(@new,$p);
231             }
232             }
233             # make rss for new items
234             my $new_count = 0;
235             for (my $i=0; $i < scalar @new; $i++){
236             last if (defined($self->maxItemCount) && $i == $self->maxItemCount);
237             $self->addNewItem($rss_new,$new[$i]);
238             $new_count++;
239             }
240             # add old items
241             my $now = scalar @new;
242             foreach my $itr (@{ $oldrss->{items} }){
243             last if (defined($self->maxItemCount) && $now >= $self->maxItemCount);
244             $rss_new->add_item(%{$itr});
245             $now++;
246             }
247             # set RSS definition
248             $self->defineRSS($rss_new);
249             # save to file
250             $self->_saveToFile($rss_new);
251             # set to $self->newItems property
252             my @newItems;
253             for (my $i=0; $i < scalar @new; $i++){
254             push(@newItems,$rss_new->{items}[$i]);
255             }
256             $self->newItems(\@newItems);
257             return ($rss_new,$new_count);
258             }
259              
260             sub as_string {
261             my $self = shift;
262             $self->_loadOldRss if(!$self->rssObj);
263             return $self->rssObj->as_string();
264             }
265              
266             sub as_object {
267             my $self = shift;
268             $self->_loadOldRss if(!$self->rssObj);
269             return $self->rssObj;
270             }
271              
272             sub name {
273             my $self = shift;
274             if(@_){
275             my $s = shift;
276             $s =~ s/[^a-zA-z0-9\-]/_/g;
277             $self->{name} = $s;
278             }
279             return $self->{name};
280             }
281              
282             sub getDateTime {
283             my $self = shift;
284             my $str = shift;
285             my $t;
286             require HTTP::Date;
287             if($str){
288             $t = HTTP::Date::str2time($str);
289             }
290             return HTTP::Date::time2str($t);
291             }
292              
293             sub _loadOldRss {
294             my $self = shift;
295             my $file = $self->_getFeedFilePath();
296             my $r = XML::RSS->new(%{ $self->{passthru} });
297             eval {
298             $r->parsefile($file) if(-f $file);
299             };
300             if( $@ || scalar( @{$r->{items} || []} ) < 1 ){
301             $self->updateStatus("old rss file was broken, so ignoring - $@");
302             }
303             if($self->unicodeDowngrade){
304             eval { require Unicode::RecursiveDowngrade };
305             if( $@ ){
306             warn 'you will need to install Unicode::RecursiveDowngrade module to use $self->unicodeDowngrade option';
307             }else{
308             $r = Unicode::RecursiveDowngrade->new->downgrade($r);
309             }
310             }
311             $self->rssObj($r);
312             return $r;
313             }
314              
315             sub _getCacheFilePath {
316             my $self = shift;
317             return $self->cacheDir.'/'.$self->name.'.cache';
318             }
319              
320             sub _getFeedFilePath {
321             my $self = shift;
322             return $self->feedDir.'/'.$self->name.'.xml';
323             }
324              
325             sub _saveToFile {
326             my $self = shift;
327             my $rss_new = shift;
328             my $saveFile = $self->_getFeedFilePath();
329             if($self->outFileName){
330             my $n = $self->outFileName;
331             my ($name, $dir, $suffix) = File::Basename::fileparse( $saveFile, qr/\.[^.]*/ );
332             $saveFile = "$dir$n$suffix";
333             }
334             $rss_new->save( $saveFile ) or confess $!;
335             return 1;
336             }
337              
338             # below are all must-override methods
339             sub init {
340             confess q(
341             must override this method with sub-class using the following interface:
342             sub init {
343             my $self = shift;
344             # set feed url, name, and other constant stuff here #
345             $self->url('http://target.site/updates.html');
346             $self->name('sample feed');
347             $self->passthru({
348             version => '1.0',
349             encode_output => 1,
350             });
351             return 1;
352             }
353             );
354             }
355              
356             sub makeItemList {
357             confess q(
358             must override this method with sub-class using the following interface:
359             sub makeItemList {
360             my $self = shift;
361             my $html = shift;
362             my @list;
363             # parse html and make an item list here #
364             while ($html =~ /(.+?)/){
365             push(@list,{
366             link => $1,
367             title => $2,
368             });
369             }
370             return \@list;
371             }
372             );
373             }
374              
375             sub addNewItem {
376             confess q(
377             must override this method with sub-class using the following interface:
378             sub addNewItem {
379             my $self = shift;
380             my ($rssObject,$item) = @_;
381             # create & add new item to rssObject using data in #
382             # $item hashRef, which you made in makeItemList() #
383             $rssObject->add_item(
384             link => $item->{link},
385             title => $item->{title},
386             );
387             return 1;
388             }
389             );
390             }
391              
392             sub defineRSS {
393             confess q(
394             must override this method with sub-class using the following interface:
395             sub defineRSS {
396             my $self = shift;
397             my $rssObject = shift;
398             # define rss channel info, and other stuffs here #
399             $rssObject->channel(
400             title => 'blabla rss feed',
401             description => 'foo bar',
402             link => 'http://mysite/rss/',
403             );
404             $rssObject->image(
405             title => "blabla rss feed",
406             url => "http://mysite/rss/feed.png",
407             );
408             return 1;
409             }
410             );
411             }
412              
413             1;
414             __END__