Project

General

Profile

RE: Synology xmltv module and zap2xml help » zap2xml.pl

Greg H, 2017-01-09 02:44

 
1
#!/usr/bin/env perl
2
# zap2xml - <[email protected]> - tvschedule scraper - for personal use/not for redistribution     
3

    
4
BEGIN { $SIG{__DIE__} = sub { 
5
  return if $^S;
6
  my $msg = join(" ", @_);
7
  print STDERR "$msg";
8
  if ($msg =~ /can't locate/i) {
9
    print "\nSee homepage for tips on installing missing modules (example: \"perl -MCPAN -e shell\")\n";
10
    if ($^O eq 'MSWin32') {
11
      print "Use \"ppm install\" on windows\n";
12
    }
13
  }
14
  if ($^O eq 'MSWin32') {
15
    if ($msg =~ /uri.pm/i && $msg =~ /temp/i) {
16
      print "\nIf your scanner deleted the perl URI.pm file see the homepage for tips\n";
17
      if ($msg =~ /(\ .\:.+?par-.+?\\)/) {
18
        print "(Delete the $1 folder and retry)\n";
19
      }
20
    }
21
    sleep(5);
22
  } 
23
  exit 1;
24
}}
25

    
26
use Compress::Zlib;
27
use Encode;
28
use File::Basename;
29
use File::Copy;
30
use Getopt::Std;
31
use HTML::Parser 3.00 ();
32
use HTTP::Cookies;
33
use URI;
34
use URI::Escape;
35
use LWP::UserAgent;
36
use POSIX;
37
use Time::Local;
38
use JSON::PP;
39

    
40
no warnings 'utf8';
41

    
42
%options=();
43
getopts("?aA:bc:C:d:DeE:Fgi:Il:jJ:Lm:Mn:N:o:Op:P:qr:s:S:t:Tu:UwxY:zZ:",\%options);
44

    
45
$homeDir = $ENV{HOME};
46
$homeDir = $ENV{USERPROFILE} if !defined($homeDir);
47
$homeDir = '.' if !defined($homeDir);
48
$confFile = $homeDir . '/.zap2xmlrc';
49

    
50
# Defaults
51
$start = 0;
52
$days = 7;
53
$ncdays = 0;
54
$ncsdays = 0;
55
$retries = 3;
56
$outFile = 'xmltv.xml';
57
$outFile = 'xtvd.xml' if defined $options{x};
58
$cacheDir = 'cache';
59
$lang = 'en';
60
$userEmail = '';
61
$password = '';
62
$proxy;
63
$postalcode; 
64
$lineupId; 
65
$sleeptime = 0;
66
$shiftMinutes = 0;
67

    
68
$outputXTVD = 0;
69
$lineuptype;
70
$lineupname;
71
$lineuplocation;
72

    
73
$sTBA = "\\bTBA\\b|To Be Announced";
74

    
75
%tvgfavs=();
76

    
77
&printHelp() if defined $options{'?'};
78

    
79
$confFile = $options{C} if defined $options{C};
80
# read config file
81
if (open (CONF, $confFile))
82
{
83
  &pout("Reading config file: $confFile\n");
84
  while (<CONF>)
85
  {
86
    s/#.*//; # comments
87
    if (/^\s*$/i)                            { }
88
    elsif (/^\s*start\s*=\s*(\d+)/i)         { $start = $1; }
89
    elsif (/^\s*days\s*=\s*(\d+)/i)          { $days = $1; }
90
    elsif (/^\s*ncdays\s*=\s*(\d+)/i)        { $ncdays = $1; }
91
    elsif (/^\s*ncsdays\s*=\s*(\d+)/i)       { $ncsdays = $1; }
92
    elsif (/^\s*retries\s*=\s*(\d+)/i)       { $retries = $1; }
93
    elsif (/^\s*user[\w\s]*=\s*(.+)/i)       { $userEmail = &rtrim($1); }
94
    elsif (/^\s*pass[\w\s]*=\s*(.+)/i)       { $password = &rtrim($1); }
95
    elsif (/^\s*cache\s*=\s*(.+)/i)          { $cacheDir = &rtrim($1); }
96
    elsif (/^\s*icon\s*=\s*(.+)/i)           { $iconDir = &rtrim($1); }
97
    elsif (/^\s*trailer\s*=\s*(.+)/i)        { $trailerDir = &rtrim($1); }
98
    elsif (/^\s*lang\s*=\s*(.+)/i)           { $lang = &rtrim($1); }
99
    elsif (/^\s*outfile\s*=\s*(.+)/i)        { $outFile = &rtrim($1); }
100
    elsif (/^\s*proxy\s*=\s*(.+)/i)          { $proxy = &rtrim($1); }
101
    elsif (/^\s*outformat\s*=\s*(.+)/i)      { $outputXTVD = 1 if $1 =~ /xtvd/i; }
102
    elsif (/^\s*lineupid\s*=\s*(.+)/i)       { $lineupId = &rtrim($1); }
103
    elsif (/^\s*lineupname\s*=\s*(.+)/i)     { $lineupname = &rtrim($1); }
104
    elsif (/^\s*lineuptype\s*=\s*(.+)/i)     { $lineuptype = &rtrim($1); }
105
    elsif (/^\s*lineuplocation\s*=\s*(.+)/i) { $lineuplocation = &rtrim($1); }
106
    elsif (/^\s*postalcode\s*=\s*(.+)/i)     { $postalcode = &rtrim($1); }
107
    else
108
    {
109
      die "Oddline in config file \"$confFile\".\n\t$_";
110
    }
111
  }
112
  close (CONF);
113
} 
114
&printHelp() if !(%options) && $userEmail eq '';
115

    
116
$cacheDir = $options{c} if defined $options{c};
117
$days = $options{d} if defined $options{d};
118
$ncdays = $options{n} if defined $options{n};
119
$ncsdays = $options{N} if defined $options{N};
120
$start = $options{s} if defined $options{s};
121
$retries = $options{r} if defined $options{r};
122
$iconDir = $options{i} if defined $options{i};
123
$trailerDir = $options{t} if defined $options{t};
124
$lang = $options{l} if defined $options{l};
125
$outFile = $options{o} if defined $options{o};
126
$password = $options{p} if defined $options{p};
127
$userEmail = $options{u} if defined $options{u};
128
$proxy = $options{P} if defined $options{P};
129
$zlineupId = $options{Y} if defined $options{Y};
130
$zipcode = $options{Z} if defined $options{Z};
131
$includeXMLTV = $options{J} if defined $options{J} && -e $options{J};
132
$outputXTVD = 1 if defined $options{x};
133
$sleeptime = $options{S} if defined $options{S};
134
$shiftMinutes = $options{m} if defined $options{m};
135

    
136
$urlRoot = 'http://tvschedule.zap2it.com/tvlistings/';
137
$tvgurlRoot = 'http://mobilelistings.tvguide.com/';
138
$tvgMapiRoot = 'http://mapi.tvguide.com/';
139
$tvgurl = 'http://www.tvguide.com/';
140
$tvgspritesurl = 'http://static.tvgcdn.net/sprites/';
141

    
142
$retries = 20 if $retries > 20; # Too many
143

    
144
my %programs = ();
145
my $cp;
146
my %stations = ();
147
my $cs;
148
my $rcs;
149
my %schedule = ();
150
my $sch;
151
my $gridtimes = 0;
152
my $mismatch = 0;
153

    
154
my $coNum = 0;
155
my $tb = 0;
156
my $treq = 0;
157
my $expired = 0;
158
my $inStationTd = 0;
159
my $inIcons = 0;
160
my $inStationLogo = 0;
161
my $ua;
162
my $tba = 0;
163
my $exp = 0;
164
my @fh = ();
165

    
166
my $XTVD_startTime;
167
my $XTVD_endTime;
168

    
169
if (! -d $cacheDir) {
170
  mkdir($cacheDir) or die "Can't mkdir: $!\n";
171
} else {
172
  opendir (DIR, "$cacheDir/");
173
  @cacheFiles = grep(/\.html|\.js/,readdir(DIR));
174
  closedir (DIR);
175
  foreach $cacheFile (@cacheFiles) {
176
    $fn = "$cacheDir/$cacheFile";
177
    $atime = (stat($fn))[8];
178
    if ($atime + ( ($days + 2) * 86400) < time) {
179
      &pout("Deleting old cached file: $fn\n");
180
      unlink($fn);
181
    }
182
  }
183
}
184

    
185
my $s1 = time();
186
if (defined($options{z})) {
187

    
188
  &login() if !defined($options{a}); # get favorites
189
  &parseTVGIcons() if defined($iconDir);
190
  $gridHours = 3;
191
  $maxCount = $days * (24 / $gridHours);
192
  $ncCount = $maxCount - ($ncdays * (24 / $gridHours));
193
  $offset = $start * 3600 * 24 * 1000;
194
  $ncsCount = $ncsdays * (24 / $gridHours);
195
  $ms = &hourToMillis() + $offset;
196

    
197
  for ($count=0; $count < $maxCount; $count++) {
198
    if ($count == 0) { 
199
      $XTVD_startTime = $ms;
200
    } elsif ($count == $maxCount - 1) { 
201
      $XTVD_endTime = $ms + ($gridHours * 3600000) - 1;
202
    }
203

    
204
    $fn = "$cacheDir/$ms\.js\.gz";
205
    if (! -e $fn || $count >= $ncCount || $count < $ncsCount) {
206
      &login() if !defined($zlineupId);
207
      my $duration = $gridHours * 60;
208
      my $tvgstart = substr($ms, 0, -3);
209
      $rc = Encode::encode('utf8', &getURL($tvgurlRoot . "Listingsweb/ws/rest/schedules/$zlineupId/start/$tvgstart/duration/$duration"));
210
      &wbf($fn, Compress::Zlib::memGzip($rc));
211
    }
212
    &pout("[" . ($count+1) . "/" . "$maxCount] Parsing: $fn\n");
213
    &parseTVGGrid($fn);
214

    
215
    if (defined($options{T}) && $tba) {
216
      &pout("Deleting: $fn (contains \"$sTBA\")\n");
217
      unlink($fn);
218
    }
219
    if ($exp) {
220
      &pout("Deleting: $fn (expired)\n");
221
      unlink($fn);
222
    }
223
    $exp = 0;
224
    $tba = 0;
225
    $ms += ($gridHours * 3600 * 1000); 
226
  } 
227

    
228
} else {
229

    
230
  $gridHours = 6;
231
  $maxCount = $days * (24 / $gridHours);
232
  $ncCount = $maxCount - ($ncdays * (24 / $gridHours));
233
  $offset = $start * 3600 * 24 * 1000;
234
  $ncsCount = $ncsdays * (24 / $gridHours);
235
  $ms = &hourToMillis() + $offset;
236
  for ($count=0; $count < $maxCount; $count++) {
237
    if ($count == 0) { 
238
      $XTVD_startTime = $ms;
239
    } elsif ($count == $maxCount - 1) { 
240
      $XTVD_endTime = $ms + ($gridHours * 3600000) - 1;
241
    }
242

    
243
    $fn = "$cacheDir/$ms\.html\.gz";
244
    if (! -e $fn || $count >= $ncCount || $count < $ncsCount) {
245
      $params = "";
246
      $params .= "&lineupId=$zlineupId" if defined($zlineupId);
247
      $params .= "&zipcode=$zipcode" if defined($zipcode);
248
      $rc = Encode::encode('utf8', &getURL($urlRoot . "ZCGrid.do?isDescriptionOn=true&fromTimeInMillis=$ms$params&aid=tvschedule") );
249
      &wbf($fn, Compress::Zlib::memGzip($rc));
250
    }
251
    &pout("[" . ($count+1) . "/" . "$maxCount] Parsing: $fn\n");
252
    &parseGrid($fn);
253

    
254
    if ($count == 0) { #ugly
255
      $gridHours = $gridtimes / 2;
256
      if ($gridHours < 1) {
257
        &perr("Error: The grid is not being displayed, try logging in to the zap2it website\n");
258
        &perr("Deleting: $fn\n");
259
        unlink($fn);
260
        exit;
261
      } elsif ($gridHours != 6) {
262
        &pout("Notice: \"Six hour grid\" not selected in zap2it preferences, adjusting to $gridHours hour grid\n");
263
      } # reset anyway in case of cache mismatch
264
      $maxCount = $days * (24 / $gridHours);
265
      $ncCount = $maxCount - ($ncdays * (24 / $gridHours));
266
      $ncsCount = $ncsdays * (24 / $gridHours);
267
    } elsif ($mismatch == 0) {
268
      if ($gridHours != $gridtimes / 2) {
269
        &pout("Notice: Grid mismatch in cache, ignoring cache & restarting.\n");
270
        $mismatch = 1;
271
        $ncsdays = 99;
272
        $ncsCount = $ncsdays * 24;
273
        $ms = &hourToMillis() + $offset;
274
        $count = -1;
275
        $gridtimes = 0;
276
        next; #skip ms incr
277
      }
278
    }
279
    $gridtimes = 0;
280

    
281
    if (defined($options{T}) && $tba) {
282
      &pout("Deleting: $fn (contains \"$sTBA\")\n");
283
      unlink($fn);
284
    }
285
    if ($exp) {
286
      &pout("Deleting: $fn (expired)\n");
287
      unlink($fn);
288
    }
289
    $exp = 0;
290
    $tba = 0;
291
    $ms += ($gridHours * 3600 * 1000);
292
  } 
293

    
294
}
295
my $s2 = time();
296

    
297
&pout("Downloaded $tb bytes in $treq http requests.\n") if $tb > 0;
298
&pout("Expired programs: $expired\n") if $expired > 0;
299
&pout("Writing XML file: $outFile\n");
300
open($FH, ">$outFile");
301
my $enc = 'ISO-8859-1';
302
if (defined($options{U})) {
303
  $enc = 'UTF-8';
304
} 
305
if ($outputXTVD) {
306
  &printHeaderXTVD($FH, $enc);
307
  &printStationsXTVD($FH);
308
  &printLineupsXTVD($FH);
309
  &printSchedulesXTVD($FH);
310
  &printProgramsXTVD($FH);
311
  &printGenresXTVD($FH);
312
  &printFooterXTVD($FH);
313
} else {
314
  &printHeader($FH, $enc);
315
  &printChannels($FH);
316
  if (defined($includeXMLTV)) {
317
    &pout("Reading XML file: $includeXMLTV\n");
318
    &incXML("<channel","<programme", $FH);
319
  } 
320
  &printProgrammes($FH);
321
  &incXML("<programme","</tv", $FH) if defined($includeXMLTV);
322
  &printFooter($FH);
323
}
324

    
325
close($FH);
326

    
327
my $ts = 0;
328
for my $station (keys %stations ) {
329
  $ts += scalar (keys %{$schedule{$station}})
330
}
331
my $s3 = time();
332
&pout("Completed in " . ( $s3 - $s1 ) . "s (Parse: " . ( $s2 - $s1 ) . "s) " . keys(%stations) . " stations, " . keys(%programs) . " programs, $ts scheduled.\n");
333

    
334
if (defined($options{w})) {
335
  print "Press ENTER to exit:";
336
  <STDIN>;
337
} else {
338
  sleep(3) if ($^O eq 'MSWin32');
339
}
340

    
341
exit 0;
342

    
343
sub incXML {
344
  my ($st, $en, $FH) = @_;
345
  open($XF, "<$includeXMLTV");
346
  while (<$XF>) {
347
    if (/^\s*$st/../^\s*$en/) {
348
      print $FH $_ unless /^\s*$en/
349
    }
350
  }
351
  close($XF);
352
}
353

    
354
sub pout {
355
  print @_ if !defined $options{q};
356
}
357

    
358
sub perr {
359
  warn @_;
360
}
361

    
362
sub rtrim {
363
  my $s = shift;
364
  $s =~ s/\s+$//;
365
  return $s;
366
}
367

    
368
sub trim {
369
  my $s = shift;
370
  $s =~ s/^\s+//;
371
  $s =~ s/\s+$//;
372
  return $s;
373
}
374

    
375
sub trim2 {
376
  my $s = &trim(shift);
377
  $s =~ s/[^\w\s\(\)\,]//gsi;
378
  $s =~ s/\s+/ /gsi; 
379
  return $s;
380
}
381

    
382
sub _rtrim3 {
383
  my $s = shift;
384
  return substr($s, 0, length($s)-3);
385
}
386

    
387
sub convTime {
388
  my $t = shift;
389
  $t += $shiftMinutes * 60 * 1000;
390
  return strftime "%Y%m%d%H%M%S", localtime(&_rtrim3($t));
391
}
392

    
393
sub convTimeXTVD {
394
  my $t = shift;
395
  $t += $shiftMinutes * 60 * 1000;
396
  return strftime "%Y-%m-%dT%H:%M:%SZ", gmtime(&_rtrim3($t));
397
}
398

    
399
sub convDateLocal {
400
  return strftime "%Y%m%d", localtime(&_rtrim3(shift));
401
}
402

    
403
sub convDateLocalXTVD {
404
  return strftime "%Y-%m-%d", localtime(&_rtrim3(shift));
405
}
406

    
407
sub convDurationXTVD {
408
  my $duration = shift; 
409
  my $hour = int($duration / 3600000);
410
  my $minutes = int(($duration - ($hour * 3600000)) / 60000);
411
  return sprintf("PT%02dH%02dM", $hour, $minutes);
412
}
413

    
414
sub appendAsterisk {
415
  my ($title, $station, $s) = @_;
416
  if (defined($options{A})) {
417
    if (($options{A} =~ "new" && defined($schedule{$station}{$s}{new}))
418
      || ($options{A} =~ "live" && defined($schedule{$station}{$s}{live}))) {
419
      $title .= " *";
420
    }
421
  }
422
  return $title;
423
}
424

    
425
sub stationToChannel {
426
  my $s = shift;
427
  if (defined($options{z})) {
428
    return sprintf("I%s.%s.tvguide.com", $stations{$s}{number},$stations{$s}{stnNum});
429
  } elsif (defined($options{O})) {
430
    return sprintf("C%s%s.zap2it.com",$stations{$s}{number},lc($stations{$s}{name}));
431
  }
432
  return sprintf("I%s.labs.zap2it.com", $stations{$s}{stnNum});
433
}
434

    
435
sub sortChan {
436
  if (defined($stations{$a}{order}) && defined($stations{$b}{order})) {
437
    return $stations{$a}{order} <=> $stations{$b}{order};
438
  } else {
439
    return $stations{$a}{name} cmp $stations{$b}{name};
440
  }
441
}
442

    
443
sub enc {
444
  my $t = shift;
445
  if (!defined($options{U})) {$t = Encode::decode('utf8', $t);}
446
  if (!defined($options{E}) || $options{E} =~ /amp/) {$t =~ s/&/&amp;/gs;}
447
  if (!defined($options{E}) || $options{E} =~ /quot/) {$t =~ s/"/&quot;/gs;}
448
  if (!defined($options{E}) || $options{E} =~ /apos/) {$t =~ s/'/&apos;/gs;}
449
  if (!defined($options{E}) || $options{E} =~ /lt/) {$t =~ s/</&lt;/gs;}
450
  if (!defined($options{E}) || $options{E} =~ /gt/) {$t =~ s/>/&gt;/gs;}
451
  if (defined($options{e})) {
452
    $t =~ s/([^\x20-\x7F])/'&#' . ord($1) . ';'/gse;
453
  }
454
  return $t;
455
}
456

    
457
sub printHeader {
458
  my ($FH, $enc) = @_;
459
  print $FH "<?xml version=\"1.0\" encoding=\"$enc\"?>\n";
460
  print $FH "<!DOCTYPE tv SYSTEM \"xmltv.dtd\">\n\n";
461
  if (defined($options{z})) {
462
    print $FH "<tv source-info-url=\"http://tvguide.com/\" source-info-name=\"tvguide.com\"";
463
  } else {
464
    print $FH "<tv source-info-url=\"http://tvschedule.zap2it.com/\" source-info-name=\"zap2it.com\"";
465
  }
466
  print $FH " generator-info-name=\"zap2xml\" generator-info-url=\"zap2xml\@gmail.com\">\n";
467
}
468

    
469
sub printFooter {
470
  my $FH = shift;
471
  print $FH "</tv>\n";
472
} 
473

    
474
sub printChannels {
475
  my $FH = shift;
476
  for my $key ( sort sortChan keys %stations ) {
477
    $sname = &enc($stations{$key}{name});
478
    $fname = &enc($stations{$key}{fullname});
479
    $snum = $stations{$key}{number};
480
    print $FH "\t<channel id=\"" . &stationToChannel($key) . "\">\n";
481
    print $FH "\t\t<display-name>" . $sname . "</display-name>\n" if defined($options{F}) && defined($sname);
482
    if (defined($snum)) {
483
      &copyLogo($key);
484
      print $FH "\t\t<display-name>" . $snum . " " . $sname . "</display-name>\n";
485
      print $FH "\t\t<display-name>" . $snum . "</display-name>\n";
486
    }
487
    print $FH "\t\t<display-name>" . $sname . "</display-name>\n" if !defined($options{F}) && defined($sname);
488
    print $FH "\t\t<display-name>" . $fname . "</display-name>\n" if (defined($fname));
489
    if (defined($stations{$key}{logoURL})) {
490
      print $FH "\t\t<icon src=\"" . $stations{$key}{logoURL} . "\" />\n";
491
    }
492
    print $FH "\t</channel>\n";
493
  }
494
}
495

    
496
sub printProgrammes {
497
  my $FH = shift;
498
  for my $station ( sort sortChan keys %stations ) {
499
    my $i = 0; 
500
    my @keyArray = sort { $schedule{$station}{$a}{time} cmp $schedule{$station}{$b}{time} } keys %{$schedule{$station}};
501
    foreach $s (@keyArray) {
502
      if ($#keyArray <= $i && !defined($schedule{$station}{$s}{endtime})) {
503
        delete $schedule{$station}{$s};
504
        next; 
505
      } 
506
      my $p = $schedule{$station}{$s}{program};
507
      my $startTime = &convTime($schedule{$station}{$s}{time});
508
      my $startTZ = &timezone($schedule{$station}{$s}{time});
509
      my $endTime;
510
      if (defined($schedule{$station}{$s}{endtime})) {
511
        $endTime = $schedule{$station}{$s}{endtime};
512
      } else {
513
        $endTime = $schedule{$station}{$keyArray[$i+1]}{time};
514
      }
515

    
516
      my $stopTime = &convTime($endTime);
517
      my $stopTZ = &timezone($endTime);
518

    
519
      print $FH "\t<programme start=\"$startTime $startTZ\" stop=\"$stopTime $stopTZ\" channel=\"" . &stationToChannel($schedule{$station}{$s}{station}) . "\">\n";
520
      if (defined($programs{$p}{title})) {
521
        my $title = &enc($programs{$p}{title});
522
        $title = &appendAsterisk($title, $station, $s);
523
        print $FH "\t\t<title lang=\"$lang\">" . $title . "</title>\n";
524
      } 
525

    
526
      if (defined($programs{$p}{episode}) || (defined($options{M}) && defined($programs{$p}{movie_year}))) {
527
        print $FH "\t\t<sub-title lang=\"$lang\">";
528
          if (defined($programs{$p}{episode})) {
529
             print $FH &enc($programs{$p}{episode});
530
          } else {
531
             print $FH "Movie (" . $programs{$p}{movie_year} . ")";
532
          } 
533
        print $FH "</sub-title>\n"
534
      }
535

    
536
      print $FH "\t\t<desc lang=\"$lang\">" . &enc($programs{$p}{description}) . "</desc>\n" if defined($programs{$p}{description});
537

    
538
      if (defined($programs{$p}{credits})) {
539
        print $FH "\t\t<credits>\n";
540
        foreach my $g (sort { $programs{$p}{credits}{$a} <=> $programs{$p}{credits}{$b} } keys %{$programs{$p}{credits}} ) {
541
          print $FH "\t\t\t<actor>" . &enc($g) . "</actor>\n";
542
        }
543
        print $FH "\t\t</credits>\n";
544
      }
545
  
546
      my $date;
547
      if (defined($programs{$p}{movie_year})) {
548
        $date = $programs{$p}{movie_year};
549
      } elsif (defined($programs{$p}{originalAirDate}) && $p =~ /^EP|^\d/) {
550
        $date = &convDateLocal($programs{$p}{originalAirDate});
551
      }
552

    
553
      print $FH "\t\t<date>$date</date>\n" if defined($date);
554
      if (defined($programs{$p}{genres})) {
555
        foreach my $g (sort { $programs{$p}{genres}{$a} <=> $programs{$p}{genres}{$b} } keys %{$programs{$p}{genres}} ) {
556
          print $FH "\t\t<category lang=\"$lang\">" . &enc(ucfirst($g)) . "</category>\n";
557
        }
558
      }
559

    
560
      if (defined($programs{$p}{imageUrl})) {
561
        print $FH "\t\t<icon src=\"" . $programs{$p}{imageUrl} . "\" />\n";
562
      }
563

    
564
      if (defined($programs{$p}{url})) {
565
        print $FH "\t\t<url>" . $programs{$p}{url} . "</url>\n";
566
      }
567

    
568
      my $xs;
569
      my $xe;
570

    
571
      if (defined($programs{$p}{seasonNum}) && defined($programs{$p}{episodeNum})) {
572
        my $s = $programs{$p}{seasonNum};
573
        my $sf = sprintf("S%0*d", &max(2, length($s)), $s);
574
        my $e = $programs{$p}{episodeNum};
575
        my $ef = sprintf("E%0*d", &max(2, length($e)), $e);
576

    
577
        $xs = int($s) - 1;
578
        $xe = int($e) - 1;
579

    
580
        if ($s > 0 || $e > 0) {
581
          print $FH "\t\t<episode-num system=\"common\">" . $sf . $ef . "</episode-num>\n";
582
        }
583
      }
584

    
585
      $dd_prog_id = $p;
586
      if ( $dd_prog_id =~ /^(..\d{8})(\d{4})/ ) {
587
        $dd_prog_id = sprintf("%s.%s",$1,$2);
588
        print $FH "\t\t<episode-num system=\"dd_progid\">" . $dd_prog_id  . "</episode-num>\n";
589
      }
590

    
591
      if (defined($xs) && defined($xe) && $xs >= 0 && $xe >= 0) {
592
        print $FH "\t\t<episode-num system=\"xmltv_ns\">" . $xs . "." . $xe . ".</episode-num>\n";
593
      }
594

    
595
      if (defined($schedule{$station}{$s}{quality})) {
596
        print $FH "\t\t<video>\n";
597
        print $FH "\t\t\t<aspect>16:9</aspect>\n";
598
        print $FH "\t\t\t<quality>HDTV</quality>\n";
599
        print $FH "\t\t</video>\n";
600
      }
601
      my $new = defined($schedule{$station}{$s}{new});
602
      my $live = defined($schedule{$station}{$s}{live});
603
      my $cc = defined($schedule{$station}{$s}{cc});
604

    
605
      if (! $new && ! $live && $p =~ /^EP|^SH|^\d/) {
606
        print $FH "\t\t<previously-shown ";
607
        if (defined($programs{$p}{originalAirDate})) {
608
          $date = &convDateLocal($programs{$p}{originalAirDate});
609
          print $FH "start=\"" . $date . "000000\" ";
610
        }
611
        print $FH "/>\n";
612
      }
613

    
614
      if (defined($schedule{$station}{$s}{premiere})) {
615
        print $FH "\t\t<premiere>" . $schedule{$station}{$s}{premiere} . "</premiere>\n";
616
      }
617

    
618
      if (defined($schedule{$station}{$s}{finale})) {
619
        print $FH "\t\t<last-chance>" . $schedule{$station}{$s}{finale} . "</last-chance>\n";
620
      }
621

    
622
      print $FH "\t\t<new />\n" if $new;
623
      # not part of XMLTV format yet?
624
      print $FH "\t\t<live />\n" if (defined($options{L}) && $live);
625
      print $FH "\t\t<subtitles type=\"teletext\" />\n" if $cc;
626

    
627
      if (defined($programs{$p}{rating})) {
628
        print $FH "\t\t<rating>\n\t\t\t<value>" . $programs{$p}{rating} . "</value>\n\t\t</rating>\n"
629
      }
630

    
631
      if (defined($programs{$p}{starRating})) {
632
        print $FH "\t\t<star-rating>\n\t\t\t<value>" . $programs{$p}{starRating} . "/4</value>\n\t\t</star-rating>\n";
633
      }
634
      print $FH "\t</programme>\n";
635
      $i++;
636
    }
637
  }
638
}
639

    
640
sub printHeaderXTVD {
641
  my ($FH, $enc) = @_;
642
  print $FH "<?xml version='1.0' encoding='$enc'?>\n";
643
  print $FH "<xtvd from='" . &convTimeXTVD($XTVD_startTime) . "' to='" . &convTimeXTVD($XTVD_endTime)  . "' schemaVersion='1.3' xmlns='urn:TMSWebServices' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='urn:TMSWebServices http://docs.tms.tribune.com/tech/xml/schemas/tmsxtvd.xsd'>\n";
644
}
645

    
646
sub printFooterXTVD {
647
  my $FH = shift;
648
  print $FH "</xtvd>\n";
649
} 
650

    
651
sub printStationsXTVD {
652
  my $FH = shift;
653
  print $FH "<stations>\n";
654
  for my $key ( sort sortChan keys %stations ) {
655
    print $FH "\t<station id='" . $stations{$key}{stnNum} . "'>\n";
656
    if (defined($stations{$key}{number})) {
657
      $sname = &enc($stations{$key}{name});
658
      print $FH "\t\t<callSign>" . $sname . "</callSign>\n";
659
      print $FH "\t\t<name>" . $sname . "</name>\n";
660
      print $FH "\t\t<fccChannelNumber>" . $stations{$key}{number} . "</fccChannelNumber>\n";
661
      if (defined($stations{$key}{logo}) && $stations{$key}{logo} =~ /_affiliate/i) {
662
        $affiliate = $stations{$key}{logo};
663
        $affiliate =~ s/(.*)\_.*/uc($1)/e;
664
        print $FH "\t\t<affiliate>" . $affiliate . " Affiliate</affiliate>\n";
665
      }
666
      &copyLogo($key);
667
    }
668
    print $FH "\t</station>\n";
669
  }
670
  print $FH "</stations>\n";
671
}
672

    
673
sub printLineupsXTVD {
674
  my $FH = shift;
675
  print $FH "<lineups>\n";
676
  print $FH "\t<lineup id='$lineupId' name='$lineupname' location='$lineuplocation' type='$lineuptype' postalCode='$postalcode'>\n";
677
  for my $key ( sort sortChan keys %stations ) {
678
    if (defined($stations{$key}{number})) {
679
      print $FH "\t<map station='" . $stations{$key}{stnNum} . "' channel='" . $stations{$key}{number} . "'></map>\n";
680
    }
681
  }
682
  print $FH "\t</lineup>\n";
683
  print $FH "</lineups>\n";
684
}
685

    
686
sub printSchedulesXTVD {
687
  my $FH = shift;
688
  print $FH "<schedules>\n";
689
  for my $station ( sort sortChan keys %stations ) {
690
    my $i = 0; 
691
    my @keyArray = sort { $schedule{$station}{$a}{time} cmp $schedule{$station}{$b}{time} } keys %{$schedule{$station}};
692
    foreach $s (@keyArray) {
693
      if ($#keyArray <= $i) {
694
        delete $schedule{$station}{$s};
695
        next; 
696
      } 
697
      my $p = $schedule{$station}{$s}{program};
698
      my $startTime = &convTimeXTVD($schedule{$station}{$s}{time});
699
      my $stopTime = &convTimeXTVD($schedule{$station}{$keyArray[$i+1]}{time});
700
      my $duration = &convDurationXTVD($schedule{$station}{$keyArray[$i+1]}{time} - $schedule{$station}{$s}{time});
701

    
702
      print $FH "\t<schedule program='$p' station='" . $stations{$station}{stnNum} . "' time='$startTime' duration='$duration'"; 
703
      print $FH " hdtv='true' " if (defined($schedule{$station}{$s}{quality}));
704
      print $FH " new='true' " if (defined($schedule{$station}{$s}{new}) || defined($schedule{$station}{$s}{live}));
705
      print $FH "/>\n";
706
      $i++;
707
    }
708
  }
709
  print $FH "</schedules>\n";
710
}
711

    
712
sub printProgramsXTVD {
713
  my $FH = shift;
714
  print $FH "<programs>\n";
715
  foreach $p (keys %programs) {
716
      print $FH "\t<program id='" . $p . "'>\n";
717
      print $FH "\t\t<title>" . &enc($programs{$p}{title}) . "</title>\n" if defined($programs{$p}{title});
718
      print $FH "\t\t<subtitle>" . &enc($programs{$p}{episode}) . "</subtitle>\n" if defined($programs{$p}{episode});
719
      print $FH "\t\t<description>" . &enc($programs{$p}{description}) . "</description>\n" if defined($programs{$p}{description});
720
      
721
      if (defined($programs{$p}{movie_year})) {
722
        print $FH "\t\t<year>" . $programs{$p}{movie_year} . "</year>\n";
723
      } else { #Guess
724
        my $showType = "Series"; 
725
        if ($programs{$p}{title} =~ /Paid Programming/i) {
726
          $showType = "Paid Programming";
727
        } 
728
        print $FH "\t\t<showType>$showType</showType>\n"; 
729
        print $FH "\t\t<series>EP" . substr($p,2,8) . "</series>\n"; 
730
        print $FH "\t\t<originalAirDate>" . &convDateLocalXTVD($programs{$p}{originalAirDate}) . "</originalAirDate>\n" if defined($programs{$p}{originalAirDate});
731
      }
732
      print $FH "\t</program>\n";
733
  }
734
  print $FH "</programs>\n";
735
}
736

    
737
sub printGenresXTVD {
738
  my $FH = shift;
739
  print $FH "<genres>\n";
740
  foreach $p (keys %programs) {
741
    if (defined($programs{$p}{genres}) && $programs{$p}{genres}{movie} != 1) {
742
      print $FH "\t<programGenre program='" . $p . "'>\n";
743
      foreach my $g (keys %{$programs{$p}{genres}}) {
744
        print $FH "\t\t<genre>\n";
745
        print $FH "\t\t\t<class>" . &enc(ucfirst($g)) . "</class>\n";
746
        print $FH "\t\t\t<relevance>0</relevance>\n";
747
        print $FH "\t\t</genre>\n";
748
      }
749
      print $FH "\t</programGenre>\n";
750
    }
751
  }
752
  print $FH "</genres>\n";
753
}
754

    
755
sub loginTVG {
756
  my $r = $ua->get($tvgurl . 'user/_modal/');
757
  if ($r->is_success) {
758
    my $str = $r->decoded_content;
759
    if ($str =~ /<input.+name=\"_token\".+?value=\"(.*?)\"/is) {
760
      $token = $1;
761
      if ($userEmail ne '' && $password ne '') {
762
        my $rc = 0;
763
        while ($rc++ < $retries) {
764
          my $r = $ua->post($tvgurl . 'user/attempt/', 
765
            { 
766
              _token => $token,
767
              email => $userEmail, 
768
              password => $password,
769
            }, 'X-Requested-With' => 'XMLHttpRequest'
770
          ); 
771

    
772
          $dc = Encode::encode('utf8', $r->decoded_content( raise_error => 1 ));
773
          if ($dc =~ /success/) {
774
            $ua->cookie_jar->scan(sub { if ($_[1] eq "ServiceID") { $zlineupId = $_[2]; }; }); 
775
            if (!defined($options{a})) {
776
              my $r = $ua->get($tvgurl . "user/favorites/?provider=$zlineupId",'X-Requested-With' => 'XMLHttpRequest'); 
777
              $dc = Encode::encode('utf8', $r->decoded_content( raise_error => 1 ));
778
              if ($dc =~ /\{\"code\":200/) {
779
                &parseTVGFavs($dc);
780
              } 
781
            }
782
            return $dc; 
783
          } else {
784
            &pout("[Attempt $rc] " . $dc . "\n");
785
            sleep ($sleeptime + 1);
786
          }
787
        }
788
        die "Failed to login within $retries retries.\n";
789
      }
790
    } else {
791
      die "Login token not found\n";
792
    }
793
  }
794
}
795

    
796
sub loginZAP {
797
  my $rc = 0;
798
  while ($rc++ < $retries) {
799
    my $r = $ua->post($urlRoot . 'ZCLogin.do', 
800
      { 
801
        username => $userEmail, 
802
        password => $password,
803
        xhr => 'true', # xml
804
      }
805
    ); 
806
 
807
    $dc = Encode::encode('utf8', $r->decoded_content( raise_error => 1 ));
808
    if ($dc =~ /success,$userEmail/) {
809
      return $dc; 
810
    } else {
811
      &pout("[Attempt $rc] " . $dc . "\n");
812
      sleep ($sleeptime + 1);
813
    }
814
  }
815
  die "Failed to login within $retries retries.\n";
816
}
817

    
818
sub login {
819
  if (!defined($userEmail) || $userEmail eq '' || !defined($password) || $password eq '') {
820
    if (!defined($zlineupId)) {
821
      die "Unable to login: Unspecified username or password.\n"
822
    }
823
  }
824

    
825
  if (!defined($ua)) {
826
    $ua = new LWP::UserAgent; 
827
    $ua->cookie_jar(HTTP::Cookies->new);
828
    $ua->proxy('http', $proxy) if defined($proxy);
829
    $ua->agent('Mozilla/4.0');
830
    $ua->default_headers->push_header('Accept-Encoding' => 'gzip, deflate');
831
  }
832

    
833
  if ($userEmail ne '' && $password ne '') {
834
    &pout("Logging in as \"$userEmail\" (" . localtime . ")\n");
835
    if (defined($options{z})) {
836
      &loginTVG();
837
    } else {
838
      &loginZAP();
839
    }
840
  } else {
841
    &pout("Connecting with lineupId \"$zlineupId\" (" . localtime . ")\n");
842
  }
843
}
844

    
845
sub getURL {
846
  my $url = shift;
847
  &login() if !defined($ua);
848

    
849
  my $rc = 0;
850
  while ($rc++ < $retries) {
851
    &pout("Getting: $url\n");
852
    sleep $sleeptime; # do these rapid requests flood servers?
853
    $treq++;
854
    my $r = $ua->get($url);
855
    if ($r->is_success) {
856
      $tb += length($r->content);
857
      return $r->decoded_content( raise_error => 1 );
858
    } else {
859
      &perr("[Attempt $rc] " . $r->status_line . "\n");
860
      sleep ($sleeptime + 1);
861
    }
862
  }
863
  die "Failed to download within $retries retries.\n";
864
}
865

    
866
sub wbf {
867
  my($f, $s) = @_;
868
  open(FO, ">$f");
869
  binmode(FO);
870
  print FO $s;
871
  close(FO);
872
}
873

    
874
sub copyLogo {
875
  my $key = shift;
876
  if (defined($iconDir) && defined($stations{$key}{logo})) {
877
    my $num = $stations{$key}{number};
878
    my $src = "$iconDir/" . $stations{$key}{logo} . $stations{$key}{logoExt};
879
    my $dest1 = "$iconDir/$num" . $stations{$key}{logoExt};
880
    my $dest2 = "$iconDir/$num " . $stations{$key}{name} . $stations{$key}{logoExt};
881
    copy($src, $dest1);
882
    copy($src, $dest2);
883
  }
884
}
885

    
886
sub handleLogo {
887
  my $url = shift;
888
  if (! -d $iconDir) {
889
    mkdir($iconDir) or die "Can't mkdir: $!\n";
890
  }
891
  ($n,$_,$s) = fileparse($url, qr"\..*");
892
  $stations{$cs}{logo} = $n;
893
  $stations{$cs}{logoExt} = $s;
894
  $stations{$cs}{logoURL} = $url;
895
  $f = $iconDir . "/" . $n . $s;
896
  if (! -e $f) { &wbf($f, &getURL($url)); }
897
}
898

    
899
sub setOriginalAirDate {
900
  if (substr($cp,10,4) ne '0000') {
901
    if (!defined($programs{$cp}{originalAirDate})
902
        || ($schedule{$cs}{$sch}{time} < $programs{$cp}{originalAirDate})) {
903
      $programs{$cp}{originalAirDate} = $schedule{$cs}{$sch}{time};
904
    }
905
  }
906
}
907

    
908
sub on_th {
909
  my($self, $tag, $attr) = @_;
910
  if (defined($attr->{class})) {
911
    if ($attr->{class} =~ /zc-st/) {
912
      $inStationTd = 1;
913
    }
914
  } 
915
}
916

    
917
sub on_td {
918
  my($self, $tag, $attr) = @_;
919
  if (defined($attr->{class})) {
920
    if ($attr->{class} =~ /zc-pg/) {
921
      if (defined($attr->{onclick})) {
922
        $cs = $rcs;
923
        $oc = $attr->{onclick};
924
        $oc =~ s/.*\((.*)\).*/$1/s;
925
        @a = split(/,/, $oc);
926
        $cp = $a[1];
927
        $cp =~ s/'//g;
928
        $sch = $a[2];
929
        if (length($cp) == 0) {
930
          $cp = $cs = $sch = -1;
931
          $expired++;
932
          $exp = 1;
933
        }
934
        $schedule{$cs}{$sch}{time} = $sch;
935
        $schedule{$cs}{$sch}{program} = $cp;
936
        $schedule{$cs}{$sch}{station} = $cs;
937

    
938
        if ($attr->{class} =~ /zc-g-C/) { $programs{$cp}{genres}{children} = 1 }
939
        elsif ($attr->{class} =~ /zc-g-N/) { $programs{$cp}{genres}{news} = 1 }
940
        elsif ($attr->{class} =~ /zc-g-M/) { $programs{$cp}{genres}{movie} = 1 }
941
        elsif ($attr->{class} =~ /zc-g-S/) { $programs{$cp}{genres}{sports} = 1 }
942

    
943
        if ($cp =~ /^MV/) { $programs{$cp}{genres}{movie} = 1 }
944
        elsif ($cp =~ /^SP/) { $programs{$cp}{genres}{sports} = 1 }
945
        elsif ($cp =~ /^EP/) { $programs{$cp}{genres}{series} = 9 }
946
        elsif ($cp =~ /^SH/ && $options{j}) { $programs{$cp}{genres}{series} = 9 }
947

    
948
        if ($cp != -1) {
949
          if (defined $options{D}) {
950
            my $fn = "$cacheDir/$cp\.js\.gz";
951
            if (! -e $fn) {
952
              $rc = Encode::encode('utf8', &getURL($urlRoot . "gridDetailService?pgmId=$cp") );
953
              &wbf($fn, Compress::Zlib::memGzip($rc));
954
            }
955
            &pout("[D] Parsing: $cp\n");
956
            &parseJSOND($fn);
957
          }
958
          if (defined $options{I}) {
959
            my $fn = "$cacheDir/I$cp\.js\.gz";
960
            if (! -e $fn) {
961
              $rc = Encode::encode('utf8', &getURL($urlRoot . "gridDetailService?rtype=pgmimg&pgmId=$cp") );
962
              &wbf($fn, Compress::Zlib::memGzip($rc));
963
            }
964
            &pout("[I] Parsing: $cp\n");
965
            &parseJSONI($fn);
966
          }
967
        } 
968
      }
969
    } elsif ($attr->{class} =~ /zc-st/) {
970
      $inStationTd = 1;
971
    }
972
  } 
973
}
974

    
975
sub handleTags {
976
  my $text = shift;
977
  if ($text =~ /LIVE/) {
978
    $schedule{$cs}{$sch}{live} = 'Live';
979
    &setOriginalAirDate();
980
  } elsif ($text =~ /HD/) {
981
    $schedule{$cs}{$sch}{quality} = 'HD';
982
  } elsif ($text =~ /NEW/) {
983
    $schedule{$cs}{$sch}{new} = 'New';
984
    &setOriginalAirDate();
985
  }
986
}
987

    
988
sub on_li {
989
  my($self, $tag, $attr) = @_;
990
  if ($attr->{class} =~ /zc-ic-ne/) {
991
    $schedule{$cs}{$sch}{new} = 'New';
992
    &setOriginalAirDate();
993
  } elsif ($attr->{class} =~ /zc-ic-cc/) {
994
    $schedule{$cs}{$sch}{cc} = 'CC';
995
  } elsif ($attr->{class} =~ /zc-ic-tvratings/) {
996
    $self->handler(text => sub { $programs{$cp}{rating} = &trim2(shift); }, "dtext");
997
  } elsif ($attr->{class} =~ /zc-ic/) { 
998
    $self->handler(text => sub { &handleTags(shift); }, "dtext");
999
  } elsif ($attr->{class} =~ /zc-icons-live/) {
1000
    $schedule{$cs}{$sch}{live} = 'Live';
1001
    &setOriginalAirDate();
1002
  } elsif ($attr->{class} =~ /zc-icons-hd/) {
1003
    $schedule{$cs}{$sch}{quality} = 'HD';
1004
  }
1005
}
1006

    
1007
sub on_img {
1008
  my($self, $tag, $attr) = @_;
1009
  if ($inIcons) {
1010
    if ($attr->{alt} =~ /Live/) {
1011
      $schedule{$cs}{$sch}{live} = 'Live';
1012
      &setOriginalAirDate();
1013
    } elsif ($attr->{alt} =~ /New/) {
1014
      $schedule{$cs}{$sch}{new} = 'New';
1015
      &setOriginalAirDate();
1016
    } elsif ($attr->{alt} =~ /HD/ || $attr->{alt} =~ /High Definition/ 
1017
      || $attr->{src} =~ /video-hd/ || $attr->{src} =~ /video-ahd/) {
1018
      $schedule{$cs}{$sch}{quality} = 'HD';
1019
    } 
1020
  } elsif ($inStationTd && $attr->{alt} =~ /Logo/) {
1021
    &handleLogo($attr->{src}) if defined($iconDir);
1022
  }
1023
}
1024

    
1025
sub on_a {
1026
  my($self, $tag, $attr) = @_;
1027
  if ($attr->{class} =~ /zc-pg-t/) {
1028
    $self->handler(text => sub { $programs{$cp}{title} = (shift); $tba = 1 if $programs{$cp}{title} =~ /$sTBA/i;}, "dtext");
1029
  } elsif ($inStationTd) {
1030
    my $tcs = $attr->{href};
1031
    $tcs =~ s/.*stnNum=(\w+).*/$1/;
1032
    if (! ($tcs =~ /stnNum/)) {
1033
      $cs = $rcs = $tcs;
1034
    }
1035
    if (!defined($stations{$cs}{stnNum})) {
1036
      $stations{$cs}{stnNum} = $cs;
1037
    }
1038
    if (!defined($stations{$cs}{number})) {
1039
      my $tnum = uri_unescape($attr->{href});
1040
      $tnum =~ s/\s//gs;
1041
      $tnum =~ s/.*channel=([.\w]+).*/$1/;
1042
      $stations{$cs}{number} = $tnum if ! ($tnum =~ /channel=/);
1043
      if (!defined($stations{$cs}{order})) {
1044
        if (defined($options{b})) {
1045
          $stations{$cs}{order} = $coNum++;
1046
        } else {
1047
          $stations{$cs}{order} = $stations{$cs}{number};
1048
        } 
1049
      }
1050
    }
1051
    if (!defined($postalcode) && $attr->{href} =~ /zipcode/) {
1052
      $postalcode = $attr->{href};
1053
      $postalcode =~ s/.*zipcode=(\w+).*/$1/;
1054
    }
1055
    if (!defined($lineupId) && $attr->{href} =~ /lineup/) {
1056
      $lineupId = $attr->{href};
1057
      $lineupId =~ s/.*lineupId=(.*?)&.*/uri_unescape($1)/e;
1058
    }
1059
    if ($count == 0 && $inStationLogo && $iconDir) {
1060
      my $fn = "$cacheDir/STNNUM$cs\.html\.gz";
1061
      if (! -e $fn) {
1062
        $rc = Encode::encode('utf8', &getURL($attr->{href}) );
1063
        &wbf($fn, Compress::Zlib::memGzip($rc));
1064
      }
1065
      &pout("[STNNUM] Parsing: $cs\n");
1066
      &parseSTNNUM($fn);
1067
    }
1068
  }
1069
}
1070

    
1071
sub on_p {
1072
  my($self, $tag, $attr) = @_;
1073
  if (defined($attr->{class}) && ($attr->{class} =~ /zc-pg-d/)) {
1074
    $self->handler(text => sub { $d = &trim(shift); $programs{$cp}{description} = $d if length($d) && !defined($programs{$cp}{description}) }, "dtext");
1075
  }
1076
}
1077

    
1078
sub on_div {
1079
  my($self, $tag, $attr) = @_;
1080
  if (defined($attr->{class}) && ($attr->{class} =~ /zc-icons/)) {
1081
    $inIcons = 1;
1082
  }
1083
  if (defined($attr->{class}) && ($attr->{class} =~ /zc-tn-c/)) {
1084
    $self->handler(text => sub { $gridtimes = 0; }, "dtext");
1085
  }
1086
  if (defined($attr->{class}) && ($attr->{class} =~ /zc-tn-t/)) {
1087
    $self->handler(text => sub { $gridtimes++; }, "dtext");
1088
  }
1089
  if (defined($attr->{class}) && ($attr->{class} =~ /stationLogo/i)) {
1090
    $inStationLogo = 1;
1091
  }
1092
}
1093

    
1094
sub on_span {
1095
  my($self, $tag, $attr) = @_;
1096
  if (defined($attr->{class})) {
1097
    if ($attr->{class} =~ /zc-pg-y/) {
1098
      $self->handler(text => sub { $y = shift; $y =~ s/[^\d]//gs; $programs{$cp}{movie_year} = $y }, "dtext");
1099
    } elsif ($attr->{class} =~ /zc-pg-e/) {
1100
      $self->handler(text => sub { $programs{$cp}{episode} = shift; $tba = 1 if $programs{$cp}{episode} =~ /$sTBA/i;}, "dtext"); 
1101
    } elsif ($attr->{class} =~ /zc-st-c/) {
1102
      $self->handler(text => sub { $stations{$cs}{name} = &trim(shift) }, "dtext");
1103
    } elsif ($attr->{class} =~ /zc-ic-s/) {
1104
      $self->handler(text => sub { &handleTags(shift); }, "dtext");
1105
    } elsif ($attr->{class} =~ /zc-pg-t/) {
1106
      $self->handler(text => sub { $programs{$cp}{title} = (shift); $tba = 1 if $programs{$cp}{title} =~ /$sTBA/i;}, "dtext");
1107
    } elsif ($attr->{class} =~ /zc-ic-premiere/) {
1108
      $self->handler(text => sub { $schedule{$cs}{$sch}{premiere} = &trim(shift); }, "dtext");
1109
    } elsif ($attr->{class} =~ /zc-ic-finale/) {
1110
      $self->handler(text => sub { $schedule{$cs}{$sch}{finale} = &trim(shift); }, "dtext");
1111
    }
1112
  }
1113
  if (defined($attr->{id})) {
1114
    if ($attr->{id} =~ /zc-topbar-provider-name/) {
1115
      $self->handler(text => sub { 
1116
        $n = $l = $t = shift;
1117
        $n =~ s/(.*)\-.*/&trim($1)/es;
1118
        $l =~ s/.*\(\s*(.*)\s*\).*/&trim($1)/es;
1119
        $t =~ s/.*\-(.*)\(.*/&trim($1)/es;
1120

    
1121
        if (!defined($lineuptype)) {
1122
          if ($t =~ /satellite/i) { $lineuptype = "Satellite"; }
1123
          elsif ($t =~ /digital/i) { $lineuptype = "CableDigital"; }
1124
          elsif ($t =~ /cable/i) { $lineuptype = "Cable"; }
1125
          else { $lineuptype = "LocalBroadcast"; }
1126
        }
1127
        $lineupname = $n if !defined($lineupname);
1128
        $lineuplocation = $l if !defined($lineuplocation);
1129
      }, "dtext");
1130
    }
1131
  }
1132
}
1133

    
1134
sub on_stnnum_img {
1135
  my($self, $tag, $attr) = @_;
1136
  if (defined($attr->{id}) && $attr->{id} =~ /zc-ssl-logo/) {
1137
    &handleLogo($attr->{src}) if defined($iconDir);
1138
  }
1139
}
1140

    
1141
sub handler_start {
1142
  my($self, $tag, $attr) = @_;
1143
  $f = "on_$tag";
1144
  &$f(@_);
1145
}
1146

    
1147
sub handler_end {
1148
  my ($self, $tag) = @_;
1149
  if ($tag eq 'td' || $tag eq 'th') { $inStationTd = 0; } 
1150
  elsif ($tag eq 'div') { $inIcons = 0; $inStationLogo = 0; }
1151
  $self->handler(text => undef);
1152
}
1153

    
1154
sub handler_stnnum_start {
1155
  my($self, $tag, $attr) = @_;
1156
  $f = "on_stnnum_$tag";
1157
  &$f(@_);
1158
}
1159

    
1160
sub handler_stnnum_end {
1161
  my ($self, $tag) = @_;
1162
  $self->handler(text => undef);
1163
}
1164

    
1165
sub parseTVGFavs {
1166
  my $buffer = shift;
1167
  my $t = decode_json($buffer);
1168

    
1169
  if (defined($t->{'message'})) {
1170
    my $m = $t->{'message'};
1171
    foreach my $f (@{$m}) {
1172
      my $source = $f->{"source"};
1173
      my $channel = $f->{"channel"};
1174
      $tvgfavs{$channel} = $source;
1175
    }
1176
    &pout("Lineup $zlineupId favorites: " .  (keys %tvgfavs) . "\n");
1177
  }
1178
}
1179

    
1180
sub parseTVGIcons {
1181
  require GD;
1182
  $rc = Encode::encode('utf8', &getURL($tvgspritesurl . "$zlineupId\.css") );
1183
  if ($rc =~ /background-image:.+?url\((.+?)\)/) {
1184
    my $url = $tvgspritesurl . $1;
1185

    
1186
    if (! -d $iconDir) {
1187
      mkdir($iconDir) or die "Can't mkdir: $!\n";
1188
    }
1189

    
1190
    ($n,$_,$s) = fileparse($url, qr"\..*");
1191
    $f = $iconDir . "/sprites-" . $n . $s;
1192
    &wbf($f, &getURL($url));
1193

    
1194
    GD::Image->trueColor(1);
1195
    $im =  new GD::Image->new($f);
1196

    
1197
    my $iconw = 30;
1198
    my $iconh = 20;
1199
    while ($rc =~ /listings-channel-icon-(.+?)\{.+?position:.*?\-(\d+).+?(\d+).*?\}/isg) {
1200
      my $cid = $1;
1201
      my $iconx = $2;
1202
      my $icony = $3;
1203

    
1204
      my $icon = new GD::Image($iconw,$iconh);
1205
      $icon->alphaBlending(0);
1206
      $icon->saveAlpha(1);
1207
      $icon->copy($im, 0, 0, $iconx, $icony, $iconw, $iconh);
1208

    
1209
      $stations{$cid}{logo} = "sprite-" . $cid;
1210
      $stations{$cid}{logoExt} = $s;
1211

    
1212
      my $ifn = $iconDir . "/" . $stations{$cid}{logo} . $stations{$cid}{logoExt};
1213
      &wbf($ifn, $icon->png);
1214
    }
1215
  }
1216
}
1217

    
1218
sub parseTVGD {
1219
  my $gz = gzopen(shift, "rb");
1220
  my $json = new JSON::PP;
1221
  my $buffer;
1222
  $buffer .= $b while $gz->gzread($b, 65535) > 0;
1223
  $gz->gzclose();
1224
  my $t = decode_json($buffer);
1225

    
1226
  if (defined($t->{'program'})) {
1227
    my $prog = $t->{'program'};
1228
    if (defined($prog->{'release_year'})) {
1229
      $programs{$cp}{movie_year} = $prog->{'release_year'};
1230
    }
1231
  }
1232
  if (defined($t->{'tvobject'})) {
1233
    my $tvo = $t->{'tvobject'};
1234
    if (defined($tvo->{'photos'})) {
1235
      my $photos = $tvo->{'photos'};
1236
      my %phash;
1237
      foreach $ph (@{$photos}) {
1238
        my $w = $ph->{'width'} * $ph->{'height'};
1239
        my $u = $ph->{'url'};
1240
        $phash{$w} = $u;
1241
      }
1242
      my $big = (sort {$b <=> $a} keys %phash)[0];
1243
      $programs{$cp}{imageUrl} = $phash{$big};
1244
    }
1245
  }
1246
}
1247

    
1248
sub parseTVGGrid {
1249
  my $gz = gzopen(shift, "rb");
1250
  my $json = new JSON::PP;
1251
  my $buffer;
1252
  $buffer .= $b while $gz->gzread($b, 65535) > 0;
1253
  $gz->gzclose();
1254
  my $t = decode_json($buffer);
1255

    
1256
  foreach my $e (@{$t}) {
1257
    my $cjs = $e->{'Channel'};
1258
    $cs = $cjs->{'SourceId'};
1259

    
1260
    if (%tvgfavs) {
1261
      if (defined($cjs->{'Number'}) && $cjs->{'Number'} ne '') {
1262
        my $n = $cjs->{'Number'};
1263
        if ($cs != $tvgfavs{$n}) {
1264
          next;
1265
        }
1266
      }
1267
    }
1268

    
1269
    if (!defined($stations{$cs}{stnNum})) {
1270
      $stations{$cs}{stnNum} = $cs;
1271
      $stations{$cs}{number} = $cjs->{'Number'} if defined($cjs->{'Number'}) && $cjs->{'Number'} ne '';
1272
      $stations{$cs}{name} = $cjs->{'Name'};
1273
      if (defined($cjs->{'FullName'}) && $cjs->{'FullName'} ne $cjs->{'Name'}) {
1274
        $stations{$cs}{fullname} = $cjs->{'FullName'};
1275
      }
1276

    
1277
      if (!defined($stations{$cs}{order})) {
1278
        if (defined($options{b})) {
1279
          $stations{$cs}{order} = $coNum++;
1280
        } else {
1281
          $stations{$cs}{order} = $stations{$cs}{number};
1282
        }
1283
      }
1284
    }
1285

    
1286
    my $cps = $e->{'ProgramSchedules'};
1287
    foreach my $pe (@{$cps}) {
1288
      $cp = $pe->{'ProgramId'};
1289
      my $catid = $pe->{'CatId'};
1290

    
1291
      if ($catid == 1) { $programs{$cp}{genres}{movie} = 1 } 
1292
      elsif ($catid == 2) { $programs{$cp}{genres}{sports} = 1 } 
1293
      elsif ($catid == 3) { $programs{$cp}{genres}{family} = 1 } 
1294
      elsif ($catid == 4) { $programs{$cp}{genres}{news} = 1 } 
1295
      # 5 - 10?
1296
      # my $subcatid = $pe->{'SubCatId'}; 
1297

    
1298
      my $ppid = $pe->{'ParentProgramId'};
1299
      if ((defined($ppid) && $ppid != 0)
1300
        || (defined($options{j}) && $catid != 1)) {
1301
        $programs{$cp}{genres}{series} = 9; 
1302
      }
1303

    
1304
      $programs{$cp}{title} = $pe->{'Title'};
1305
      $tba = 1 if $programs{$cp}{title} =~ /$sTBA/i;
1306

    
1307
      if (defined($pe->{'EpisodeTitle'}) && $pe->{'EpisodeTitle'} ne '') {
1308
        $programs{$cp}{episode} = $pe->{'EpisodeTitle'};
1309
        $tba = 1 if $programs{$cp}{episode} =~ /$sTBA/i;
1310
      }
1311

    
1312
      $programs{$cp}{description} = $pe->{'CopyText'} if defined($pe->{'CopyText'}) && $pe->{'CopyText'} ne '';
1313
      $programs{$cp}{rating} = $pe->{'Rating'} if defined($pe->{'Rating'}) && $pe->{'Rating'} ne '';
1314

    
1315
      my $sch = $pe->{'StartTime'} * 1000;
1316
      $schedule{$cs}{$sch}{time} = $sch;
1317
      $schedule{$cs}{$sch}{endtime} = $pe->{'EndTime'} * 1000;
1318
      $schedule{$cs}{$sch}{program} = $cp;
1319
      $schedule{$cs}{$sch}{station} = $cs;
1320

    
1321
      my $airat = $pe->{'AiringAttrib'};
1322
      if ($airat & 1) { $schedule{$cs}{$sch}{live} = 1 }
1323
      elsif ($airat & 4) { $schedule{$cs}{$sch}{new} = 1 }
1324
      # other bits?
1325

    
1326
      my $tvo = $pe->{'TVObject'};
1327
      if (defined($tvo)) {
1328
        if (defined($tvo->{'SeasonNumber'}) && $tvo->{'SeasonNumber'} != 0) {
1329
          $programs{$cp}{seasonNum} = $tvo->{'SeasonNumber'};
1330
          if (defined($tvo->{'EpisodeNumber'}) && $tvo->{'EpisodeNumber'} != 0) {
1331
            $programs{$cp}{episodeNum} = $tvo->{'EpisodeNumber'};
1332
          }
1333
        }
1334
        if (defined($tvo->{'EpisodeAirDate'})) {
1335
          my $eaid = $tvo->{'EpisodeAirDate'};
1336
          $eaid =~ tr/0-9//cd;
1337
          $programs{$cp}{originalAirDate} = $eaid if ($eaid ne '');
1338
        }
1339
        my $url;
1340
        if (defined($tvo->{'EpisodeSEOUrl'}) && $tvo->{'EpisodeSEOUrl'} ne '') {
1341
          $url = $tvo->{'EpisodeSEOUrl'};
1342
        } elsif(defined($tvo->{'SEOUrl'}) && $tvo->{'SEOUrl'} ne '') {
1343
          $url = $tvo->{'SEOUrl'};
1344
          $url = "/movies$url" if ($catid == 1 && $url !~ /movies/); 
1345
        }
1346
        $programs{$cp}{url} = substr($tvgurl, 0, -1) . $url if defined($url);
1347
      }
1348
  
1349
      if (defined($options{I}) || (defined($options{D}) && $catid == 1)) {
1350
        my $fn = "$cacheDir/$cp\.js\.gz";
1351
        if (! -e $fn) {
1352
          $rc = Encode::encode('utf8', &getURL($tvgMapiRoot . "listings/details?program=$cp") );
1353
          &wbf($fn, Compress::Zlib::memGzip($rc));
1354
        }
1355
        &pout("[D] Parsing: $cp\n");
1356
        &parseTVGD($fn);
1357
      }
1358
    }
1359
  }
1360
}
1361

    
1362
sub parseJSONI {
1363
  my $gz = gzopen(shift, "rb");
1364
  my $json = new JSON::PP;
1365
  my $buffer;
1366
  $buffer .= $b while $gz->gzread($b, 65535) > 0;
1367
  $gz->gzclose();
1368
  $buffer =~ s/'/"/g;
1369
  my $t = decode_json($buffer);
1370

    
1371
  if (defined($t->{imageUrl}) && $t->{imageUrl} =~ /^http/i) {
1372
    $programs{$cp}{imageUrl} = $t->{imageUrl}
1373
  }
1374
}
1375

    
1376
sub parseJSOND {
1377
  my $gz = gzopen(shift, "rb");
1378
  my $json = new JSON::PP;
1379
  my $buffer;
1380
  $buffer .= $b while $gz->gzread($b, 65535) > 0;
1381
  $gz->gzclose();
1382
  $buffer =~ s/^.+?\=\ //gim;
1383
  my $t = decode_json($buffer);
1384
  my $p = $t->{'program'};
1385

    
1386
  if (defined($p->{'seasonNumber'})) {
1387
    my $sn = $p->{'seasonNumber'};
1388
    $sn =~ s/S//i;
1389
    $programs{$cp}{seasonNum} = $sn if ($sn ne '');
1390
  }
1391
  if (defined($p->{'episodeNumber'})) {
1392
    my $en = $p->{'episodeNumber'};
1393
    $en =~ s/E//i;
1394
    $programs{$cp}{episodeNum} = $en if ($en ne '');
1395
  }
1396
  if (defined($p->{'originalAirDate'})) {
1397
    my $oad = $p->{'originalAirDate'};
1398
    $programs{$cp}{originalAirDate} = $oad if ($oad ne '');
1399
  }
1400
  if (defined($p->{'description'})) {
1401
    my $desc = $p->{'description'};
1402
    $programs{$cp}{description} = $desc if ($desc ne '');
1403
  }
1404
  if (defined($p->{'genres'})) {
1405
    my $genres = $p->{'genres'};
1406
    my $i = 1;
1407
    foreach $g (@{$genres}) {
1408
      ${$programs{$cp}{genres}}{lc($g)} = $i++;
1409
    }
1410
  }
1411
  if (defined($p->{'seriesId'})) {
1412
    my $seriesId = $p->{'seriesId'};
1413
    ${$programs{$cp}{genres}}{series} = 9 if ($seriesId ne '');
1414
  }
1415

    
1416
  if (defined($p->{'credits'})) {
1417
    my $credits = $p->{'credits'};
1418
    my $i = 1;
1419
    foreach $g (@{$credits}) {
1420
      ${$programs{$cp}{credits}}{$g} = $i++;
1421
    }
1422
  }
1423
  if (defined($p->{'starRating'})) {
1424
    my $sr = $p->{'starRating'};
1425
    my $tsr = length($sr);
1426
    if ($sr =~ /\+$/) {
1427
      $tsr = $tsr - 1;
1428
      $tsr .= ".5";
1429
     } 
1430
    $programs{$cp}{starRating} = $tsr;
1431
  }
1432
}
1433

    
1434
sub parseGrid {
1435
  my @report_tags = qw(td th span a p div img li);
1436
  my $p = HTML::Parser->new(
1437
    api_version => 3,
1438
    unbroken_text => 1,
1439
    report_tags => \@report_tags,
1440
    handlers  => [
1441
      start => [\&handler_start, "self, tagname, attr"],
1442
      end => [\&handler_end, "self, tagname"],
1443
    ],
1444
  );
1445
  
1446
  my $gz = gzopen(shift, "rb");
1447
  my $b;
1448
  $p->parse($b) while $gz->gzread($b, 65535) > 0;
1449
  $gz->gzclose();
1450
  $p->eof;
1451
}
1452

    
1453
sub parseSTNNUM {
1454
  my @report_tags = qw(img);
1455
  my $p = HTML::Parser->new(
1456
    api_version => 3,
1457
    unbroken_text => 1,
1458
    report_tags => \@report_tags,
1459
    handlers  => [
1460
      start => [\&handler_stnnum_start, "self, tagname, attr"],
1461
      end => [\&handler_stnnum_end, "self, tagname"],
1462
    ],
1463
  );
1464
  
1465
  my $gz = gzopen(shift, "rb");
1466
  my $b;
1467
  $p->parse($b) while $gz->gzread($b, 65535) > 0;
1468
  $gz->gzclose();
1469
  $p->eof;
1470
}
1471

    
1472
sub hourToMillis {
1473
  ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
1474
  if ($start == 0) {
1475
    $hour = int($hour/$gridHours) * $gridHours;
1476
  } else {
1477
    $hour = 0; 
1478
  }
1479
  $t = timegm(0,0,$hour,$mday,$mon,$year);
1480
  $t = $t - (&tz_offset * 3600) if !defined($options{g});
1481
  ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($t);
1482
  $t = timegm($sec, $min, $hour,$mday,$mon,$year);
1483
  return $t . "000";
1484
}
1485

    
1486
sub tz_offset {
1487
  my $n = defined $_[0] ? $_[0] : time;
1488
  my ($lm, $lh, $ly, $lyd) = (localtime $n)[1, 2, 5, 7];
1489
  my ($gm, $gh, $gy, $gyd) = (gmtime $n)[1, 2, 5, 7];
1490
  ($lm - $gm)/60 + $lh - $gh + 24 * ($ly - $gy || $lyd - $gyd)
1491
}
1492

    
1493
sub timezone {
1494
  my $tztime = defined $_[0] ? &_rtrim3(shift) : time; 
1495
  my $os = sprintf "%.1f", (timegm(localtime($tztime)) - $tztime) / 3600;
1496
  my $mins = sprintf "%02d", abs( $os - int($os) ) * 60;
1497
  return sprintf("%+03d", int($os)) . $mins;
1498
}
1499

    
1500
sub max ($$) { $_[$_[0] < $_[1]] }
1501
sub min ($$) { $_[$_[0] > $_[1]] }
1502

    
1503
sub printHelp {
1504
print <<END;
1505
zap2xml <zap2xml\@gmail.com> (2016-05-21)
1506
  -u <username>
1507
  -p <password>
1508
  -d <# of days> (default = $days)
1509
  -n <# of no-cache days> (from end)   (default = $ncdays)
1510
  -N <# of no-cache days> (from start) (default = $ncsdays)
1511
  -s <start day offset> (default = $start)
1512
  -o <output xml filename> (default = "$outFile")
1513
  -c <cacheDirectory> (default = "$cacheDir")
1514
  -l <lang> (default = "$lang")
1515
  -i <iconDirectory> (default = don't download channel icons)
1516
  -m <#> = offset program times by # minutes (better to use TZ env var)
1517
  -b = retain website channel order
1518
  -x = output XTVD xml file format (default = XMLTV)
1519
  -w = wait on exit (require keypress before exiting)
1520
  -q = quiet (no status output)
1521
  -r <# of connection retries before failure> (default = $retries, max 20)
1522
  -e = hex encode entities (html special characters like accents)
1523
  -E "amp apos quot lt gt" = selectively encode standard XML entities
1524
  -F = output channel names first (rather than "number name")
1525
  -O = use old tv_grab_na style channel ids (C###nnnn.zap2it.com)
1526
  -A "new live" = append " *" to program titles that are "new" and/or "live"
1527
  -M = copy movie_year to empty movie sub-title tags
1528
  -U = UTF-8 encoding (default = "ISO-8859-1")
1529
  -L = output "<live />" tag (not part of xmltv.dtd)
1530
  -T = don't cache files containing programs with "$sTBA" titles 
1531
  -P <http://proxyhost:port> = to use an http proxy
1532
  -C <configuration file> (default = "$confFile")
1533
  -S <#seconds> = sleep between requests to prevent flooding of server 
1534
  -D = include details = 1 extra http request per program!
1535
  -I = include icons (image URLs) - 1 extra http request per program!
1536
  -J <xmltv> = include xmltv file in output
1537
  -Y <lineupId> (if not using username/password)
1538
  -Z <zipcode> (if not using username/password)
1539
  -z = use tvguide.com instead of zap2it.com
1540
  -a = output all channels (not just favorites) on tvguide.com
1541
  -j = add "series" category to all non-movie programs
1542
END
1543
sleep(5) if ($^O eq 'MSWin32');
1544
exit 0;
1545
}
(2-2/3)