root/trunk/grabbers/citysearch @ 1061

Revision 1061, 17.0 kB (checked in by paul, 5 years ago)

regions 90 and 98 change and new regions 266,267,268
remove Prime (Regional Victoria),Prime (Albury) and Prime Canberra/Sth Coast
add TEN HD to region 74 darwin
other cleanups

  • Property svn:executable set to *
Line 
1#!/usr/bin/perl
2#
3# citysearch TV guide grabber
4#
5
6my $version = '3.0.1';
7
8use strict;
9use Getopt::Long;
10use POSIX;
11use Data::Dumper;
12use IO::File;
13use XMLTV;
14use HTML::TreeBuilder;
15use Shepherd::Common;
16
17# ---------------------------------------------------------------------------
18# --- Global Variables
19
20my $progname = "citysearch";
21
22my $DATASOURCE = "citysearch.com.au";
23
24my $lang = 'en';
25my $debug = 0;
26my $channels;
27my $opt_channels;
28my $opt = { };
29my $gaps;
30my %stats;
31my $shows;
32my $cache;
33my $runtime = time;
34my $zerohr;
35my @skipped_channels;
36
37# ---------------------------------------------------------------------------
38# --- Setup
39
40print "$progname $version\n";
41
42$| = 1;
43
44&get_command_line_options;
45
46exit 0 if ($opt->{version});
47
48&help if ($opt->{help});
49
50&set_defaults;
51
52&read_channels_file;
53
54unless ($channels)
55{
56    print "ERROR: No channels requested. Please use --channels_file.\n";
57    exit 33;
58}
59
60&read_gaps_file;
61
62&read_cache;
63
64&set_region;
65
66&get_guide_data;
67
68&calculate_stop_times;
69
70&details;
71
72&write_cache;
73
74&write_xml;
75
76&Shepherd::Common::print_stats($progname, $version, $runtime, %stats);
77
78&log("Done.");
79exit;
80
81
82# ---------------------------------------------------------------------------
83# --- Subs
84
85sub get_guide_data
86{
87    &log("Grabbing data for days " . $opt->{offset} .
88         " - " . ($opt->{days} - 1) .
89         ($opt->{output} ? " into " . $opt->{output} : '') .
90         ".");
91
92    # Calculate midnight on day zero in epoch time
93    my @today = localtime($runtime); # 0=sec,1=min,2=hour,3=day,4=month,5=year,6=wday,7=yday,8=isdst
94    $zerohr = $runtime - (($today[0]) + ($today[1]*60) + ($today[2]*60*60));
95
96    for my $day ($opt->{offset} .. ($opt->{days} - 1))
97    { 
98        my $dow = &POSIX::strftime("%A", localtime($runtime + ($day * 86400)));
99
100        &log("Day $day ($dow)");
101
102        my $start_hr = 0;
103        if (!$day)
104        {
105            $start_hr = int($today[2] / 3) * 3;
106        }
107        for (my $hr = $start_hr; $hr < 24; $hr += 3)
108        {
109
110            &log("Time window $hr:00 - " . ($hr+3) . ":00");
111
112            my $url = "$DATASOURCE/tvguide/$day/$hr:00";
113            my $guidedata = &Shepherd::Common::get_url($url);
114            exit 11 unless ($guidedata);
115
116            # Verify that the guide page really is for the day we want.
117            my $daystr = &POSIX::strftime("%A, %d %B", localtime($runtime + ($day * 86400)));
118            $daystr =~ s/, 0(\d)/, $1/;
119
120            unless ($guidedata =~ /<p class="date">$daystr<\/p>/)
121            {
122                &log("Exiting: couldn't locate daystring \"$daystr\" in guide page for $dow.");
123                exit 21;
124            }
125
126            &parse_guide($guidedata, $day, $hr);
127        }
128    }
129    &log("Found " . &num_items($shows) . " shows on " . scalar(keys %$shows) . " channels.");
130}
131
132sub parse_guide
133{
134    my ($guidedata, $day, $window) = @_;
135
136    &log("Parsing guide page (Day $day hr $window).") if ($debug);
137    my $tree = HTML::TreeBuilder->new;
138    $tree->no_space_compacting(1);
139    $tree->parse($guidedata);
140    $tree->eof;
141
142    foreach my $table ($tree->look_down(_tag => 'table', id => 'tvGuideTable'))
143    {
144        &log("Found table.") if ($debug);
145        foreach my $tr ($table->look_down(_tag => 'tr'))
146        {
147            my $ctag = $tr->look_down(_tag => 'td', class => 'channel');
148            next unless ($ctag);
149
150            my $channame = $ctag->as_text();
151            if ($ctag->as_HTML =~ /<span .*?>.*?<\/span>.*?<span .*?>(.*?)<\/span>/s)
152            {
153                $channame = $1;
154            }
155            $channame =~ s/^ //g;
156            $channame =~ s/ $//g;
157            $channame =~ s/\n//g;
158#           $channame = 'Prime Canberra/Sth Coast' if ($channame eq 'Prime' and $opt->{region} == 126);
159
160            my $chanid = $channels->{$channame};
161            unless ($chanid)
162            {
163                unless (grep $_ eq $channame, @skipped_channels)
164                {
165                    &log("Skipping unsubscribed channel \"$channame\".");
166                    push @skipped_channels, $channame;
167                }
168                next;
169            }
170
171            &log("Channel $channame") if ($debug);
172           
173            # When we hit a "Continue Before" block, it means we're missing
174            # a show's start time. Skip next show in this case.
175            my $continue_before = 0;
176
177            foreach my $td ($tr->look_down(_tag => 'td'))
178            {
179                my $td_class = $td->attr('class');
180                next unless ($td_class and $td_class =~ /(\d\d)(\d\d)/);
181                my $block_start = ($1 * 3600) + ($2 * 60); 
182
183                $continue_before = 1 if ($td_class =~ /continueBefore/);
184
185                foreach my $showblock ($td->look_down(_tag => 'div', class => 'programWrapper'))
186                {
187
188                    if ($continue_before)
189                    {
190                        $continue_before = 0;
191                        next;
192                    }
193
194                    my $show;
195
196                    $show->{channel} = $chanid;
197
198                    my $start = $block_start;
199                    if ($showblock->as_HTML =~ /<span class="oddStartTime">(\d+)\.(\d+)<\/span> ([ap]m)/)
200                    {
201                        $start = ($1 * 3600) + ($2 * 60);
202                        $start += (12 * 3600) if ($3 eq 'pm');
203                        $start -= (12 * 3600) if ($3 eq 'am' and $1 == 12);
204                    }
205                    $show->{start} = $zerohr + (86400 * $day) + $start;
206
207                    my $atag = $showblock->look_down(_tag => 'a');
208                    unless ($atag)
209                    {
210                        # Caused by "No information available" entries
211                        &log("Empty show block: day $day hr $window chan $channame") if ($debug);
212                        next;
213                    }
214                    $show->{title} = $atag->as_text();
215
216                    die "Missing pid: day $day hr $window chan $channame title $show->{title}" unless ($atag->attr('href') =~ m"/tv/viewTvProgram/tvReviews-(.*)");
217                    $show->{pid} = $1;
218
219                    #if ($showblock->as_HTML =~ /<span class="accessLink">(\w+)<\/span>/)
220                    #{
221                    #   push @{$show->{category}}, $1;
222                    #}
223
224                    if ($showblock->as_HTML =~ /Rpt/)
225                    {
226                        $show->{'previously-shown'} = { };
227                    }
228                    &log("- $show->{title}") if ($debug);
229                    $shows->{$chanid}->{$show->{start}} = $show;
230                }
231            }
232        }
233    }
234    $tree->delete;
235}
236
237sub details
238{
239    # iterate through our list, compare to cache, lookup if necessary
240    my $count = 0;
241    my $num_shows = &num_items($shows);
242    foreach my $ch (keys %$shows)
243    {
244        foreach my $s (sort keys %{$shows->{$ch}})
245        {
246            my $show = $shows->{$ch}->{$s};
247            if ($show->{start} > $zerohr + (86400 * $opt->{days}))
248            {
249                &log("Late  : " . $show->{title}) if ($debug);
250                delete $shows->{$ch}->{$s};
251            }
252            elsif ($show->{stop} and $show->{stop} < $zerohr + (86400 * $opt->{offset}))
253            {
254                &log("Early : " . $show->{title}) if ($debug);
255                delete $shows->{$ch}->{$s};
256            }
257            elsif ($gaps and &is_outside_gaps($show->{channel}, $show->{start}, $show->{stop}))
258            {
259                &log("Nongap: " . $show->{title}) if ($debug);
260                delete $shows->{$ch}->{$s};
261            }
262            elsif (
263                $cache 
264                    and 
265                $cache->{$ch} 
266                    and 
267                $cache->{$ch}->{$s}
268                    and
269                $cache->{$ch}->{$s}->{details}
270                    and
271                $cache->{$ch}->{$s}->{stop} eq $show->{stop}
272                    and
273                $cache->{$ch}->{$s}->{title} eq $show->{title})
274            {
275                &log("Cached: ". $show->{title}) if ($debug);
276                $shows->{$ch}->{$s} = $cache->{$ch}->{$s};
277                $stats{cache_hits}++;
278                $stats{shows}++;
279            }
280            else
281            {
282                &log("New   : " . $show->{title}) if ($debug);
283                my $html = &fetch_details($show->{pid});
284                if ($html)
285                {
286                    &parse_details($html, $show);
287                    $show->{details} = 1;
288                    $cache->{$ch}->{$s} = $show;
289                    $stats{shows}++;
290                }
291                else
292                {
293                    &log("Couldn't fetch " . $show->{title} .
294                         " (pid " . $show->{pid} . ")!");
295                }
296            }
297            $count++;
298            if ($count % 25 == 0)
299            {
300                &log(sprintf " ...processed %d of %d shows [%s elapsed, %d new, %d cached, %d unwanted]",
301                    $count, $num_shows, 
302                    &Shepherd::Common::pretty_duration(time - $runtime),
303                    $stats{shows} - $stats{cache_hits},
304                    $stats{cache_hits},
305                    $count - $stats{shows});
306            }
307        }
308    }
309}
310
311sub fetch_details
312{
313    my $pid = shift;
314
315    my $url = "$DATASOURCE/tv/viewTvProgram/tvReviews-$pid";
316    my $html = &Shepherd::Common::get_url($url);
317    return $html;
318}
319
320sub parse_details
321{
322    my ($html, $show) = @_;
323
324    &log("Parsing \"$show->{title}\"") if ($debug);
325    my $tree = HTML::TreeBuilder->new_from_content($html);
326
327    my $desc = $tree->look_down(_tag => 'p', class => 'teaser');
328    unless (defined $desc)
329    {
330        print "Dumping bad HTML:\n$html\n" if ($debug);
331        print "Can't parse details page! Title: $show->{title}\n";
332        exit 22;
333    }
334
335    if ($desc)
336    {
337        $desc = &strip_whitespace($desc->as_text);
338        $show->{desc} = $desc if ($desc);
339    }
340
341    my $div = $tree->look_down(_tag => 'div', class => 'contentDetails');
342    my (%video, $category, %type);
343    foreach my $tr ($div->look_down(_tag => 'tr'))
344    {
345        if ($tr->as_text =~ /(.*?):(.*)/)
346        {
347            if ($1 eq 'Type')
348            {
349                $category = &strip_whitespace($2);
350            }
351            elsif ($1 eq 'Country')
352            {
353                $show->{country} = $2;
354            }
355            elsif ($1 eq 'Language')
356            {
357                $show->{language} = $2;
358            }
359            elsif ($1 eq 'Cast')
360            {
361                foreach (split /, /, $2)
362                {
363                    push @{$show->{credits}{actor}}, &strip_whitespace($_);
364                }
365            }
366            elsif ($1 eq 'Director')
367            {
368                foreach (split /, /, $2)
369                {
370                    push @{$show->{credits}{director}}, &strip_whitespace($_);
371                }
372            }
373            elsif ($1 eq 'Writer') # unseen
374            {
375                foreach (split /, /, $2)
376                {
377                    push @{$show->{credits}{writer}}, &strip_whitespace($_);
378                }
379            }
380            elsif ($1 eq 'Duration')
381            {
382                if ($2 =~ /(\d+) min/)
383                {
384                    $show->{length} = $1 * 60;
385                    if (!$show->{stop}) {
386                        $show->{stop} = $show->{start} + ($1 * 60);
387                        &log("Filled in stop time! $1 minutes.") if ($debug);
388                    }
389                }
390            }
391            elsif ($1 eq 'Format')
392            {
393                foreach my $info (split /, /, $2)
394                {
395                    $info = &strip_whitespace($info);
396                    if ($info eq 'Closed Captions')
397                    {
398                        push @{$show->{'subtitles'}}, 'teletext';
399                    }
400                    elsif ($info eq 'Subtitles')
401                    {
402                        push @{$show->{'subtitles'}}, 'onscreen';
403                    }
404                    elsif ($info eq 'Widescreen')
405                    {
406                        $video{aspect} = '16:9';
407                    }
408                    elsif ($info eq 'High Definition')
409                    {
410                        $video{'quality'} = 'HDTV';
411                    }
412                    elsif ($info eq 'Premiere')
413                    {
414                        $show->{'premiere'} = [ $info ];
415                        $type{premiere} = 1;
416                    }
417                    elsif ($info eq 'Live')
418                    {
419                        $type{live} = 1;
420                    }
421                    elsif ($info eq 'Final' || $info eq 'Finale') # unseen
422                    {
423                        $type{final} = 1;
424                    }
425                    elsif ($info eq 'Return') # unseen
426                    {
427                        $type{return} = 1;
428                    }
429                    elsif ($info eq 'Repeat')
430                    {
431                        $show->{'previously-shown'} = { };
432                    }
433                    elsif ($info eq 'Movie')
434                    {
435                        $type{movie} = 1;
436                    }
437                    else
438                    {
439                        &log("Unknown info field: \"$info\"");
440                    }
441                }
442            }
443            elsif ($1 eq 'Rating')
444            {
445                $show->{rating} = $2;
446            }
447            elsif ($1 eq 'Year')
448            {
449                $show->{date} = $2;
450            }
451            elsif ($1 eq 'Channel' or $1 eq 'Time')
452            {
453                # ignore: handled elsewhere
454            }
455            else
456            {
457                &log("Ignoring $1: $2") if ($debug);
458            }
459        }
460        else
461        {
462            &log("Unknown field: " .$tr->as_text);
463        }
464    }
465    $show->{video} = { %video } if (%video);
466    $show->{category} = [ &Shepherd::Common::generate_category(
467        $show->{title}, $category, %type) ];
468
469    $tree->delete;
470
471    print "Parsed: " . Dumper($show) if ($debug);
472}
473
474sub calculate_stop_times
475{
476    foreach my $ch (keys %$shows)
477    {
478        my $last_start_time;
479        foreach my $s (reverse sort keys %{$shows->{$ch}})
480        {
481            $shows->{$ch}->{$s}->{stop} = $last_start_time if ($last_start_time);
482            $last_start_time = $shows->{$ch}->{$s}->{start};
483        }
484    }
485}
486
487sub write_xml
488{
489    my %writer_args = ( encoding => 'ISO-8859-1' );
490
491    &log("Writing " . &num_items($shows) . " shows to XML.");
492
493    if ($opt->{output})
494    {
495        my $fh = new IO::File(">" . $opt->{output})
496            or die "Can't open " . $opt->{output} . ": $!";
497        $writer_args{OUTPUT} = $fh;
498    }
499
500    my $writer = new XMLTV::Writer(%writer_args);
501
502    $writer->start
503        ( { 'source-info-url'    => $DATASOURCE,
504            'source-info-name'   => "Citysearch",
505            'generator-info-name' => "$progname $version"} );
506
507    for my $channel (sort keys %$channels)
508    {
509        $writer->write_channel( { 
510                'display-name' => [ [ $channel, $lang ] ],
511                'id' => $channels->{$channel} } );
512    }
513
514    foreach my $ch (sort keys %$shows)
515    {
516        foreach my $s (sort keys %{$shows->{$ch}})
517        {
518            # Don't return shows with no stop time
519            next unless ($shows->{$ch}->{$s}->{stop});
520
521            # Format for XMLTV-compliance
522            my %p = %{$shows->{$ch}->{$s}};
523            foreach my $field ('title', 'sub-title', 'desc', 'country')
524            {
525                $p{$field} = [[ $p{$field}, $lang ]] if ($p{$field});
526            }
527            $p{language} = [ $p{language}, $lang ] if ($p{language});
528            $p{start} = &POSIX::strftime("%Y%m%d%H%M", localtime($p{start}));
529            $p{stop} = &POSIX::strftime("%Y%m%d%H%M", localtime($p{stop}));
530            $p{rating} = [[ $p{rating}, 'ABA', undef ]] if ($p{rating});
531            if ($p{category} && ref($p{category}) eq "ARRAY"
532                    && $p{category}[0] && ref($p{category}[0]) ne "ARRAY") # obsolete 14/10/2007
533            {
534                foreach (@{$p{category}})
535                {
536                    $_ = [ &Shepherd::Common::translate_category($_), $lang ];
537                }
538            }
539            if ($p{subtitles})
540            {
541                my @s;
542                foreach (@{$p{subtitles}})
543                {
544                    push @s, { type => $_ };
545                }
546                $p{subtitles} = [ @s ];
547            }
548            delete $p{pid};
549            delete $p{details};
550
551            &log("-> " . $shows->{$ch}->{$s}->{title}) if ($debug);
552#           print Dumper(\%p);
553            $shows->{$ch}->{$s}->{start} = &POSIX::strftime("%Y%m%d%H%M", localtime($s));
554            $writer->write_programme(\%p);
555        }
556    }
557
558    $writer->end();
559}
560
561# ---------------------------------------------------------------------
562# Helper subs
563
564sub num_items
565{
566    my $hash = shift;
567    my $count = 0;
568    foreach my $ch (keys %$hash)
569    {
570        $count += scalar keys %{$hash->{$ch}};
571    }
572    return $count;
573}
574
575sub is_outside_gaps
576{
577    my ($ch, $start, $stop) = @_;
578
579    foreach my $gap (@{$gaps->{$ch}})
580    {
581        if ($gap =~ /(\d+)-(\d+)/)
582        {
583            return 0 if ($stop > $1 and $start < $2);
584        }
585    }
586    return 1;
587}
588
589sub strip_whitespace 
590{
591    $_[0] =~ /^\s*(.*?)\s*$/ ? $1 : $_[0];
592}
593
594# ---------------------------------------------------------------------
595# Setup subs
596
597
598sub read_cache
599{
600    $cache = Shepherd::Common::read_cache(\$opt->{'cache-file'});
601    if ($cache)
602    {
603        &log("Retrieved " . &num_items($cache) . " cached items from file.");
604        &clean_cache;
605    }
606    else
607    {
608        $cache = { };
609        &log("Not using cache.");
610    }
611    if ($opt->{'dump-cache'})
612    {
613        &log("Dumping cache.");
614        print Dumper($cache);
615        exit 0;
616    }
617}
618
619sub clean_cache
620{
621    my $cutoff = $runtime - 86400;   
622    &log("Removing cached shows that finish earlier than " . localtime($cutoff) . ".") if ($debug);
623    my $count = 0;
624
625    foreach my $ch (keys %$cache)
626    {
627        foreach my $s (keys %{$cache->{$ch}})
628        {
629            if ($cache->{$ch}->{$s}->{stop} < $cutoff)
630            {
631                &log("Removing $cache->{$ch}->{$s}->{title}.") if ($debug);
632                delete $cache->{$ch}->{$s};
633                $count++;
634            }
635        }
636    }
637    &log("Removed $count stale items from cache.") if ($count);
638}
639
640sub write_cache
641{
642    my $n = &num_items($cache);
643    return unless ($n);
644    &log("Writing $n shows to cache.");
645    Shepherd::Common::write_cache($opt->{'cache-file'}, $cache);
646}
647
648sub set_region
649{
650    my %regions = ( 81 => 'adelaide', 75 => 'brisbane', 126 => 'canberra',
651                    74 => 'darwin', 88 => 'hobart', 94 => 'melbourne', 
652                    101 => 'perth', 73 => 'sydney' );
653    unless ($regions{$opt->{region}})
654    {
655        &log("ERROR: unsupported region \"$opt->{region}\".");
656        exit 32;
657    }
658    $opt->{rname} = $regions{$opt->{region}};
659    $DATASOURCE = "http://$opt->{rname}.$DATASOURCE";
660    &log("Datasource: $DATASOURCE") if ($debug);
661}
662
663
664sub get_command_line_options
665{
666    &Getopt::Long::Configure('pass_through');
667    &GetOptions($opt, qw(
668                            help
669                            debug
670                            output=s
671                            days=i
672                            offset=i
673                            region=i
674                            dump-cache
675                            cache-file=s
676                            channels_file=s
677                            gaps_file=s
678                            version
679                            warper
680                        ));
681    $debug = $opt->{debug};
682
683    if (@ARGV)
684    {
685        &log("\nUnknown option(s): @ARGV\n");
686    }
687}
688
689sub set_defaults
690{
691    my $defaults = {
692        'days' => 7,
693        'offset' => 0,
694        'region' => 94,
695        'output' => &getcwd . '/output.xmltv',
696        'cache-file' => &getcwd . '/' . $progname . '.cache',
697        'channels_file' => &getcwd . '/channels.conf'
698    };
699
700    foreach (keys %$defaults)
701    {
702        unless (defined $opt->{$_})
703        {
704            $opt->{$_} = $defaults->{$_};
705        }
706    }
707
708    $opt->{'days'} = 7 if ($opt->{'days'} > 7);
709
710    &Shepherd::Common::set_defaults(
711        stats => \%stats,
712        delay => "1-5",
713        debug => $debug,
714        webwarper => $opt->{warper}
715        );
716
717    # Initialize stats
718    %stats = ( );
719    foreach (qw( cache_hits shows ))
720    {
721        $stats{$_} = 0;
722    }
723}
724
725sub read_channels_file 
726{
727    &read_config_file('channels', 'channels_file');
728}
729
730sub read_gaps_file
731{
732    &read_config_file('gaps', 'gaps_file');
733    if ($gaps)
734    {
735        foreach (keys %$gaps)
736        {
737            $gaps->{$channels->{$_}} = $gaps->{$_};
738            delete $gaps->{$_};
739        }
740    }
741}
742
743sub read_config_file
744{
745    my ($name, $arg) = @_;
746
747    return unless ($opt->{$arg});
748    &log("Reading $name file: $opt->{$arg}");
749    if (-r $opt->{$arg})
750    {
751        local (@ARGV, $/) = ($opt->{$arg});
752        no warnings 'all';
753        eval <>;
754        die "Can't parse $name file: $@" if ($@);
755    }
756    else
757    {
758        &log("Unable to read $name file.");
759    }
760}
761
762sub log
763{
764    &Shepherd::Common::log(@_);
765}
766
767sub help
768{
769    print q{
770Command-line options:
771  --help                 Print this message
772  --version              Show current version
773
774  --output <file>        Write XML into the specified file
775  --channels_file <file> Read channel subscriptions from file
776
777  --region <n>           Grab data for region code <n>
778  --days <n>             Grab <n> days of data (today being day 1)
779  --offset <n>           Skip the first <n> days
780
781  --debug                Print lots of debugging output
782};
783    exit 0;
784}
Note: See TracBrowser for help on using the browser.