root/grabbers/oztivo @ 860

Revision 860, 13.3 kB (checked in by max, 6 years ago)

oztivo: drop 0/10 star-ratings

Line 
1#!/usr/bin/perl -w
2
3# OzTivo grabber
4
5my $version = '1.10';
6
7# Requires configuration!
8# 1. Register at http://www.tvguide.org.au/
9# 2. Run "./oztivo --configure" to create "oztivo.pw" file.
10
11use strict;
12
13use Getopt::Long;
14use HTML::Entities;
15use POSIX;
16use Shepherd::Common;
17
18my $progname = 'oztivo';
19my $nicename = 'OzTivo';
20my $config_file = "$progname.pw";
21my $output_file = "output.xmltv";
22my $channels_file;
23my $channels, my $opt_channels;
24my @clist;
25my $ver;
26my $ready;
27my $configure;
28my $raw_input;
29my $raw_output;
30my $d;
31
32print "$nicename Grabber v$version\n";
33
34$| = 1;
35
36GetOptions( 
37            'channels_file=s'   => \$channels_file,
38            'output=s'          => \$output_file,
39            'rawoutput=s'       => \$raw_output,
40            'rawinput=s'        => \$raw_input,
41            'version'           => \$ver,
42            'ready'             => \$ready,
43            'configure'         => \$configure
44          );
45
46exit 0 if ($ver);
47
48configure() if ($configure);
49
50unless (-r $config_file)
51{
52    print "Can't find $config_file!\n";
53    please_configure_me();
54    exit 1;
55}
56
57print "Reading configuration file $config_file.\n";
58
59unless (open(CONF, $config_file))
60{
61    print "Unable to read config file $config_file: $!\n";
62    please_configure_me();
63    exit 1;
64}
65my $line = <CONF>;
66close CONF;
67
68unless ($line =~ /^(.*):(.*)$/)
69{
70    print "Unable to parse config file!\n" .
71          "It should be in the format: username:password\n";
72    please_configure_me();
73    exit 1;
74}
75my ($user, $pw) = ($1, $2);
76
77unless ($user and $pw)
78{
79    print "Failed to extract a sensible username and password from config file.\n";
80    please_configure_me();
81    exit 1;
82}
83
84exit 0 if ($ready);
85
86unless ($channels_file)
87{
88    die "No --channels_file specified.\n";
89}
90
91unless( -r $channels_file)
92{
93    die "Unable to read channels file $channels_file: $!";
94}
95local (@ARGV, $/) = ($channels_file);
96eval <>;
97die "\nError in channels file!\nDetails:\n$@" if ($@);
98
99my $shortchannels;
100while (my ($name, $chanid) = each %$channels) 
101{
102  # Ignore differences between rural stations like
103  # Seven (Rockhampton) and Seven (Cairns) -- this is not a great
104  # solution and should be fixed.
105  $name =~ s/ *\(.*?\)//g;
106  # Ignore differences between rural stations like
107  # Prime Tamworth/Taree/Port,Prime Lismore/Coffs Hbr -- this is not a great
108  # solution and should be fixed.
109  $name = "Prime" if $name =~ /^Prime/;
110  $shortchannels->{$name} = $chanid;
111}
112
113# Create a list of channel names from longest to shortest
114@clist = sort { length $b <=> length $a } keys %$shortchannels;
115
116my $data;
117
118if (!defined $raw_input) {
119    my ($success, $status);
120
121    # Don't rely on Shepherd::Common::get_url()'s retry, because
122    # if we get a 401 (wrong password) there's no point in retrying.
123    my $max_tries = 3;
124    foreach my $tries (1 .. $max_tries) {
125        ($data, $success, $status) = Shepherd::Common::get_url(
126                url => "http://$user:$pw\@minnie.tuhs.org/tivo-bin/xmlguide.pl",
127                retries => 0);
128
129        unless ($success)
130        {
131            if ($status =~ /401/)
132            {
133                print "Your OzTivo username and/or password may be incorrect!\n".
134                        "The username and password you supplied when configuring the oztivo grabber\n" .
135                        "must match your registration details on the OzTivo.com site.\n".
136                        "If this error persists, try reconfiguring: ".
137                        "  tv_grab_au --configure $progname\n";
138                $tries = $max_tries;
139            }
140            print "Download failed: $status\n";
141        }
142
143        my $bytes = do { use bytes; length($data) };
144        my $sleep = 15;
145
146        # If we get a special message from the oztivo server, sleep
147        # a longer time before retrying.
148        if ($bytes < 1000 and defined $data and $data =~ /<message>(.*?)<\/message>/si)
149        {
150            $success = 0;
151            print "OzTivo message: $1\n";
152            # Giveup unless message says to try again later
153            # eg. OzTivo message: System load on minnie is too high right now to run your request. Please try again later.
154            $tries = $max_tries unless $1 =~ /try\s+again\s+later/si;
155            $sleep = 30 + int(rand(120));
156        }
157        last if ($success);
158        exit 6 if $tries == $max_tries; 
159        print "Sleeping for $sleep seconds...\n";
160        sleep($sleep);
161    }
162
163    if (defined $raw_output) {
164        open(F,">$raw_output") || die "could not write raw output to $raw_output: $!\n";
165        print F $data;
166        close(F);
167        print "Raw output saved in $raw_output.\n";
168    }
169} else {
170    open(F,"<$raw_input") || die "could not read raw input from $raw_input: $!\n";
171    while(<F>) {
172        $data .= $_;
173    }
174    close(F);
175    print "Raw input read from $raw_input.\n";
176}
177
178print "Converting apostrophes.\n";
179$data =~ s/\&#39;/'/g;
180
181# Adjust ABC2 times if we're in a non-DST zone but Sydney is in DST.
182# Apparently ABC2 times are set off Sydney's clock.
183adjust_abc2();
184
185print "Transforming XMLTVIDs.\n";
186&setup_channel_mappings;
187$data =~ s/channel="(.*)"/'channel="'.subme($1).'"'/ge;
188
189print "Writing output.\n";
190open (OUT, ">$output_file") || die "could not write to $output_file: $!\n";
191
192#
193# oztivo generates invalid XMLTV with fields out of order and
194# some blank fields.
195# the standard XMLTV.pm perl module gets very unhappy about these
196# write output in a manner which addresses the bad input
197#
198
199my @xmltv_tag_order = qw [ title sub-title desc credits date category language
200        orig-language length icon url country episode-num video audio
201        previously-shown permiere last-chance new subtitles rating
202        star-rating ];
203my %xmltv_tags = map { $_ => "" } @xmltv_tag_order;
204$xmltv_tags{"programme"}=""; $xmltv_tags{"/programme"}="";
205my $cur_field = "";
206
207foreach my $line (split/\n/,$data) {
208
209    # oztivo generates blank data for these fields - skip if blank
210    next if ($line =~ /<director><\/director>/);
211    next if ($line =~ /<desc><\/desc>/);
212    next if ($line =~ /^\s*$/);
213
214    # more oztivo weirdness. filter out 0/10 star-ratings
215    next if ($line =~ /<star-rating><value>0\/10</);
216
217    if ($line =~ /\s*<([\/a-zA-Z\-]+)/) {
218        my $field = lc($1);
219        # do we know about this tag?
220        $cur_field = $field if (defined $xmltv_tags{$field});
221    }
222
223    if ($cur_field eq "programme") {
224        # if we have a start="(time)" and/or stop="(time)" make sure they
225        # have a timezone on them.
226        $line = $1."start=\"".$2." +0000\"".$3 if ($line =~ /^(.*)start="([0-9]+)"(.*)/);
227        $line = $1."stop=\"".$2." +0000\"".$3 if ($line =~ /^(.*)stop="([0-9]+)"(.*)/);
228
229        print OUT $line."\n"; # programme tag
230        $cur_field = "";
231    } elsif ($cur_field eq "/programme") {
232        print "Program with no title!\n" 
233                if (($xmltv_tags{title} eq "") or ($xmltv_tags{title} =~ />\s*</));
234        # print all previously seen tags in xmltv_tag_order
235        foreach my $xmltag (@xmltv_tag_order) {
236            if ($xmltv_tags{$xmltag} ne "") {
237                print OUT $xmltv_tags{$xmltag};
238                $xmltv_tags{$xmltag} = "";
239            }
240        }
241        print OUT $line."\n"; # /programme tag
242        $cur_field = "";
243    } else {
244        if ($cur_field eq "") {
245            print OUT $line."\n"; # ?xml, tv and /tv tags
246        } else {
247            $xmltv_tags{$cur_field} .= $line."\n"; # xmltv_tags tags
248        }
249    }
250}
251
252close OUT;
253
254print "Done.\n";
255
256
257
258sub please_configure_me
259{
260    print "If you wish to use $nicename, please run \"" .
261          "tv_grab_au --configure $progname\".\n" .
262          "Configuration of $progname is required.\n";
263}
264
265sub configure
266{
267    print "Configuring...\n\n" .
268          "Before you can use the $nicename grabber, you must create an\n" .
269          "account here:\n\n" .
270          '  http://minnie.tuhs.org/twiki/bin/view/TWiki/TWikiRegistration' .
271          "\n\n" .
272          "When you're done, you'll have a username and a password. Enter\n" .
273          "these here.\n\n" .
274          "Username?\n";
275    my $username = <>;
276    chomp $username;
277    unless ($username)
278    {
279        print "No username supplied. Exiting configuration.\n";
280        exit 3;
281    }
282    print "Password?\n";
283    my $pw = <>;
284    chomp $pw;
285    unless ($pw)
286    {
287        print "No password supplied. Exiting configuration.\n";
288        exit 3;
289    }
290    print "Creating config file $config_file...\n";
291    open (CONF, ">$config_file")
292        or die "Unable to create $config_file: $!";
293    print CONF "$username:$pw";
294    close CONF;
295
296    print "Done.\n";
297    exit 0;
298}
299
300sub subme
301{
302    my $station = shift;
303
304    $station = $d->{chan_map}->{$station} if (defined $d->{chan_map}->{$station});
305    $station =~ s/SC10/Sthn Cross TEN/g; # clashes with TEN but @clist is sorted longest to shortest
306
307    my $num = $1 if ($station =~ /(\d{2,})/);
308    foreach my $ch (@clist)
309    {
310        if ($station =~ /$ch/i
311                or
312            ($num and $ch =~ /$num/))
313        {
314            return $shortchannels->{$ch};
315        }
316    }
317
318    return $opt_channels->{$station} if ((defined $opt_channels) && (defined $opt_channels->{$station}));
319   
320    if (!defined $d->{ignored_channels}->{$station}) {
321        $d->{ignored_channels}->{$station} = 1;
322        print "Warning: station \"$station\" unknown.\n";
323    }
324    return $station;
325}
326
327sub setup_channel_mappings
328{
329    my %map = (
330        "SBS-NEWS" => "SBS NEWS",
331        "ADV1" => "A1",
332        "ANIMAL" => "AnimalPlanet",
333        "ANT" => "AntennaPacific",
334        "ARNA" => "ARENATV",
335        "ARNA+2" => "ARENATV2",
336        "AUR" => "Aurora",
337        "BBC" => "BBCWorld",
338        "BIOG" => "Bio",
339        "BLM" => "BloombergTelevision",
340        "BOOM" => "Boomerang",
341        "CART" => "CartoonNetwork",
342        "CLAS" => "FOXClassics",
343        "CLAS+2" => "FOXClassics2",
344        "CMC" => "CountryMusicChannel",
345        "CMDY" => "THECOMEDYCHANNEL",
346        "CMDY+2" => "THECOMEDYCHANNEL2",
347        "CNBC" => "CNBCAustralia",
348        "CNNI" => "CNN",
349        "CRIME" => "CrimeandInvestigation",
350        "DISC" => "DiscoveryChannel",
351        "DISN" => "DisneyChannel",
352        "E!" => "E!Entertainment",
353        "ESPN" => "ESPN",
354        "EUROSPORT" => "Eurosportnews",
355        "EXPO" => "EXPO",
356        "FASH" => "FashionTV",
357        "FOOD" => "LifeStyleFOOD",
358        "FOX8" => "FOX8",
359        "FOX8+2" => "FOX82",
360        "FOXN" => "FOXNews",
361        "FS1" => "FOXSPORTS1",
362        "FS2" => "FOXSPORTS2",
363        "FS3" => "FOXSPORTS3",
364        "FSN" => "FOXSPORTSNews",
365        "FUEL" => "FUELTV",
366        "HALL" => "Hallmark",
367        "HEALTH" => "DiscoveryHealth",
368        "HIST" => "TheHistoryChannel",
369        "HOWTO" => "HOWTOChannel",
370        "LIFE" => "TheLifeStyleChannel",
371        "LIFE+2" => "LifestyleChannel2",
372        "MOV1" => "MOVIEONE",
373        "MOV1+2" => "MOVIETWO",
374        "MOVG" => "MOVIEGREATS",
375        "MOVX" => "MOVIEEXTRA",
376        "MTV" => "MTV",
377        "NGEO" => "NationalGeographic",
378        "NICK" => "Nickelodeon",
379        "NICKJR" => "NickJr",
380        "OVAT" => "Ovation",
381        "PHDISN" => "PlayhouseDisney",
382        "RAI" => "RAIInternational",
383        "SCIENCE" => "DiscoveryScience",
384        "SHOW" => "SHOWTIME",
385        "SHW2" => "SHOWTIME2",
386        "SHWGRTS" => "SHOWTIMEGreats",
387        "SKYN" => "SkyNewsAustralia",
388        "SKYR" => "SkyRacing",
389        "Sci-Fi" => "SCIFI",
390        "TCM" => "TCM",
391        "TRAVEL" => "DiscoveryTravel",
392        "TV1" => "TV1",
393        "TV1+2" => "TV12",
394        "TVSN" => "TVSN",
395        "TWC" => "TheWeatherChannel",
396        "UKTV" => "UKTV",
397        "UKTV+2" => "UKTV2",
398        "V" => "ChannelV",
399        "V2" => "ChannelV2",
400        "VH1" => "VH1",
401        "W" => "W",
402        "W+2" => "W2",
403        "WMOV" => "WORLDMOVIES",
404        "max" => "MAX"
405    );
406
407    foreach (keys %map)
408    {
409        $d->{chan_map}->{$_} = $map{$_};
410    }
411}
412
413# Wrapper sub to ensure TZ gets reset properly
414sub adjust_abc2
415{
416    adjust_abc2_times();
417    POSIX::tzset();
418}
419
420sub adjust_abc2_times
421{
422    my $local_tz = POSIX::strftime("%z",localtime);
423
424    local %ENV;
425    $ENV{TZ} = 'Australia/Sydney';
426    my $sydney_tz = POSIX::strftime("%z",localtime);
427
428    # Okay, I can't believe how hard this is... compute difference
429    # between two TZ strings.
430    my $l_min = int($local_tz / 100) * 60 + $local_tz % 100 % 60; 
431    my $s_min = int($sydney_tz / 100) * 60 + $sydney_tz % 100 % 60; 
432    my $tz_diff = int(($l_min - $s_min) / 60) * 100 + (($l_min - $s_min) % 60);
433
434    # Are we in DST?
435    my $local_dst = (localtime)[8];
436    my $sydney_dst = 0;
437    if (!$local_dst)
438    {
439        # Is Sydney in DST?
440        $sydney_dst = compare_sydney_dst();
441
442        if ($sydney_dst)
443        {
444            print "DST in Sydney but not here: adding 1hr to ABC2 times" .
445                  ($sydney_dst != 1 ? ($sydney_dst > 1 ? 
446                          " from $sydney_dst GMT" : 
447                          " until " . ($sydney_dst*-1) . ' GMT') : '') .
448                  ".\n";
449        }
450    }
451    $data =~ s/(?!=")(\d+)(?=".* channel="ABC2")/$1.add_offset($1,$tz_diff,$sydney_dst)/ge;
452}
453
454# Return '+0100' for any times that are in a period when Sydney's in DST
455# but we're not.
456sub add_offset
457{
458    my ($timestamp, $tz_diff, $sydney_dst) = @_;
459
460    if ($sydney_dst == 1
461            or
462        ($sydney_dst > 1 and $timestamp >= $sydney_dst)
463            or
464        ($sydney_dst < 0 and $timestamp <= ($sydney_dst*-1)))
465    {
466        $tz_diff += 0100;
467    }
468    return sprintf " %+0.4d", $tz_diff;
469}
470
471# Returns:
472# 0         : Sydney is not in DST in the next 7 days
473# 1         : Sydney is in DST for the next 7 days
474# <n>       : Sydney is in DST from <n> GMT, where n is (eg) 20070321030000
475# <-n>      : Sydney is in DST until <n> GMT, where n is (eg) -20070321030000
476# This func assumes that TZ has been set to Sydney by the caller (adjust_abc2_times)
477sub compare_sydney_dst
478{
479    my $sydney_dst = (localtime)[8];
480
481    # Normalize $start to 3AM Sydney time, or 2AM if it's in DST
482    my $start = time - (time % (60*60*24)) - (($sydney_dst ? 11 : 10) * 60*60) + (3*60*60);
483
484    # Check Sydney's DST status each day for 7 days
485    for (my $day = 0; $day < 7; $day++)
486    {
487        my $t = $start + ($day * 24*60*60);
488        my $sydney_dst_thisday = (localtime($t))[8];
489
490        # Any changeover?
491        if ($sydney_dst_thisday != $sydney_dst)
492        {
493            # Convert $t to GMT
494            $t -= (($sydney_dst ? 11 : 10) * 60*60);
495            my $changeoverday = POSIX::strftime("%Y%m%d%H%M%S", localtime($t));
496            $changeoverday *= -1 if ($sydney_dst);
497            return $changeoverday;
498        }
499    }
500    return $sydney_dst;
501}
Note: See TracBrowser for help on using the browser.