root/grabbers/abc2_website @ 102

Revision 102, 16.3 kB (checked in by max, 7 years ago)

Stop a bunch of warnings in the ABC grabbers.

  • Property svn:executable set to *
Line 
1#!/usr/bin/perl -w
2
3# abc2_website au_tv guide grabber - runs from "Shepherd" master grabber
4#  * written by ltd
5#  * uses ABC website for ABC2 data ((http://www.abc.net.au/tv/guide/)
6#  * when used in conjunction with Shepherd, shepherd can collect other channels
7#    using other grabbers
8#  * this does NOT use any config file - all settings are passed in from shepherd
9
10#  changelog:
11#    1.50  22sep06      added support for "shepherd" master grabber script
12#    1.51  02oct06      --ready option
13#    1.52  03oct06      split out abc grabber into its own grabber
14#    1.55  09oct06      formalize --cheap option
15
16use strict;
17
18my $progname = "abc2_website";
19my $version = "1.55_00oct06";
20
21use LWP::UserAgent;
22use Time::HiRes qw(gettimeofday tv_interval);
23use XMLTV;
24use POSIX qw(strftime mktime);
25use Getopt::Long;
26use HTML::TreeBuilder;
27use Data::Dumper;
28use Cwd;
29
30#
31# some initial cruft
32#
33
34my $script_start_time = [gettimeofday];
35my %stats;
36my $channels;
37my $tv_guide;
38my $data_cache;
39
40# lets make sure we look exactly like the yahoo widget engine...
41my $ua;
42BEGIN {
43        $ua = LWP::UserAgent->new(
44                'timeout' => 30,
45                'keep_alive' => 1,
46                'agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-us)'
47                );
48        $ua->env_proxy;
49        # $ua->cookie_jar({});
50        $| = 1;
51}
52
53#
54# parse command line
55#
56
57my $opt_days =          7;                              # default
58my $opt_offset =        0;                              # default
59my $opt_timezone =      "1000";                         # default
60my $opt_outputfile =    cwd() . "/abc2_website.xmltv";  # default
61my $opt_configfile =    cwd() . "/abc2_website.conf";   # ignored
62my $opt_cache_file =    cwd() . "/abc2_website.cache";  # ignored
63my $opt_channels_file=  "";
64my $opt_no_cache =      0;
65my $opt_cheap =         0;
66my $opt_fast =          0;
67my $opt_warper =        0;
68my $opt_obfuscate =     0;
69my $opt_no_extra_days = 0;
70my $opt_help =          0;
71my $opt_version =       0;
72my $opt_desc =          0;
73my $opt_dont_retry =    0;
74my $debug =             1;
75my $lang =              "en";
76my $region =            94;
77my $time_offset =       0;
78
79GetOptions(
80        'region=i'      => \$region,
81        'days=i'        => \$opt_days,
82        'offset=i'      => \$opt_offset,
83        'timezone=s'    => \$opt_timezone,
84        'channels_file=s' => \$opt_channels_file,
85        'output=s'      => \$opt_outputfile,
86        'config-file=s' => \$opt_configfile,
87        'cache-file=s'  => \$opt_cache_file,
88        'no-extra-days' => \$opt_no_extra_days,
89        'fast'          => \$opt_fast,
90        'no-cache'      => \$opt_no_cache,
91        'cheap'         => \$opt_cheap,
92        'debug+'        => \$debug,
93        'warper'        => \$opt_warper,
94        'lang=s'        => \$lang,
95        'obfuscate'     => \$opt_obfuscate,
96        'no-retry'      => \$opt_dont_retry,
97        'help'          => \$opt_help,
98        'verbose'       => \$opt_help,
99        'version'       => \$opt_version,
100        'ready'         => \$opt_version,
101        'desc'          => \$opt_desc,
102        'v'             => \$opt_help);
103
104&help if ($opt_help);
105
106if ($opt_version || $opt_desc) {
107        printf "%s %s\n",$progname,$version;
108        printf "%s is a details-aware grabber that collects decent quality data using the ABC website for ABC2 only.",$progname if $opt_desc;
109        exit(0);
110}
111
112die "no channel file specified, see --help for instructions\n", if ($opt_channels_file eq "");
113
114#
115# go go go!
116#
117
118# normalize starttime to an hour..
119my $starttime = time;
120my ($sec,$min,@rest) = localtime($starttime);
121$starttime -= ((60 * $min) + $sec);
122my $endtime = $starttime + (($opt_no_extra_days ? $opt_days : 14) * 86400);
123$starttime += (86400 * $opt_offset);
124
125&log(sprintf "going to %s %s%d%s days%s of data into %s (%s%s%s)",
126        ($opt_cheap ? "verify (cache-validate)" : "grab"),
127        ($opt_no_extra_days ? "" : "somewhere between "),
128        $opt_days,
129        ($opt_no_extra_days ? "" : " to 14"),
130        ($opt_offset ? " (skipping first %d days)" : ""),
131        $opt_outputfile,
132        ($opt_fast ? "with haste" : "slowly"),
133        ($opt_warper ? ", anonymously" : ""),
134        ($opt_no_cache ? ", without caching" : ", with caching"));
135
136# read channels file
137if (-r $opt_channels_file) {
138        local (@ARGV, $/) = ($opt_channels_file);
139        no warnings 'all'; eval <>; die "$@" if $@;
140} else {
141        die "WARNING: channels file $opt_channels_file could not be read\n";
142}
143
144&read_cache if ($opt_no_cache == 0);
145&get_abc_data($starttime,$endtime,"ABC2",$channels->{ABC2},"http://www.abc.net.au/tv/guide/abc2");
146&write_cache if ($opt_no_cache == 0);
147
148&write_data;
149&print_stats;
150exit(0);
151
152######################################################################################################
153# help
154
155sub help
156{
157        print<<EOF
158$progname $version
159
160options are as follows:
161        --help                  show these help options
162        --days=N                fetch 'n' days of data (default: $opt_days)
163        --output=file           send xml output to file (default: "$opt_outputfile")
164        --config-file=file      (ignored - historically used by grabbers not not this one)
165        --no-cache              don't use a cache to optimize (reduce) number of web queries
166        --cheap                 validate contents of cache - fetch summary only, not details
167        --cache-file=file       where to store cache (default "$opt_cache_file")
168        --fast                  don't run slow - get data as quick as you can - not recommended
169        --debug                 increase debug level
170        --warper                fetch data using WebWarper web anonymizer service
171        --obfuscate             pretend to be a proxy servicing multiple clients
172        --no-extra-days         don't fetch extra (14 days) from ABC website
173        --no-retry              if webserver is rejecting our request, don't retry (default: do retry)
174        --lang=[s]              set language of xmltv output data (default $lang)
175
176        --shepherd              set if being called from the shepherd script
177        --region=N              set region for where to collect data from (default: $region)
178        --channels_file=file    where to get channel data from (if not set manually)
179        --timezone=HHMM         timezone for channel data (default: $opt_timezone)
180EOF
181;
182
183        exit(0);
184}
185
186######################################################################################################
187# populate cache
188
189sub read_cache
190{
191        if (-r $opt_cache_file) {
192                local (@ARGV, $/) = ($opt_cache_file);
193                no warnings 'all'; eval <>; die "$@" if $@;
194        } else {
195                printf STDERR "WARNING: no programme cache $opt_cache_file - have to fetch all details\n";
196
197                # try to write to it - if directory doesn't exist this will then cause an error
198                &write_cache;
199        }
200}
201
202######################################################################################################
203# write out updated cache
204
205sub write_cache
206{
207        if (!(open(F,">$opt_cache_file"))) {
208                printf STDERR "WARNING: could not write cache file $opt_cache_file: $!\n";
209                printf STDERR "Please fix this in order to reduce the number of queries for data!\n";
210                sleep 10;
211        } else {
212                # cleanup old entries from cache
213                for my $cache_key (keys %{$data_cache}) {
214                        my ($starttime, $endtime, $channel, $progname) = split(/,/,$cache_key);
215                        if ($starttime < (time-86400)) {
216                                delete $data_cache->{$cache_key};
217                                $stats{removed_items_from_cache}++;
218                        }
219                }
220                print F Data::Dumper->Dump([$data_cache], ["data_cache"]);
221                close F;
222        }
223}
224
225######################################################################################################
226
227sub add_cached_data
228{
229        my ($channel,$starttime,$cache_key) = @_;
230        $tv_guide->{$channel}->{data}->{$starttime}->{'sub-title'} =    [[ $data_cache->{$cache_key}->{subtitle}, $lang ]] if $data_cache->{$cache_key}->{subtitle};
231        $tv_guide->{$channel}->{data}->{$starttime}->{'desc'} =         [[ $data_cache->{$cache_key}->{desc}, $lang ]] if $data_cache->{$cache_key}->{desc};
232        $tv_guide->{$channel}->{data}->{$starttime}->{'category'} =     [[ $data_cache->{$cache_key}->{genre}, $lang ]] if $data_cache->{$cache_key}->{genre};
233}
234
235######################################################################################################
236
237sub get_abc_data
238{
239        my ($starttime,$endtime,$channel,$xmlid,$urlbase) = @_;
240        my $try_to_add_abc_detail;
241        my $unprocessed_programmes = 0;
242        my $stop_fetching = 0;
243        my @unprocessed_progname, my @unprocessed_starttime, my @unprocessed_url;
244        my $daynum = 0;
245
246        for (my $currtime = $starttime; $currtime < $endtime; $currtime += 86400) {
247                $daynum++;
248                # for abc portal data, treat a faulure as a hint that there is no further data.
249                # sometimes they have as much as 30 days of data ahead.  sometimes much less...
250                if ($stop_fetching == 0) {
251                        my @timeattr = localtime($currtime); # 0=sec,1=min,2=hour,3=day,4=month,5=year,6=wday,7=yday,8=isdst
252
253                        my $url = sprintf "%s/%s.htm",$urlbase,(strftime "%Y%m/%Y%m%d",localtime($currtime));
254                        my $status = sprintf "%s summary data: day %d of %d", $xmlid, ((($currtime-$starttime)/86400)+1),(($endtime-$starttime)/86400);
255                        my $data = &get_url($url,$status,($daynum < 8 ? 5 : 2));
256                        my $seen_programmes = 0;
257
258                        if ($data) {   
259                                my $tree = HTML::TreeBuilder->new_from_content($data);
260                                for ($tree->look_down('_tag' => 'div', 'class' => 'scheduleDiv')) {
261                                        foreach my $tree_tr ($_->look_down('_tag' => 'tr')) {
262                                                if (my $tree_row = $tree_tr->look_down('_tag' => 'th', 'scope' => 'row')) {
263                                                        if ($tree_row->as_text() =~ /^(\d+):(\d+)(.)m/) {
264                                                                $timeattr[2] = $1; # hour
265                                                                $timeattr[2] += 12 if ($3 eq "p"); # pm
266                                                                $timeattr[1] = $2; # min
267                                                                my $found_time = mktime(@timeattr);
268       
269                                                                if ($tree_tr->look_down('_tag' => 'td')) {
270                                                                        if ($_ = $tree_tr->look_down('_tag' => 'a')) {
271                                                                                my $programme = $_->as_text();
272                                                                                my $progurl = $_->attr('href');
273               
274                                                                                if ($progurl =~ /^\/tv\/guide\//) {
275                                                                                        $unprocessed_progname[$unprocessed_programmes] = $programme;
276                                                                                        $unprocessed_starttime[$unprocessed_programmes] = $found_time;
277                                                                                        $unprocessed_url[$unprocessed_programmes] = "http://www.abc.net.au".$progurl;
278                                                                                        $unprocessed_programmes++;
279                                                                                        $seen_programmes++;
280                                                                                }
281                                                                        }
282                                                                }
283                                                        }
284                                                }
285                                        }
286                                }
287                        }
288       
289                        if ($seen_programmes == 0) {
290                                $stop_fetching = 1;
291                        } else {
292                                $stats{abc_daily_pages}++;
293                        }
294                }
295        }
296
297        # have 'n' days of this channel unprocessed - process it!
298        for (my $i = 0; $i < ($unprocessed_programmes-1); $i++) {
299                $stats{programmes}++;
300                my $starttime = $unprocessed_starttime[$i];
301                my $endtime = $unprocessed_starttime[$i+1];
302                my $cache_key = sprintf "%d,%d,%s,%s", $starttime, $endtime, $xmlid, $unprocessed_progname[$i];
303
304                $tv_guide->{$channel}->{data}->{$starttime}->{'channel'} =      $xmlid;
305                $tv_guide->{$channel}->{data}->{$starttime}->{'start'} =        strftime "%Y%m%d%H%M %z", localtime($starttime);
306                $tv_guide->{$channel}->{data}->{$starttime}->{'stop'} =         strftime "%Y%m%d%H%M %z", localtime($endtime);
307                $tv_guide->{$channel}->{data}->{$starttime}->{'title'} =        [[ $unprocessed_progname[$i], $lang ]];
308
309                if ($data_cache->{$cache_key}) {
310                        $stats{used_cached_data}++;
311                        &add_cached_data($channel,$starttime,$cache_key);
312                } else {
313                        &get_one_abc_event($channel, $cache_key, $unprocessed_url[$i], "$xmlid detail pages ($i of $unprocessed_programmes)") if (!$opt_cheap);
314                }
315        }
316}
317
318######################################################################################################
319
320sub get_one_abc_event
321{
322        my ($channel, $cache_key, $url, $status) = @_;
323        my $seen_programme = 0;
324        my ($starttime, $endtime, $xmlid, $progname) = split(/,/,$cache_key);
325
326        do {
327                my $data = &get_url($url,$status,3);
328
329                my $tree = HTML::TreeBuilder->new_from_content($data);
330                if (my $inner_tree = $tree->look_down('_tag' => 'div', 'class' => 'column2')) {
331                        my $event_title = undef, my $event_subtitle = undef, my $event_description = undef, my $event_genre = undef;
332
333                        if (my $prog_h2 = $inner_tree->look_down('_tag' => 'h2')) {
334                                my $full_title = $prog_h2->as_HTML();
335                                ($event_title,$event_subtitle) = split(/<br>/,$full_title);
336
337                                $event_title =~ s/(<[a-zA-Z0-9]+\>)//g; # remove html tags
338                                $event_title =~ s/(^\n|\n$)//g;         # strip trailing/leading blank lines
339
340                                if ($event_subtitle) {
341                                    $event_subtitle =~ s/(<[\/a-zA-Z0-9]+\>)//g;        # remove html tags
342                                    $event_subtitle =~ s/(^\n|\n$)//g;          # strip trailing/leading blank lines
343                                }
344                        }
345                       
346                        my $paranum = 0;
347                        foreach my $para ($inner_tree->look_down('_tag' => 'p')) {
348                                $paranum++;
349
350                                if (($paranum > 1) && (!($para->as_text() =~ /^Go to website/)) && (!($para->as_text() =~ /^Send to a Friend/))) {
351                                        $event_description .= $para->as_text() . "\n";
352
353                                        if (my $try_genre = $para->look_down('_tag' => 'a')) {
354                                                $event_genre = $try_genre->as_text();
355                                        }
356                                }
357                        }
358                        $stats{portal_detail_pages}++;
359                        $seen_programme++;
360
361                        $data_cache->{$cache_key}->{subtitle} = $event_subtitle if $event_subtitle;
362                        $data_cache->{$cache_key}->{desc} = $event_description if $event_description;
363                        $data_cache->{$cache_key}->{genre} = $event_genre if $event_genre;
364                        &add_cached_data($channel,$starttime,$cache_key);
365                }
366                if ($seen_programme == 0) {
367                        printf STDERR "WARNING: failed to parse any programme data from '%s' - blocked/rate-limited/format-changed?\n",$url;
368                        $stats{failed_to_parse_portal_detail_page}++;
369                }
370        } until (($seen_programme> 0) || ($opt_dont_retry>0));
371}
372
373######################################################################################################
374# logic to fetch a page via http
375#  retries up to 3 times to get a page with 5 second pauses inbetween
376
377sub get_url
378{
379        my ($url,$status,$retrycount) = @_;
380        my $response;
381        my $attempts = 0;
382        my ($raw, $page, $base);
383
384        $retrycount = 5 if ($retrycount == 0);
385        $url =~ s#^http://#http://webwarper.net/ww/# if $opt_warper;
386        my $request = HTTP::Request->new(GET => $url);
387        $request->header('Accept-Encoding' => 'gzip');
388
389        if ($opt_obfuscate) {
390                my $randomaddr = sprintf "203.%d.%d.%d",rand(255),rand(255),(rand(254)+1);
391                $request->header('Via' => '1.0 proxy:81 (Squid/2.3.STABLE3)');
392                $request->header('X-Forwarded-For' => $randomaddr);
393        }
394        &log(sprintf "fetching %s%s: %s",$status,($opt_obfuscate ? "[obfuscate]" : ""),$url);
395        for (1..$retrycount) {
396                $response = $ua->request($request);
397                last if $response->is_success;
398
399                $stats{http_failed_requests}++;
400                $stats{slept_for} += 10;
401                $attempts++;
402                sleep 10;
403        }
404        if (!($response->is_success)) {
405                &log("aborting after $attempts attempts to fetch url $url") if ($debug && $retrycount > 3);
406                return undef;
407        }
408
409        $stats{bytes_fetched} += do {use bytes; length($response->content)};
410        $stats{http_successful_requests}++;
411
412        if (!$opt_fast) {
413                my $sleeptimer = int(rand(5)) + 1;  # sleep anywhere from 1 to 5 seconds
414                $stats{slept_for} += $sleeptimer;
415                sleep $sleeptimer;
416        }
417
418        if ($response->header('Content-Encoding') &&
419            $response->header('Content-Encoding') eq 'gzip') {
420                $stats{compressed_pages} += do {use bytes; length($response->content)};
421                $response->content(Compress::Zlib::memGunzip($response->content));
422        }
423        return $response->content;
424}
425
426######################################################################################################
427
428sub log
429{
430        my ($entry) = @_;
431        printf STDERR "%s [%d] %s\n",$progname,time,$entry;
432}
433
434######################################################################################################
435
436sub print_stats
437{
438        printf STDERR "%s v%s [%d] completed in %0.2f seconds",$progname,$version,time,tv_interval($script_start_time);
439        foreach my $key (sort keys %stats) {
440                printf STDERR ", %d %s",$stats{$key},$key;
441        }
442        printf STDERR "\n";
443}
444
445######################################################################################################
446# descend a structure and clean up various things, including stripping
447# leading/trailing spaces in strings, translations of html stuff etc
448#   -- taken & modified from Michael 'Immir' Smith's excellent tv_grab_au
449
450my %amp;
451BEGIN { %amp = ( nbsp => ' ', qw{ amp & lt < gt > apos ' quot " } ) }
452
453sub cleanup {
454        my $x = shift;
455        if    (ref $x eq "REF")   { cleanup($_) }
456        elsif (ref $x eq "HASH")  { cleanup(\$_) for values %$x }
457        elsif (ref $x eq "ARRAY") { cleanup(\$_) for @$x }
458        elsif (defined $$x) {
459                $$x =~ s/&(#(\d+)|(.*?));/ $2 ? chr($2) : $amp{$3}||' ' /eg;
460                # $$x =~ s/[^\x20-\x7f]/ /g;
461                $$x =~ s/(^\s+|\s+$)//g;
462        }
463}
464
465######################################################################################################
466
467sub write_data
468{
469        my %writer_args = ( encoding => 'ISO-8859-1' );
470        if ($opt_outputfile) {
471                my $fh = new IO::File(">$opt_outputfile")  or die "can't open $opt_outputfile: $!";
472                $writer_args{OUTPUT} = $fh;
473        }
474
475        my $writer = new XMLTV::Writer(%writer_args);
476
477        $writer->start
478          ( { 'source-info-url'    => "about:blank",
479              'source-info-name'   => "$progname $version",
480              'generator-info-name' => "$progname $version"} );
481
482        for my $channel (sort keys %{$channels}) {
483                $writer->write_channel( {
484                        'display-name' => [[ $channel, $lang ]],
485                        'id' => $channels->{$channel}
486                        } );
487        }
488
489        for my $channel (sort keys %{$channels}) {
490                for my $event_id (sort {$a <=> $b} keys %{($tv_guide->{$channel}->{data})}) {
491                        my $show = $tv_guide->{$channel}->{data}->{$event_id};
492                        &cleanup($show);
493                        $writer->write_programme($show);
494                }
495        }
496
497        $writer->end();
498}
499
500######################################################################################################
Note: See TracBrowser for help on using the browser.