| 1 | #!/usr/bin/env perl |
|---|
| 2 | |
|---|
| 3 | # Augment XMLTV start/stop times with the local timezone if MythTV's |
|---|
| 4 | # TimeOffset setting is anything other than "None" |
|---|
| 5 | # * to be used as a postprocessor for XMLTV data |
|---|
| 6 | # * can be used in conjunction with 'shepherd' XMLTV reconciler or standalone |
|---|
| 7 | # (pipe-through) |
|---|
| 8 | # * no configuration necessary |
|---|
| 9 | # |
|---|
| 10 | # input XMLTV files will either have programme start/stop with or without |
|---|
| 11 | # timezones. If no timezone is present, Shepherd assumes the input |
|---|
| 12 | # start/stop times are in 'localtime'. |
|---|
| 13 | # |
|---|
| 14 | # If MythTV's "TimeOffset" setting is set to anything other than 'None', |
|---|
| 15 | # this can cause programming information to be out: |
|---|
| 16 | # - if set to 'All', all programs will be out by the difference between |
|---|
| 17 | # GMT and locatime ('All' means MythTV is expecting all start/stop times |
|---|
| 18 | # in GMT) |
|---|
| 19 | # - if explicitly set to GMT +/- XX then this will cause programming to |
|---|
| 20 | # be out whenever there is a switchover to/from daylight savings |
|---|
| 21 | # |
|---|
| 22 | # this postprocessor addresses this by explicitly putting a timezone |
|---|
| 23 | # on every programme that doesn't already have one, BUT ONLY IF |
|---|
| 24 | # MythTV is configured to anything other than 'None'. |
|---|
| 25 | # |
|---|
| 26 | # provided your unix system is configured into the correct timezone, |
|---|
| 27 | # this will work just fine including boundaries crossing daylight savings. |
|---|
| 28 | # |
|---|
| 29 | # it means that it doesn't matter if MythTV's "TimeOffset" is set to |
|---|
| 30 | # 'All' or 'None', or something inbetween, the data will be right regardless. |
|---|
| 31 | |
|---|
| 32 | # Specific enhancement for Broken Hill: |
|---|
| 33 | # it seems ABC1 broadcasts to Broken Hill with incorrect times. They're using |
|---|
| 34 | # a satellite feed from NSW so Broken Hill is always out by 30 minutes _just_ |
|---|
| 35 | # for ABC1 (not ABC2 or any other stations). |
|---|
| 36 | # Since this seems to happen across all ABC1 data from all grabbers (i.e. all |
|---|
| 37 | # data sources get this wrong), fix it up in this postprocessor |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | use strict; |
|---|
| 41 | use warnings; |
|---|
| 42 | my $progname = "augment_timezone"; |
|---|
| 43 | my $version = "0.22"; |
|---|
| 44 | |
|---|
| 45 | use XMLTV; |
|---|
| 46 | use POSIX qw(strftime mktime); |
|---|
| 47 | use Getopt::Long; |
|---|
| 48 | use IO::File; |
|---|
| 49 | |
|---|
| 50 | $| = 1; |
|---|
| 51 | my %stats; |
|---|
| 52 | my $channels, my $opt_channels; |
|---|
| 53 | |
|---|
| 54 | my $opt = { }; |
|---|
| 55 | $opt->{output_file} = "output.xmltv"; |
|---|
| 56 | $opt->{debug} = 0; |
|---|
| 57 | |
|---|
| 58 | # parse command line |
|---|
| 59 | GetOptions( |
|---|
| 60 | 'output=s' => \$opt->{output_file}, |
|---|
| 61 | 'mysql_file=s' => \$opt->{mysql_file}, |
|---|
| 62 | 'timeoffset=s' => \$opt->{timeoffset}, |
|---|
| 63 | 'chanadjust=s' => \$opt->{chanadjust}, |
|---|
| 64 | |
|---|
| 65 | 'region=i' => \$opt->{region}, |
|---|
| 66 | 'days=i' => \$opt->{days}, # ignored |
|---|
| 67 | 'offset=i' => \$opt->{offset}, # ignored |
|---|
| 68 | 'timezone=s' => \$opt->{timezone}, # ignored |
|---|
| 69 | 'channels_file=s' => \$opt->{channels_file}, |
|---|
| 70 | 'config-file=s' => \$opt->{configfile}, # ignored |
|---|
| 71 | |
|---|
| 72 | 'help' => \$opt->{help}, |
|---|
| 73 | 'verbose' => \$opt->{help}, |
|---|
| 74 | 'version' => \$opt->{version}, |
|---|
| 75 | 'ready' => \$opt->{ready}, |
|---|
| 76 | 'desc' => \$opt->{desc}, |
|---|
| 77 | 'v' => \$opt->{version}); |
|---|
| 78 | |
|---|
| 79 | printf "%s v%s\n",$progname,$version; |
|---|
| 80 | |
|---|
| 81 | # Check if Shepherd::MythTV is working |
|---|
| 82 | my $mythtv_access; |
|---|
| 83 | unless ($opt->{timeoffset}) |
|---|
| 84 | { |
|---|
| 85 | eval { require Shepherd::MythTV; }; |
|---|
| 86 | if ($@) |
|---|
| 87 | { |
|---|
| 88 | print "\nWARNING: No support for Shepherd::MythTV!\n". |
|---|
| 89 | "Continuing without MythTV access. Target timeoffset will be \"None\".\n". |
|---|
| 90 | "(You may override this with --timeoffset <offset>)\n". |
|---|
| 91 | "Reason: $@\n"; |
|---|
| 92 | } else { |
|---|
| 93 | $mythtv_access = 1; |
|---|
| 94 | } |
|---|
| 95 | } |
|---|
| 96 | |
|---|
| 97 | if ($opt->{version} || $opt->{desc} || $opt->{help} || $opt->{ready} || |
|---|
| 98 | $opt->{output_file} eq "") { |
|---|
| 99 | printf "Automatically adjust the XMLTV start/stop timezone based on MythTV's\n". |
|---|
| 100 | "TImeOffset setting.\n" if $opt->{desc}; |
|---|
| 101 | |
|---|
| 102 | printf "$progname is ready for operation.\n" if ($opt->{ready}); |
|---|
| 103 | |
|---|
| 104 | printf "No --output file specified.\n" if ($opt->{output_file} eq ""); |
|---|
| 105 | |
|---|
| 106 | if ($opt->{help} || $opt->{output_file} eq "") { |
|---|
| 107 | my $default_loc = ($mythtv_access ? &Shepherd::MythTV::standard_mysql_locations : 'none'); |
|---|
| 108 | print<<EOF |
|---|
| 109 | |
|---|
| 110 | usage: $0 [options] {FILE(s)} |
|---|
| 111 | |
|---|
| 112 | Supported options include: |
|---|
| 113 | --output={file} Send final XMLTV output to {file} (default: $opt->{output_file}) |
|---|
| 114 | --mysql_file={file} File where we look for mythtv database user/pass/dbi (default: $default_loc) |
|---|
| 115 | --timeoffset={s} Specify MythTV's setting, rather than try to look it up |
|---|
| 116 | in MythTV's database. (E.g. "Auto", "None", "+1000") |
|---|
| 117 | --chanadjust={s} Specify a specific channel to be time-adjusted |
|---|
| 118 | (e.g. "SBS,30" will add 30 minutes to all SBS programs) |
|---|
| 119 | EOF |
|---|
| 120 | ; |
|---|
| 121 | } |
|---|
| 122 | exit(0); |
|---|
| 123 | } |
|---|
| 124 | |
|---|
| 125 | if (!$opt->{timeoffset} and $mythtv_access) { |
|---|
| 126 | # Specify a non-standard location for mysql.txt |
|---|
| 127 | Shepherd::MythTV::setup($opt->{mysql_file}) if ($opt->{mysql_file}); |
|---|
| 128 | |
|---|
| 129 | my $sql = "SELECT data FROM settings WHERE value LIKE 'TimeOffset'"; |
|---|
| 130 | ($opt->{timeoffset}) = Shepherd::MythTV::query($sql); |
|---|
| 131 | if ($opt->{timeoffset}) |
|---|
| 132 | { |
|---|
| 133 | printf " MythTV's TimeOffset is \"%s\".\n", $opt->{timeoffset}; |
|---|
| 134 | } else { |
|---|
| 135 | print " No valid response from MythTV.\n". |
|---|
| 136 | " Assuming MythTV's timezone is \"None\".\n". |
|---|
| 137 | " *** If this is wrong, guide data may be in wrong timezone! ***\n\n"; |
|---|
| 138 | } |
|---|
| 139 | } |
|---|
| 140 | |
|---|
| 141 | $opt->{timeoffset} = "None" if (!defined $opt->{timeoffset}); |
|---|
| 142 | if ($opt->{timeoffset} eq "None") { |
|---|
| 143 | print " - Target timezone is \"None\". No need to do anything.\n"; |
|---|
| 144 | } else { |
|---|
| 145 | printf " - Target timezone is \"%s\". Adding timezones.\n",$opt->{timeoffset}; |
|---|
| 146 | } |
|---|
| 147 | |
|---|
| 148 | # ABC1 fixup for Broken Hill; also chanadjust |
|---|
| 149 | if ($opt->{chanadjust} or (defined $opt->{region}) && ($opt->{region} == 63)) { |
|---|
| 150 | die "no channel file specified\n", if (!$opt->{channels_file}); |
|---|
| 151 | |
|---|
| 152 | # read channels file |
|---|
| 153 | if (-r $opt->{channels_file}) { |
|---|
| 154 | local (@ARGV, $/) = ($opt->{channels_file}); |
|---|
| 155 | no warnings 'all'; eval <>; die "$@" if $@; |
|---|
| 156 | } else { |
|---|
| 157 | die "WARNING: channels file $opt->{channels_file} could not be read\n"; |
|---|
| 158 | } |
|---|
| 159 | |
|---|
| 160 | if ($opt->{region} and $opt->{region} == 63 and $channels->{ABC1}) { |
|---|
| 161 | print " - System is in Broken Hill. Adjusting ABC1 guide data by 30 minutes!\n"; |
|---|
| 162 | } |
|---|
| 163 | } |
|---|
| 164 | |
|---|
| 165 | if ($opt->{chanadjust} and $opt->{chanadjust} =~ /(.*),(\d+)/) |
|---|
| 166 | { |
|---|
| 167 | $opt->{'chanadjust-channel'} = $channels->{$1}; |
|---|
| 168 | $opt->{'chanadjust-time'} = $2; |
|---|
| 169 | printf " - Manual channel adjustment: will add %d minutes to shows on %s.\n", |
|---|
| 170 | $opt->{'chanadjust-time'}, $opt->{'chanadjust-channel'}; |
|---|
| 171 | } |
|---|
| 172 | |
|---|
| 173 | my %writer_args = ( encoding => 'ISO-8859-1' ); |
|---|
| 174 | my $fh = new IO::File(">".$opt->{output_file}) || die "can't open $opt->{output_file} for writing: $!"; |
|---|
| 175 | $writer_args{OUTPUT} = $fh; |
|---|
| 176 | |
|---|
| 177 | my $writer = new XMLTV::Writer(%writer_args); |
|---|
| 178 | $writer->start( { |
|---|
| 179 | 'source-info-name' => "$progname $version", |
|---|
| 180 | 'generator-info-name' => "$progname $version"} ); |
|---|
| 181 | |
|---|
| 182 | foreach my $file (@ARGV) { |
|---|
| 183 | printf " - parsing: %s\n", ($file eq "-" ? "(from-stdin, hit control-D to finish)" : $file); |
|---|
| 184 | XMLTV::parsefiles_callback(undef, undef, \&channel_cb,\&programme_cb, $file); |
|---|
| 185 | } |
|---|
| 186 | |
|---|
| 187 | $writer->end(); |
|---|
| 188 | |
|---|
| 189 | printf "Finished parsing, output in $opt->{output_file}\n"; |
|---|
| 190 | printf "STATS: TimeOffset=".$opt->{timeoffset}; |
|---|
| 191 | foreach my $k (keys %stats) { |
|---|
| 192 | printf ", %d %s", $stats{$k}, $k; |
|---|
| 193 | } |
|---|
| 194 | printf "\n"; |
|---|
| 195 | |
|---|
| 196 | exit(0); |
|---|
| 197 | |
|---|
| 198 | ############################################################################## |
|---|
| 199 | |
|---|
| 200 | sub channel_cb( $ ) |
|---|
| 201 | { |
|---|
| 202 | my $c = shift; |
|---|
| 203 | # printf "got channel ".Dumper($c); |
|---|
| 204 | $writer->write_channel($c); |
|---|
| 205 | } |
|---|
| 206 | |
|---|
| 207 | ############################################################################## |
|---|
| 208 | |
|---|
| 209 | sub programme_cb( $ ) |
|---|
| 210 | { |
|---|
| 211 | my $prog=shift; |
|---|
| 212 | |
|---|
| 213 | # ABC1 fixup for Broken Hill |
|---|
| 214 | if ((defined $opt->{region}) && ($opt->{region} == 63) && |
|---|
| 215 | (defined $channels->{ABC1}) && ($prog->{channel} eq $channels->{ABC1})) { |
|---|
| 216 | $prog->{start} = POSIX::strftime("%Y%m%d%H%M00",localtime(parse_xmltv_date($prog->{start})-(30*60))); |
|---|
| 217 | $prog->{stop} = POSIX::strftime("%Y%m%d%H%M00",localtime(parse_xmltv_date($prog->{stop})-(30*60))); |
|---|
| 218 | } |
|---|
| 219 | |
|---|
| 220 | if ($opt->{'chanadjust-channel'} and $prog->{channel} eq $opt->{'chanadjust-channel'}) |
|---|
| 221 | { |
|---|
| 222 | $prog->{start} = POSIX::strftime("%Y%m%d%H%M00",localtime(parse_xmltv_date($prog->{start})+(60 * $opt->{'chanadjust-time'}))); |
|---|
| 223 | $prog->{stop} = POSIX::strftime("%Y%m%d%H%M00",localtime(parse_xmltv_date($prog->{stop})+(60 * $opt->{'chanadjust-time'}))); |
|---|
| 224 | } |
|---|
| 225 | |
|---|
| 226 | if ($opt->{timeoffset} ne "None") { |
|---|
| 227 | # if there is no timezone present in start time, put one there |
|---|
| 228 | if (($prog->{start} !~ /\+/) && ($prog->{start} !~ /\-/)) { |
|---|
| 229 | $prog->{start} = POSIX::strftime("%Y%m%d%H%M00 %z",localtime(parse_xmltv_date($prog->{start}))); |
|---|
| 230 | $stats{start_tz_added}++; |
|---|
| 231 | } |
|---|
| 232 | |
|---|
| 233 | # if there is no timezone present in stop time, put one there |
|---|
| 234 | if (($prog->{stop} !~ /\+/) && ($prog->{stop} !~ /\-/)) { |
|---|
| 235 | $prog->{stop} = POSIX::strftime("%Y%m%d%H%M00 %z",localtime(parse_xmltv_date($prog->{stop}))); |
|---|
| 236 | $stats{stop_tz_added}++; |
|---|
| 237 | } |
|---|
| 238 | } |
|---|
| 239 | |
|---|
| 240 | $writer->write_programme($prog); |
|---|
| 241 | } |
|---|
| 242 | |
|---|
| 243 | ############################################################################## |
|---|
| 244 | |
|---|
| 245 | # strptime type date parsing - BUT - if no timezone is present, treat time |
|---|
| 246 | # as being in localtime rather than the various other perl implementation |
|---|
| 247 | # which treat it as being in UTC/GMT |
|---|
| 248 | |
|---|
| 249 | sub parse_xmltv_date |
|---|
| 250 | { |
|---|
| 251 | my $datestring = shift; |
|---|
| 252 | my @t; # 0=sec,1=min,2=hour,3=day,4=month,5=year,6=wday,7=yday,8=isdst |
|---|
| 253 | my $tz_offset = 0; |
|---|
| 254 | |
|---|
| 255 | if ($datestring =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})/) { |
|---|
| 256 | ($t[5],$t[4],$t[3],$t[2],$t[1],$t[0]) = (int($1)-1900,int($2)-1,int($3),int($4),int($5),0); |
|---|
| 257 | ($t[6],$t[7],$t[8]) = (-1,-1,-1); |
|---|
| 258 | |
|---|
| 259 | my $e = mktime(@t); |
|---|
| 260 | |
|---|
| 261 | # if input data has a timezone offset, then offset by that |
|---|
| 262 | if ($datestring =~ /\+(\d{2})(\d{2})/) { |
|---|
| 263 | $tz_offset = calc_gmt_offset($e) - (($1*(60*60)) + ($2*60)); |
|---|
| 264 | } elsif ($datestring =~ /\-(\d{2})(\d{2})/) { |
|---|
| 265 | $tz_offset = calc_gmt_offset($e) + (($1*(60*60)) + ($2*60)); |
|---|
| 266 | } |
|---|
| 267 | |
|---|
| 268 | return ($e+$tz_offset) if ($e > 1); |
|---|
| 269 | } |
|---|
| 270 | return undef; |
|---|
| 271 | } |
|---|
| 272 | |
|---|
| 273 | ############################################################################## |
|---|
| 274 | |
|---|
| 275 | # given a particular date (in epoch time), return the local timezone offset |
|---|
| 276 | # on that date in -/+ seconds from GMT |
|---|
| 277 | |
|---|
| 278 | sub calc_gmt_offset |
|---|
| 279 | { |
|---|
| 280 | my $e = shift; |
|---|
| 281 | my $gmt_offset; |
|---|
| 282 | |
|---|
| 283 | my $tzstring = strftime("%z", localtime($e)); |
|---|
| 284 | $gmt_offset = (60*60) * int(substr($tzstring,1,2)); # hr |
|---|
| 285 | $gmt_offset += (60 * int(substr($tzstring,3,2))); # min |
|---|
| 286 | $gmt_offset *= -1 if (substr($tzstring,0,1) eq "-"); # +/- |
|---|
| 287 | |
|---|
| 288 | return $gmt_offset; |
|---|
| 289 | } |
|---|