root/trunk/grabbers/citysearch @ 947

Revision 947, 16.9 kB (checked in by paul, 6 years ago)

Common::generate_category: if no category then guess from title for Sport, News, Infomercial; translates the words in category; types (final,premiere,return,live) are prepend to category; types (movie,sports,series,tvshow) are appended to category list
Common::subrating: allow undef param and detect Medical
yahoo7widget: use new Common::generate_category, add 'return' (unseen), add 'writer' (unseen)
rex: use new Common::generate_category, don't add 'advisory', add 'length', add 'final' and 'return' (unseen)
citysearch: use new Common::generate_category, add 'language', 'director' and 'writer' (unseen), add 'length', add 'final' (unseen) and 'return' (unseen), handle old cache entries
southerncross_website: use new Common::generate_category, add 'repeat', add 'Guests Include', cut 'Starring:', drop $post_desc, improve category_from_title,

program mythtv has no colours for long categories, needs patch: libs/libmyth/uitypes.cpp:849 data->categoryColor = categoryColors[data->category];

  • Property svn:executable set to *
Line 
1#!/usr/bin/perl
2#
3# citysearch TV guide grabber
4#
5
6my $version = '2.0.3';
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 2;
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 5 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 =~ /$daystr/)
121            {
122                &log("Exiting: couldn't locate daystring \"$daystr\" in guide page for $dow.");
123                exit 8;
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 $div = $tree->look_down(_tag => 'div', id => 'contentHeader');
328    die "Can't read page!\n$html" unless ($div);
329
330    my $desc = $div->look_down(_tag => 'p');
331    if ($desc)
332    {
333        $desc = &strip_whitespace($desc->as_text);
334        $show->{desc} = $desc if ($desc);
335    }
336
337    $div = $tree->look_down(_tag => 'div', class => 'contentDetails');
338    my (%video, $category, %type);
339    foreach my $tr ($div->look_down(_tag => 'tr'))
340    {
341        if ($tr->as_text =~ /(.*?):(.*)/)
342        {
343            if ($1 eq 'Type')
344            {
345                $category = &strip_whitespace($2);
346            }
347            elsif ($1 eq 'Country')
348            {
349                $show->{country} = $2;
350            }
351            elsif ($1 eq 'Language')
352            {
353                $show->{language} = $2;
354            }
355            elsif ($1 eq 'Cast')
356            {
357                foreach (split /, /, $2)
358                {
359                    push @{$show->{credits}{actor}}, &strip_whitespace($_);
360                }
361            }
362            elsif ($1 eq 'Director')
363            {
364                foreach (split /, /, $2)
365                {
366                    push @{$show->{credits}{director}}, &strip_whitespace($_);
367                }
368            }
369            elsif ($1 eq 'Writer') # unseen
370            {
371                foreach (split /, /, $2)
372                {
373                    push @{$show->{credits}{writer}}, &strip_whitespace($_);
374                }
375            }
376            elsif ($1 eq 'Duration')
377            {
378                if ($2 =~ /(\d+) min/)
379                {
380                    $show->{length} = $1 * 60;
381                    if (!$show->{stop}) {
382                        $show->{stop} = $show->{start} + ($1 * 60);
383                        &log("Filled in stop time! $1 minutes.") if ($debug);
384                    }
385                }
386            }
387            elsif ($1 eq 'Format')
388            {
389                foreach my $info (split /, /, $2)
390                {
391                    $info = &strip_whitespace($info);
392                    if ($info eq 'Closed Captions')
393                    {
394                        push @{$show->{'subtitles'}}, 'teletext';
395                    }
396                    elsif ($info eq 'Subtitles')
397                    {
398                        push @{$show->{'subtitles'}}, 'onscreen';
399                    }
400                    elsif ($info eq 'Widescreen')
401                    {
402                        $video{aspect} = '16:9';
403                    }
404                    elsif ($info eq 'High Definition')
405                    {
406                        $video{'quality'} = 'HDTV';
407                    }
408                    elsif ($info eq 'Premiere')
409                    {
410                        $show->{'premiere'} = [ $info ];
411                        $type{premiere} = 1;
412                    }
413                    elsif ($info eq 'Live')
414                    {
415                        $type{live} = 1;
416                    }
417                    elsif ($info eq 'Final' || $info eq 'Finale') # unseen
418                    {
419                        $type{final} = 1;
420                    }
421                    elsif ($info eq 'Return') # unseen
422                    {
423                        $type{return} = 1;
424                    }
425                    elsif ($info eq 'Repeat')
426                    {
427                        $show->{'previously-shown'} = { };
428                    }
429                    elsif ($info eq 'Movie')
430                    {
431                        $type{movie} = 1;
432                    }
433                    else
434                    {
435                        &log("Unknown info field: \"$info\"");
436                    }
437                }
438            }
439            elsif ($1 eq 'Rating')
440            {
441                $show->{rating} = $2;
442            }
443            elsif ($1 eq 'Year')
444            {
445                $show->{date} = $2;
446            }
447            elsif ($1 eq 'Channel' or $1 eq 'Time')
448            {
449                # ignore: handled elsewhere
450            }
451            else
452            {
453                &log("Ignoring $1: $2") if ($debug);
454            }
455        }
456        else
457        {
458            &log("Unknown field: " .$tr->as_text);
459        }
460    }
461    $show->{video} = { %video } if (%video);
462    $show->{category} = [ &Shepherd::Common::generate_category(
463        $show->{title}, $category, %type) ];
464
465    $tree->delete;
466
467    print "Parsed: " . Dumper($show) if ($debug);
468}
469
470sub calculate_stop_times
471{
472    foreach my $ch (keys %$shows)
473    {
474        my $last_start_time;
475        foreach my $s (reverse sort keys %{$shows->{$ch}})
476        {
477            $shows->{$ch}->{$s}->{stop} = $last_start_time if ($last_start_time);
478            $last_start_time = $shows->{$ch}->{$s}->{start};
479        }
480    }
481}
482
483sub write_xml
484{
485    my %writer_args = ( encoding => 'ISO-8859-1' );
486
487    &log("Writing " . &num_items($shows) . " shows to XML.");
488
489    if ($opt->{output})
490    {
491        my $fh = new IO::File(">" . $opt->{output})
492            or die "Can't open " . $opt->{output} . ": $!";
493        $writer_args{OUTPUT} = $fh;
494    }
495
496    my $writer = new XMLTV::Writer(%writer_args);
497
498    $writer->start
499        ( { 'source-info-url'    => $DATASOURCE,
500            'source-info-name'   => "Citysearch",
501            'generator-info-name' => "$progname $version"} );
502
503    for my $channel (sort keys %$channels)
504    {
505        $writer->write_channel( { 
506                'display-name' => [ [ $channel, $lang ] ],
507                'id' => $channels->{$channel} } );
508    }
509
510    foreach my $ch (sort keys %$shows)
511    {
512        foreach my $s (sort keys %{$shows->{$ch}})
513        {
514            # Don't return shows with no stop time
515            next unless ($shows->{$ch}->{$s}->{stop});
516
517            # Format for XMLTV-compliance
518            my %p = %{$shows->{$ch}->{$s}};
519            foreach my $field ('title', 'sub-title', 'desc', 'country')
520            {
521                $p{$field} = [[ $p{$field}, $lang ]] if ($p{$field});
522            }
523            $p{language} = [ $p{language}, $lang ] if ($p{language});
524            $p{start} = &POSIX::strftime("%Y%m%d%H%M", localtime($p{start}));
525            $p{stop} = &POSIX::strftime("%Y%m%d%H%M", localtime($p{stop}));
526            $p{rating} = [[ $p{rating}, 'ABA', undef ]] if ($p{rating});
527            if ($p{category} && ref($p{category}) eq "ARRAY"
528                    && $p{category}[0] && ref($p{category}[0]) ne "ARRAY") # obsolete 14/10/2007
529            {
530                foreach (@{$p{category}})
531                {
532                    $_ = [ &Shepherd::Common::translate_category($_), $lang ];
533                }
534            }
535            if ($p{subtitles})
536            {
537                my @s;
538                foreach (@{$p{subtitles}})
539                {
540                    push @s, { type => $_ };
541                }
542                $p{subtitles} = [ @s ];
543            }
544            delete $p{pid};
545            delete $p{details};
546
547            &log("-> " . $shows->{$ch}->{$s}->{title}) if ($debug);
548#           print Dumper(\%p);
549            $shows->{$ch}->{$s}->{start} = &POSIX::strftime("%Y%m%d%H%M", localtime($s));
550            $writer->write_programme(\%p);
551        }
552    }
553
554    $writer->end();
555}
556
557# ---------------------------------------------------------------------
558# Helper subs
559
560sub num_items
561{
562    my $hash = shift;
563    my $count = 0;
564    foreach my $ch (keys %$hash)
565    {
566        $count += scalar keys %{$hash->{$ch}};
567    }
568    return $count;
569}
570
571sub is_outside_gaps
572{
573    my ($ch, $start, $stop) = @_;
574
575    foreach my $gap (@{$gaps->{$ch}})
576    {
577        if ($gap =~ /(\d+)-(\d+)/)
578        {
579            return 0 if ($stop > $1 and $start < $2);
580        }
581    }
582    return 1;
583}
584
585sub strip_whitespace 
586{
587    $_[0] =~ /^\s*(.*?)\s*$/ ? $1 : $_[0];
588}
589
590# ---------------------------------------------------------------------
591# Setup subs
592
593
594sub read_cache
595{
596    $cache = Shepherd::Common::read_cache(\$opt->{'cache-file'});
597    if ($cache)
598    {
599        &log("Retrieved " . &num_items($cache) . " cached items from file.");
600        &clean_cache;
601    }
602    else
603    {
604        $cache = { };
605        &log("Not using cache.");
606    }
607    if ($opt->{'dump-cache'})
608    {
609        &log("Dumping cache.");
610        print Dumper($cache);
611        exit 0;
612    }
613}
614
615sub clean_cache
616{
617    my $cutoff = $runtime - 86400;   
618    &log("Removing cached shows that finish earlier than " . localtime($cutoff) . ".") if ($debug);
619    my $count = 0;
620
621    foreach my $ch (keys %$cache)
622    {
623        foreach my $s (keys %{$cache->{$ch}})
624        {
625            if ($cache->{$ch}->{$s}->{stop} < $cutoff)
626            {
627                &log("Removing $cache->{$ch}->{$s}->{title}.") if ($debug);
628                delete $cache->{$ch}->{$s};
629                $count++;
630            }
631        }
632    }
633    &log("Removed $count stale items from cache.") if ($count);
634}
635
636sub write_cache
637{
638    my $n = &num_items($cache);
639    return unless ($n);
640    &log("Writing $n shows to cache.");
641    Shepherd::Common::write_cache($opt->{'cache-file'}, $cache);
642}
643
644sub set_region
645{
646    my %regions = ( 81 => 'adelaide', 75 => 'brisbane', 126 => 'canberra',
647                    74 => 'darwin', 88 => 'hobart', 94 => 'melbourne', 
648                    101 => 'perth', 73 => 'sydney' );
649    unless ($regions{$opt->{region}})
650    {
651        &log("ERROR: unsupported region \"$opt->{region}\".");
652        exit 3;
653    }
654    $opt->{rname} = $regions{$opt->{region}};
655    $DATASOURCE = "http://$opt->{rname}.$DATASOURCE";
656    &log("Datasource: $DATASOURCE") if ($debug);
657}
658
659
660sub get_command_line_options
661{
662    &Getopt::Long::Configure('pass_through');
663    &GetOptions($opt, qw(
664                            help
665                            debug
666                            output=s
667                            days=i
668                            offset=i
669                            region=i
670                            dump-cache
671                            cache-file=s
672                            channels_file=s
673                            gaps_file=s
674                            version
675                            warper
676                        ));
677    $debug = $opt->{debug};
678
679    if (@ARGV)
680    {
681        &log("\nUnknown option(s): @ARGV\n");
682    }
683}
684
685sub set_defaults
686{
687    my $defaults = {
688        'days' => 7,
689        'offset' => 0,
690        'region' => 94,
691        'output' => &getcwd . '/output.xmltv',
692        'cache-file' => &getcwd . '/' . $progname . '.cache',
693        'channels_file' => &getcwd . '/channels.conf'
694    };
695
696    foreach (keys %$defaults)
697    {
698        unless (defined $opt->{$_})
699        {
700            $opt->{$_} = $defaults->{$_};
701        }
702    }
703
704    $opt->{'days'} = 7 if ($opt->{'days'} > 7);
705
706    &Shepherd::Common::set_defaults(
707        stats => \%stats,
708        delay => "1-5",
709        debug => $debug,
710        webwarper => $opt->{warper}
711        );
712
713    # Initialize stats
714    %stats = ( );
715    foreach (qw( cache_hits shows ))
716    {
717        $stats{$_} = 0;
718    }
719}
720
721sub read_channels_file 
722{
723    &read_config_file('channels', 'channels_file');
724}
725
726sub read_gaps_file
727{
728    &read_config_file('gaps', 'gaps_file');
729    if ($gaps)
730    {
731        foreach (keys %$gaps)
732        {
733            $gaps->{$channels->{$_}} = $gaps->{$_};
734            delete $gaps->{$_};
735        }
736    }
737}
738
739sub read_config_file
740{
741    my ($name, $arg) = @_;
742
743    return unless ($opt->{$arg});
744    &log("Reading $name file: $opt->{$arg}");
745    if (-r $opt->{$arg})
746    {
747        local (@ARGV, $/) = ($opt->{$arg});
748        no warnings 'all';
749        eval <>;
750        die "Can't parse $name file: $@" if ($@);
751    }
752    else
753    {
754        &log("Unable to read $name file.");
755    }
756}
757
758sub log
759{
760    &Shepherd::Common::log(@_);
761}
762
763sub help
764{
765    print q{
766Command-line options:
767  --help                 Print this message
768  --version              Show current version
769
770  --output <file>        Write XML into the specified file
771  --channels_file <file> Read channel subscriptions from file
772
773  --region <n>           Grab data for region code <n>
774  --days <n>             Grab <n> days of data (today being day 1)
775  --offset <n>           Skip the first <n> days
776
777  --debug                Print lots of debugging output
778};
779    exit 0;
780}
Note: See TracBrowser for help on using the browser.