Project

General

Profile

RE: Is there a current Zap2It grabber that works? ยป zap2xml.pl

Joe User, 2019-11-24 09:15

 
1
#!/usr/bin/env perl
2
# zap2xml (c) <[email protected]> - for personal use only!
3
# not for redistribution of any kind, or conversion to other languages,
4
# not GPL. not for github, thank you. 
5

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

    
28
use Compress::Zlib;
29
use Encode;
30
use File::Basename;
31
use File::Copy;
32
use Getopt::Std;
33
use HTTP::Cookies;
34
use URI;
35
use URI::Escape;
36
use LWP::UserAgent;
37
use POSIX;
38
use Time::Local;
39
use Time::Piece;
40
use JSON;
41

    
42
no warnings 'utf8';
43

    
44
STDOUT->autoflush(1);
45
STDERR->autoflush(1);
46

    
47
%options=();
48
getopts("?aA:bB:c:C:d:DeE:Fgi:IjJ:l:Lm:Mn:N:o:Op:P:qRr:s:S:t:Tu:UwWxY:zZ:89",\%options);
49

    
50
$homeDir = $ENV{HOME};
51
$homeDir = $ENV{USERPROFILE} if !defined($homeDir);
52
$homeDir = '.' if !defined($homeDir);
53
$confFile = $homeDir . '/.zap2xmlrc';
54

    
55
# Defaults
56
$start = 0;
57
$days = 7;
58
$ncdays = 0;
59
$ncsdays = 0;
60
$ncmday = -1;
61
$retries = 3;
62
$outFile = 'xmltv.xml';
63
$outFile = 'xtvd.xml' if defined $options{x};
64
$cacheDir = 'cache';
65
$lang = 'en';
66
$userEmail = '';
67
$password = '';
68
$proxy;
69
$postalcode; 
70
$country;
71
$lineupId; 
72
$device;
73
$sleeptime = 0;
74
$allChan = 0;
75
$shiftMinutes = 0;
76

    
77
$outputXTVD = 0;
78
$lineuptype;
79
$lineupname;
80
$lineuplocation;
81

    
82
$zapToken;
83
$zapPref='-';
84
%zapFavorites=();
85
%sidCache=();
86

    
87
$sTBA = "\\bTBA\\b|To Be Announced";
88

    
89
%tvgfavs=();
90

    
91
&HELP_MESSAGE() if defined $options{'?'};
92

    
93
$confFile = $options{C} if defined $options{C};
94
# read config file
95
if (open (CONF, $confFile))
96
{
97
  &pout("Reading config file: $confFile\n");
98
  while (<CONF>)
99
  {
100
    s/#.*//; # comments
101
    if (/^\s*$/i)                            { }
102
    elsif (/^\s*start\s*=\s*(\d+)/i)         { $start = $1; }
103
    elsif (/^\s*days\s*=\s*(\d+)/i)          { $days = $1; }
104
    elsif (/^\s*ncdays\s*=\s*(\d+)/i)        { $ncdays = $1; }
105
    elsif (/^\s*ncsdays\s*=\s*(\d+)/i)       { $ncsdays = $1; }
106
    elsif (/^\s*ncmday\s*=\s*(\d+)/i)        { $ncmday = $1; }
107
    elsif (/^\s*retries\s*=\s*(\d+)/i)       { $retries = $1; }
108
    elsif (/^\s*user[\w\s]*=\s*(.+)/i)       { $userEmail = &rtrim($1); }
109
    elsif (/^\s*pass[\w\s]*=\s*(.+)/i)       { $password = &rtrim($1); }
110
    elsif (/^\s*cache\s*=\s*(.+)/i)          { $cacheDir = &rtrim($1); }
111
    elsif (/^\s*icon\s*=\s*(.+)/i)           { $iconDir = &rtrim($1); }
112
    elsif (/^\s*trailer\s*=\s*(.+)/i)        { $trailerDir = &rtrim($1); }
113
    elsif (/^\s*lang\s*=\s*(.+)/i)           { $lang = &rtrim($1); }
114
    elsif (/^\s*outfile\s*=\s*(.+)/i)        { $outFile = &rtrim($1); }
115
    elsif (/^\s*proxy\s*=\s*(.+)/i)          { $proxy = &rtrim($1); }
116
    elsif (/^\s*outformat\s*=\s*(.+)/i)      { $outputXTVD = 1 if $1 =~ /xtvd/i; }
117
    elsif (/^\s*lineupid\s*=\s*(.+)/i)       { $lineupId = &rtrim($1); }
118
    elsif (/^\s*lineupname\s*=\s*(.+)/i)     { $lineupname = &rtrim($1); }
119
    elsif (/^\s*lineuptype\s*=\s*(.+)/i)     { $lineuptype = &rtrim($1); }
120
    elsif (/^\s*lineuplocation\s*=\s*(.+)/i) { $lineuplocation = &rtrim($1); }
121
    elsif (/^\s*postalcode\s*=\s*(.+)/i)     { $postalcode = &rtrim($1); }
122
    else
123
    {
124
      die "Oddline in config file \"$confFile\".\n\t$_";
125
    }
126
  }
127
  close (CONF);
128
} 
129
&HELP_MESSAGE() if !(%options) && $userEmail eq '';
130

    
131
$cacheDir = $options{c} if defined $options{c};
132
$days = $options{d} if defined $options{d};
133
$ncdays = $options{n} if defined $options{n};
134
$ncsdays = $options{N} if defined $options{N};
135
$ncmday = $options{B} if defined $options{B}; 
136
$start = $options{s} if defined $options{s};
137
$retries = $options{r} if defined $options{r};
138
$iconDir = $options{i} if defined $options{i};
139
$trailerDir = $options{t} if defined $options{t};
140
$lang = $options{l} if defined $options{l};
141
$outFile = $options{o} if defined $options{o};
142
$password = $options{p} if defined $options{p};
143
$userEmail = $options{u} if defined $options{u};
144
$proxy = $options{P} if defined $options{P};
145
$zlineupId = $options{Y} if defined $options{Y};
146
$zipcode = $options{Z} if defined $options{Z};
147
$includeXMLTV = $options{J} if defined $options{J} && -e $options{J};
148
$outputXTVD = 1 if defined $options{x};
149
$allChan = 1 if defined($options{a});
150
$allChan = 1 if defined($zipcode) && defined($zlineupId);
151
$sleeptime = $options{S} if defined $options{S};
152
$shiftMinutes = $options{m} if defined $options{m};
153
$ncdays = $days - $ncdays; # make relative to the end
154
$urlRoot = 'https://tvlistings.zap2it.com/';
155
$urlAssets = 'https://zap2it.tmsimg.com/assets/';
156
$tvgurlRoot = 'http://mobilelistings.tvguide.com/';
157
$tvgMapiRoot = 'http://mapi.tvguide.com/';
158
$tvgurl = 'http://www.tvguide.com/';
159
$tvgspritesurl = 'http://static.tvgcdn.net/sprites/';
160
$retries = 20 if $retries > 20; # Too many
161

    
162
my %programs = ();
163
my $cp;
164
my %stations = ();
165
my $cs;
166
my $rcs;
167
my %schedule = ();
168
my $sch;
169

    
170
my $coNum = 0;
171
my $tb = 0;
172
my $treq = 0;
173
my $expired = 0;
174
my $ua;
175
my $tba = 0;
176
my $exp = 0;
177
my @fh = ();
178

    
179
my $XTVD_startTime;
180
my $XTVD_endTime;
181

    
182
if (! -d $cacheDir) {
183
  mkdir($cacheDir) or die "Can't mkdir: $!\n";
184
} else {
185
  opendir (DIR, "$cacheDir/");
186
  @cacheFiles = grep(/\.html|\.js/,readdir(DIR));
187
  closedir (DIR);
188
  foreach $cacheFile (@cacheFiles) {
189
    $fn = "$cacheDir/$cacheFile";
190
    $atime = (stat($fn))[8];
191
    if ($atime + ( ($days + 2) * 86400) < time) {
192
      &pout("Deleting old cached file: $fn\n");
193
      &unf($fn);
194
    }
195
  }
196
}
197

    
198
my $s1 = time();
199
if (defined($options{z})) {
200

    
201
  &login() if !defined($options{a}); # get favorites
202
  &parseTVGIcons() if defined($iconDir);
203
  $gridHours = 3;
204
  $maxCount = $days * (24 / $gridHours);
205
  $offset = $start * 3600 * 24 * 1000;
206
  $ms = &hourToMillis() + $offset;
207

    
208
  for ($count=0; $count < $maxCount; $count++) {
209
    $curday = int($count / (24/$gridHours)) + 1;
210
    if ($count == 0) { 
211
      $XTVD_startTime = $ms;
212
    } elsif ($count == $maxCount - 1) { 
213
      $XTVD_endTime = $ms + ($gridHours * 3600000) - 1;
214
    }
215

    
216
    $fn = "$cacheDir/$ms\.js\.gz";
217
    if (! -e $fn || $curday > $ncdays || $curday <= $ncsdays || $curday == $ncmday) {
218
      &login() if !defined($zlineupId);
219
      my $duration = $gridHours * 60;
220
      my $tvgstart = substr($ms, 0, -3);
221
      $rc = Encode::encode('utf8', &getURL($tvgurlRoot . "Listingsweb/ws/rest/schedules/$zlineupId/start/$tvgstart/duration/$duration"));
222
      &wbf($fn, Compress::Zlib::memGzip($rc));
223
    }
224
    &pout("[" . ($count+1) . "/" . "$maxCount] Parsing: $fn\n");
225
    &parseTVGGrid($fn);
226

    
227
    if (defined($options{T}) && $tba) {
228
      &pout("Deleting: $fn (contains \"$sTBA\")\n");
229
      &unf($fn);
230
    }
231
    if ($exp) {
232
      &pout("Deleting: $fn (expired)\n");
233
      &unf($fn);
234
    }
235
    $exp = 0;
236
    $tba = 0;
237
    $ms += ($gridHours * 3600 * 1000); 
238
  } 
239

    
240
} else {
241

    
242
  &login() if !defined($options{a}); # get favorites
243
  $gridHours = 3;
244
  $maxCount = $days * (24 / $gridHours);
245
  $offset = $start * 3600 * 24 * 1000;
246
  $ms = &hourToMillis() + $offset;
247
  for ($count=0; $count < $maxCount; $count++) {
248
    $curday = int($count / (24/$gridHours)) + 1;
249
    if ($count == 0) { 
250
      $XTVD_startTime = $ms;
251
    } elsif ($count == $maxCount - 1) { 
252
      $XTVD_endTime = $ms + ($gridHours * 3600000) - 1;
253
    }
254

    
255
    $fn = "$cacheDir/$ms\.js\.gz";
256
    if (! -e $fn || $curday > $ncdays || $curday <= $ncsdays || $curday == $ncmday) {
257
      my $zstart = substr($ms, 0, -3);
258
      $params = "?time=$zstart&timespan=$gridHours&pref=$zapPref&";
259
      $params .= &getZapGParams();
260
      $params .= '&TMSID=&AffiliateID=gapzap&FromPage=TV%20Grid';
261
      $params .= '&ActivityID=1&OVDID=&isOverride=true';
262
      $rs = &getURL($urlRoot . "api/grid$params",'X-Requested-With' => 'XMLHttpRequest');
263
      last if ($rs eq '');
264
      $rc = Encode::encode('utf8', $rs);
265
      &wbf($fn, Compress::Zlib::memGzip($rc));
266
    }
267
   
268
    &pout("[" . ($count+1) . "/" . "$maxCount] Parsing: $fn\n");
269
    &parseJSON($fn);
270

    
271
    if (defined($options{T}) && $tba) {
272
      &pout("Deleting: $fn (contains \"$sTBA\")\n");
273
      &unf($fn);
274
    }
275
    if ($exp) {
276
      &pout("Deleting: $fn (expired)\n");
277
      &unf($fn);
278
    }
279
    $exp = 0;
280
    $tba = 0;
281
    $ms += ($gridHours * 3600 * 1000);
282
  } 
283

    
284
}
285
my $s2 = time();
286

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

    
315
close($FH);
316

    
317
my $ts = 0;
318
for my $station (keys %stations ) {
319
  $ts += scalar (keys %{$schedule{$station}})
320
}
321
my $s3 = time();
322
&pout("Completed in " . ( $s3 - $s1 ) . "s (Parse: " . ( $s2 - $s1 ) . "s) " . keys(%stations) . " stations, " . keys(%programs) . " programs, $ts scheduled.\n");
323

    
324
if (defined($options{w})) {
325
  print "Press ENTER to exit:";
326
  <STDIN>;
327
} else {
328
  sleep(3) if ($^O eq 'MSWin32');
329
}
330

    
331
exit 0;
332

    
333
sub incXML {
334
  my ($st, $en, $FH) = @_;
335
  open($XF, "<$includeXMLTV");
336
  while (<$XF>) {
337
    if (/^\s*$st/../^\s*$en/) {
338
      print $FH $_ unless /^\s*$en/
339
    }
340
  }
341
  close($XF);
342
}
343

    
344
sub pout {
345
  print @_ if !defined $options{q};
346
}
347

    
348
sub perr {
349
  warn @_;
350
}
351

    
352
sub rtrim {
353
  my $s = shift;
354
  $s =~ s/\s+$//;
355
  return $s;
356
}
357

    
358
sub trim {
359
  my $s = shift;
360
  $s =~ s/^\s+//;
361
  $s =~ s/\s+$//;
362
  return $s;
363
}
364

    
365
sub trim2 {
366
  my $s = &trim(shift);
367
  $s =~ s/[^\w\s\(\)\,]//gsi;
368
  $s =~ s/\s+/ /gsi; 
369
  return $s;
370
}
371

    
372
sub _rtrim3 {
373
  my $s = shift;
374
  return substr($s, 0, length($s)-3);
375
}
376

    
377
sub convTime {
378
  my $t = shift;
379
  $t += $shiftMinutes * 60 * 1000;
380
  return strftime "%Y%m%d%H%M%S", localtime(&_rtrim3($t));
381
}
382

    
383
sub convTimeXTVD {
384
  my $t = shift;
385
  $t += $shiftMinutes * 60 * 1000;
386
  return strftime "%Y-%m-%dT%H:%M:%SZ", gmtime(&_rtrim3($t));
387
}
388

    
389
sub convOAD {
390
  return strftime "%Y%m%d", gmtime(&_rtrim3(shift));
391
}
392

    
393
sub convOADXTVD {
394
  return strftime "%Y-%m-%d", gmtime(&_rtrim3(shift));
395
}
396

    
397
sub convDurationXTVD {
398
  my $duration = shift; 
399
  my $hour = int($duration / 3600000);
400
  my $minutes = int(($duration - ($hour * 3600000)) / 60000);
401
  return sprintf("PT%02dH%02dM", $hour, $minutes);
402
}
403

    
404
sub appendAsterisk {
405
  my ($title, $station, $s) = @_;
406
  if (defined($options{A})) {
407
    if (($options{A} =~ "new" && defined($schedule{$station}{$s}{new}))
408
      || ($options{A} =~ "live" && defined($schedule{$station}{$s}{live}))) {
409
      $title .= " *";
410
    }
411
  }
412
  return $title;
413
}
414

    
415
sub stationToChannel {
416
  my $s = shift;
417
  if (defined($options{z})) {
418
    return sprintf("I%s.%s.tvguide.com", $stations{$s}{number},$stations{$s}{stnNum});
419
  } elsif (defined($options{O})) {
420
    return sprintf("C%s%s.zap2it.com",$stations{$s}{number},lc($stations{$s}{name}));
421
  } elsif (defined($options{9})) {
422
    return sprintf("I%s.labs.zap2it.com",$stations{$s}{stnNum});
423
  }
424
  return sprintf("I%s.%s.zap2it.com", $stations{$s}{number},$stations{$s}{stnNum});
425
}
426

    
427
sub sortChan {
428
  if (defined($stations{$a}{order}) && defined($stations{$b}{order})) {
429
    my $c = $stations{$a}{order} <=> $stations{$b}{order};
430
    if ($c == 0) { return $stations{$a}{stnNum} <=> $stations{$b}{stnNum} }
431
	else { return $c };
432
  } else {
433
    return $stations{$a}{name} cmp $stations{$b}{name};
434
  }
435
}
436

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

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

    
463
sub printFooter {
464
  my $FH = shift;
465
  print $FH "</tv>\n";
466
} 
467

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

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

    
510
      my $stopTime = &convTime($endTime);
511
      my $stopTZ = &timezone($endTime);
512

    
513
      print $FH "\t<programme start=\"$startTime $startTZ\" stop=\"$stopTime $stopTZ\" channel=\"" . &stationToChannel($schedule{$station}{$s}{station}) . "\">\n";
514
      if (defined($programs{$p}{title})) {
515
        my $title = &enc($programs{$p}{title});
516
        $title = &appendAsterisk($title, $station, $s);
517
        print $FH "\t\t<title lang=\"$lang\">" . $title . "</title>\n";
518
      } 
519

    
520
      if (defined($programs{$p}{episode}) || (defined($options{M}) && defined($programs{$p}{movie_year}))) {
521
        print $FH "\t\t<sub-title lang=\"$lang\">";
522
          if (defined($programs{$p}{episode})) {
523
             print $FH &enc($programs{$p}{episode});
524
          } else {
525
             print $FH "Movie (" . $programs{$p}{movie_year} . ")";
526
          } 
527
        print $FH "</sub-title>\n"
528
      }
529

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

    
532
      if (defined($programs{$p}{actor}) 
533
        || defined($programs{$p}{director})
534
        || defined($programs{$p}{writer})
535
        || defined($programs{$p}{producer})
536
        || defined($programs{$p}{preseter})
537
        ) {
538
        print $FH "\t\t<credits>\n";
539
        &printCredits($FH, $p, "director");
540
        foreach my $g (sort { $programs{$p}{actor}{$a} <=> $programs{$p}{actor}{$b} } keys %{$programs{$p}{actor}} ) {
541
          print $FH "\t\t\t<actor";
542
          print $FH " role=\"" . &enc($programs{$p}{role}{$g}) . "\"" if (defined($programs{$p}{role}{$g}));
543
          print $FH ">" . &enc($g) . "</actor>\n";
544
        }
545
        &printCredits($FH, $p, "writer");
546
        &printCredits($FH, $p, "producer");
547
        &printCredits($FH, $p, "presenter");
548
        print $FH "\t\t</credits>\n";
549
      }
550
  
551
      my $date;
552
      if (defined($programs{$p}{movie_year})) {
553
        $date = $programs{$p}{movie_year};
554
      } elsif (defined($programs{$p}{originalAirDate}) && $p =~ /^EP|^\d/) {
555
        $date = &convOAD($programs{$p}{originalAirDate});
556
      }
557
      print $FH "\t\t<date>$date</date>\n" if defined($date);
558

    
559
      if (defined($programs{$p}{genres})) {
560
        foreach my $g (sort { $programs{$p}{genres}{$a} <=> $programs{$p}{genres}{$b} or $a cmp $b } keys %{$programs{$p}{genres}} ) {
561
          print $FH "\t\t<category lang=\"$lang\">" . &enc(ucfirst($g)) . "</category>\n";
562
        }
563
      }
564

    
565
      print $FH "\t\t<length units=\"minutes\">" . $programs{$p}{duration} . "</length>\n" if defined($programs{$p}{duration});
566

    
567
      if (defined($programs{$p}{imageUrl})) {
568
        print $FH "\t\t<icon src=\"" . &enc($programs{$p}{imageUrl}) . "\" />\n";
569
      }
570

    
571
      if (defined($programs{$p}{url})) {
572
        print $FH "\t\t<url>" . &enc($programs{$p}{url}) . "</url>\n";
573
      }
574

    
575
      my $xs;
576
      my $xe;
577

    
578
      if (defined($programs{$p}{seasonNum}) && defined($programs{$p}{episodeNum})) {
579
        my $s = $programs{$p}{seasonNum};
580
        my $sf = sprintf("S%0*d", &max(2, length($s)), $s);
581
        my $e = $programs{$p}{episodeNum};
582
        my $ef = sprintf("E%0*d", &max(2, length($e)), $e);
583

    
584
        $xs = int($s) - 1;
585
        $xe = int($e) - 1;
586

    
587
        if ($s > 0 || $e > 0) {
588
          print $FH "\t\t<episode-num system=\"common\">" . $sf . $ef . "</episode-num>\n";
589
        }
590
      }
591

    
592
      $dd_prog_id = $p;
593
      if ( $dd_prog_id =~ /^(..\d{8})(\d{4})/ ) {
594
        $dd_prog_id = sprintf("%s.%s",$1,$2);
595
        print $FH "\t\t<episode-num system=\"dd_progid\">" . $dd_prog_id  . "</episode-num>\n";
596
      }
597

    
598
      if (defined($xs) && defined($xe) && $xs >= 0 && $xe >= 0) {
599
        print $FH "\t\t<episode-num system=\"xmltv_ns\">" . $xs . "." . $xe . ".</episode-num>\n";
600
      }
601

    
602
      if (defined($schedule{$station}{$s}{quality})) {
603
        print $FH "\t\t<video>\n";
604
        print $FH "\t\t\t<aspect>16:9</aspect>\n";
605
        print $FH "\t\t\t<quality>HDTV</quality>\n";
606
        print $FH "\t\t</video>\n";
607
      }
608
      my $new = defined($schedule{$station}{$s}{new});
609
      my $live = defined($schedule{$station}{$s}{live});
610
      my $cc = defined($schedule{$station}{$s}{cc});
611

    
612
      if (! $new && ! $live && $p =~ /^EP|^SH|^\d/) {
613
        print $FH "\t\t<previously-shown ";
614
        if (defined($programs{$p}{originalAirDate})) {
615
          $date = &convOAD($programs{$p}{originalAirDate});
616
          print $FH "start=\"" . $date . "000000\" ";
617
        }
618
        print $FH "/>\n";
619
      }
620

    
621
      if (defined($schedule{$station}{$s}{premiere})) {
622
        print $FH "\t\t<premiere>" . $schedule{$station}{$s}{premiere} . "</premiere>\n";
623
      }
624

    
625
      if (defined($schedule{$station}{$s}{finale})) {
626
        print $FH "\t\t<last-chance>" . $schedule{$station}{$s}{finale} . "</last-chance>\n";
627
      }
628

    
629
      print $FH "\t\t<new />\n" if $new;
630
      # not part of XMLTV format yet?
631
      print $FH "\t\t<live />\n" if (defined($options{L}) && $live);
632
      print $FH "\t\t<subtitles type=\"teletext\" />\n" if $cc;
633

    
634
      if (defined($programs{$p}{rating})) {
635
        print $FH "\t\t<rating>\n\t\t\t<value>" . $programs{$p}{rating} . "</value>\n\t\t</rating>\n"
636
      }
637

    
638
      if (defined($programs{$p}{starRating})) {
639
        print $FH "\t\t<star-rating>\n\t\t\t<value>" . $programs{$p}{starRating} . "/4</value>\n\t\t</star-rating>\n";
640
      }
641
      print $FH "\t</programme>\n";
642
      $i++;
643
    }
644
  }
645
}
646

    
647
sub printHeaderXTVD {
648
  my ($FH, $enc) = @_;
649
  print $FH "<?xml version='1.0' encoding='$enc'?>\n";
650
  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";
651
}
652

    
653
sub printCredits {
654
  my ($FH, $p, $s)  = @_;
655
  foreach my $g (sort { $programs{$p}{$s}{$a} <=> $programs{$p}{$s}{$b} } keys %{$programs{$p}{$s}} ) {
656
    print $FH "\t\t\t<$s>" . &enc($g) . "</$s>\n";
657
  }
658
}
659

    
660
sub printFooterXTVD {
661
  my $FH = shift;
662
  print $FH "</xtvd>\n";
663
} 
664

    
665
sub printStationsXTVD {
666
  my $FH = shift;
667
  print $FH "<stations>\n";
668
  for my $key ( sort sortChan keys %stations ) {
669
    print $FH "\t<station id='" . $stations{$key}{stnNum} . "'>\n";
670
    if (defined($stations{$key}{number})) {
671
      $sname = &enc($stations{$key}{name});
672
      print $FH "\t\t<callSign>" . $sname . "</callSign>\n";
673
      print $FH "\t\t<name>" . $sname . "</name>\n";
674
      print $FH "\t\t<fccChannelNumber>" . $stations{$key}{number} . "</fccChannelNumber>\n";
675
      if (defined($stations{$key}{logo}) && $stations{$key}{logo} =~ /_affiliate/i) {
676
        $affiliate = $stations{$key}{logo};
677
        $affiliate =~ s/(.*)\_.*/uc($1)/e;
678
        print $FH "\t\t<affiliate>" . $affiliate . " Affiliate</affiliate>\n";
679
      }
680
      &copyLogo($key);
681
    }
682
    print $FH "\t</station>\n";
683
  }
684
  print $FH "</stations>\n";
685
}
686

    
687
sub printLineupsXTVD {
688
  my $FH = shift;
689
  print $FH "<lineups>\n";
690
  print $FH "\t<lineup id='$lineupId' name='$lineupname' location='$lineuplocation' type='$lineuptype' postalCode='$postalcode'>\n";
691
  for my $key ( sort sortChan keys %stations ) {
692
    if (defined($stations{$key}{number})) {
693
      print $FH "\t<map station='" . $stations{$key}{stnNum} . "' channel='" . $stations{$key}{number} . "'></map>\n";
694
    }
695
  }
696
  print $FH "\t</lineup>\n";
697
  print $FH "</lineups>\n";
698
}
699

    
700
sub printSchedulesXTVD {
701
  my $FH = shift;
702
  print $FH "<schedules>\n";
703
  for my $station ( sort sortChan keys %stations ) {
704
    my $i = 0; 
705
    my @keyArray = sort { $schedule{$station}{$a}{time} cmp $schedule{$station}{$b}{time} } keys %{$schedule{$station}};
706
    foreach $s (@keyArray) {
707
      if ($#keyArray <= $i) {
708
        delete $schedule{$station}{$s};
709
        next; 
710
      } 
711
      my $p = $schedule{$station}{$s}{program};
712
      my $startTime = &convTimeXTVD($schedule{$station}{$s}{time});
713
      my $stopTime = &convTimeXTVD($schedule{$station}{$keyArray[$i+1]}{time});
714
      my $duration = &convDurationXTVD($schedule{$station}{$keyArray[$i+1]}{time} - $schedule{$station}{$s}{time});
715

    
716
      print $FH "\t<schedule program='$p' station='" . $stations{$station}{stnNum} . "' time='$startTime' duration='$duration'"; 
717
      print $FH " hdtv='true' " if (defined($schedule{$station}{$s}{quality}));
718
      print $FH " new='true' " if (defined($schedule{$station}{$s}{new}) || defined($schedule{$station}{$s}{live}));
719
      print $FH "/>\n";
720
      $i++;
721
    }
722
  }
723
  print $FH "</schedules>\n";
724
}
725

    
726
sub printProgramsXTVD {
727
  my $FH = shift;
728
  print $FH "<programs>\n";
729
  foreach $p (keys %programs) {
730
      print $FH "\t<program id='" . $p . "'>\n";
731
      print $FH "\t\t<title>" . &enc($programs{$p}{title}) . "</title>\n" if defined($programs{$p}{title});
732
      print $FH "\t\t<subtitle>" . &enc($programs{$p}{episode}) . "</subtitle>\n" if defined($programs{$p}{episode});
733
      print $FH "\t\t<description>" . &enc($programs{$p}{description}) . "</description>\n" if defined($programs{$p}{description});
734
      
735
      if (defined($programs{$p}{movie_year})) {
736
        print $FH "\t\t<year>" . $programs{$p}{movie_year} . "</year>\n";
737
      } else { #Guess
738
        my $showType = "Series"; 
739
        if ($programs{$p}{title} =~ /Paid Programming/i) {
740
          $showType = "Paid Programming";
741
        } 
742
        print $FH "\t\t<showType>$showType</showType>\n"; 
743
        print $FH "\t\t<series>EP" . substr($p,2,8) . "</series>\n"; 
744
        print $FH "\t\t<originalAirDate>" . &convOADXTVD($programs{$p}{originalAirDate}) . "</originalAirDate>\n" if defined($programs{$p}{originalAirDate});
745
      }
746
      print $FH "\t</program>\n";
747
  }
748
  print $FH "</programs>\n";
749
}
750

    
751
sub printGenresXTVD {
752
  my $FH = shift;
753
  print $FH "<genres>\n";
754
  foreach $p (keys %programs) {
755
    if (defined($programs{$p}{genres}) && $programs{$p}{genres}{movie} != 1) {
756
      print $FH "\t<programGenre program='" . $p . "'>\n";
757
      foreach my $g (keys %{$programs{$p}{genres}}) {
758
        print $FH "\t\t<genre>\n";
759
        print $FH "\t\t\t<class>" . &enc(ucfirst($g)) . "</class>\n";
760
        print $FH "\t\t\t<relevance>0</relevance>\n";
761
        print $FH "\t\t</genre>\n";
762
      }
763
      print $FH "\t</programGenre>\n";
764
    }
765
  }
766
  print $FH "</genres>\n";
767
}
768

    
769
sub loginTVG {
770
  $treq++;
771
  my $r = $ua->get($tvgurl . 'user/_modal/');
772
  if ($r->is_success) {
773
    my $str = $r->decoded_content;
774
    if ($str =~ /<input.+name=\"_token\".+?value=\"(.*?)\"/is) {
775
      $token = $1;
776
      if ($userEmail ne '' && $password ne '') {
777
        my $rc = 0;
778
        while ($rc++ < $retries) {
779
          my $r = $ua->post($tvgurl . 'user/attempt/', 
780
            { 
781
              _token => $token,
782
              email => $userEmail, 
783
              password => $password,
784
            }, 'X-Requested-With' => 'XMLHttpRequest'
785
          ); 
786

    
787
          $dc = Encode::encode('utf8', $r->decoded_content( raise_error => 1 ));
788
          if ($dc =~ /success/) {
789
            $ua->cookie_jar->scan(sub { if ($_[1] eq "ServiceID") { $zlineupId = $_[2]; }; }); 
790
            if (!defined($options{a})) {
791
              my $r = $ua->get($tvgurl . "user/favorites/?provider=$zlineupId",'X-Requested-With' => 'XMLHttpRequest'); 
792
              $dc = Encode::encode('utf8', $r->decoded_content( raise_error => 1 ));
793
              if ($dc =~ /\{\"code\":200/) {
794
                &parseTVGFavs($dc);
795
              } 
796
            }
797
            return $dc; 
798
          } else {
799
            &pout("[Attempt $rc] " . $dc . "\n");
800
            sleep ($sleeptime + 1);
801
          }
802
        }
803
        die "Failed to login within $retries retries.\n";
804
      }
805
    } else {
806
      die "Login token not found\n";
807
    }
808
  }
809
}
810

    
811
sub loginZAP {
812
  my $rc = 0;
813
  while ($rc++ < $retries) {
814
    $treq++;
815
    my $r = $ua->post($urlRoot . 'api/user/login', 
816
      { 
817
        emailid => $userEmail, password => $password,
818
        usertype => '0', facebookuser =>'false',
819
      }
820
    ); 
821
 
822
    $dc = Encode::encode('utf8', $r->decoded_content( raise_error => 1 ));
823
    if ($r->is_success) {
824
      my $t = decode_json($dc);
825
      $zapToken = $t->{'token'};
826
      $zapPref = '';
827
      $zapPref .= "m" if ($t->{isMusic});
828
      $zapPref .= "p" if ($t->{isPPV});
829
      $zapPref .= "h" if ($t->{isHD});
830
      if ($zapPref eq '') { $zapPref = '-' } 
831
      else {
832
        $zapPref = join(",", split(//, $zapPref));
833
      } 
834

    
835
      my $prs = $t->{'properties'};
836
      $postalcode = $prs->{2002};
837
      $country = $prs->{2003};
838
      ($lineupId, $device) = split(/:/, $prs->{2004});
839
      if (!defined($options{a})) {
840
        my $r = $ua->post($urlRoot . "api/user/favorites", { token => $zapToken }, 'X-Requested-With' => 'XMLHttpRequest'); 
841
        $dc = Encode::encode('utf8', $r->decoded_content( raise_error => 1 ));
842
        if ($r->is_success) {
843
          &parseZFavs($dc);
844
        } else { 
845
          &perr("FF" . $r->status_line .  ": $dc\n");
846
        }
847
      }
848
      return $dc;
849
    } else {
850
      &pout("[Attempt $rc] " . $dc . "\n");
851
      sleep ($sleeptime + 1);
852
    }
853
  }
854
  die "Failed to login within $retries retries.\n";
855
}
856

    
857
sub getZapGParams {
858
  my %hash = &getZapParams();
859
  $hash{country} = delete $hash{countryCode};
860
  return join("&", map { "$_=$hash{$_}" } keys %hash);
861
}
862

    
863
sub getZapPParams {
864
  my %hash = &getZapParams();
865
  delete $hash{lineupId};
866
  return %hash;
867
}
868

    
869
sub getZapParams {
870
  my %phash = ();
871
  if (defined($zlineupId) || defined($zipcode)) {
872
    $postalcode = $zipcode;
873
    $country = "USA";
874
    $country = "CAN" if ($zipcode =~ /[A-z]/);
875
    if ($zlineupId =~ /:/) {
876
       ($lineupId, $device) = split(/:/, $zlineupId);
877
    } else {
878
       $lineupId = $zlineupId;
879
       $device = "-";
880
    }
881
    $phash{postalCode} = $postalcode;
882
  } else {
883
    $phash{token} = &getZToken();
884
  }
885
  $phash{lineupId} = "$country-$lineupId-DEFAULT";
886
  $phash{postalCode} = $postalcode;
887
  $phash{countryCode} = $country;
888
  $phash{headendId} = $lineupId;
889
  $phash{device} = $device;
890
  $phash{aid} = 'gapzap';
891
  return %phash;
892
}
893

    
894
sub login {
895
  if (!defined($userEmail) || $userEmail eq '' || !defined($password) || $password eq '') {
896
    if (!defined($zlineupId)) {
897
      die "Unable to login: Unspecified username or password.\n"
898
    }
899
  }
900

    
901
  if (!defined($ua)) {
902
    $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 }); # WIN 
903
    $ua->cookie_jar(HTTP::Cookies->new);
904
    $ua->proxy(['http', 'https'], $proxy) if defined($proxy);
905
    $ua->agent('Mozilla/4.0');
906
    $ua->default_headers->push_header('Accept-Encoding' => 'gzip, deflate');
907
  }
908

    
909
  if ($userEmail ne '' && $password ne '') {
910
    &pout("Logging in as \"$userEmail\" (" . localtime . ")\n");
911
    if (defined($options{z})) {
912
      &loginTVG();
913
    } else {
914
      &loginZAP();
915
    }
916
  } else {
917
    &pout("Connecting with lineupId \"$zlineupId\" (" . localtime . ")\n");
918
  }
919
}
920

    
921
sub getURL {
922
  my $url = shift;
923
  &login() if !defined($ua);
924

    
925
  my $rc = 0;
926
  while ($rc++ < $retries) {
927
    &pout("[$treq] Getting: $url\n");
928
    sleep $sleeptime; # do these rapid requests flood servers?
929
    $treq++;
930
    my $r = $ua->get($url);
931
    my $cl = length($r->content);
932
    $tb += $cl;
933
    my $dc = $r->decoded_content( raise_error => 1 );
934
    if ($r->is_success && $cl) {
935
      return $dc;
936
    } elsif ($r->code == 400 && $dc =~ /Invalid time stamp passed/) {
937
      &pout("$dc\n");
938
      &pout("Date not in range (reached zap2it limit), normal exit.\n");
939
      return "";
940
    } elsif ($r->code == 500 && $dc =~ /Could not load details/) {
941
      &pout("$dc\n");
942
      return "";
943
    } else {
944
      &perr("[Attempt $rc] $cl:" . $r->status_line . "\n");
945
      sleep ($sleeptime + 2);
946
    }
947
  }
948
  die "Failed to download within $retries retries.\n";
949
}
950

    
951
sub wbf {
952
  my($f, $s) = @_;
953
  open(FO, ">$f") or die "Failed to open '$f': $!";
954
  binmode(FO);
955
  print FO $s;
956
  close(FO);
957
}
958

    
959
sub unf {
960
  my $f = shift;
961
  unlink($f) or &perr("Failed to delete '$f': $!");
962
}
963

    
964
sub copyLogo {
965
  my $key = shift;
966
  if (defined($iconDir) && defined($stations{$key}{logo})) {
967
    my $num = $stations{$key}{number};
968
    my $src = "$iconDir/" . $stations{$key}{logo} . $stations{$key}{logoExt};
969
    my $dest1 = "$iconDir/$num" . $stations{$key}{logoExt};
970
    my $dest2 = "$iconDir/$num " . $stations{$key}{name} . $stations{$key}{logoExt};
971
    my $dest3 = "$iconDir/$num\_" . $stations{$key}{name} . $stations{$key}{logoExt};
972
    copy($src, $dest1);
973
    copy($src, $dest2);
974
    copy($src, $dest3);
975
  }
976
}
977

    
978
sub handleLogo {
979
  my $url = shift;
980
  if (! -d $iconDir) {
981
    mkdir($iconDir) or die "Can't mkdir: $!\n";
982
  }
983
  my $n; my $s;  ($n,$_,$s) = fileparse($url, qr"\..*");
984
  $stations{$cs}{logo} = $n;
985
  $stations{$cs}{logoExt} = $s;
986
  $stations{$cs}{logoURL} = $url;
987
  my $f = $iconDir . "/" . $n . $s;
988
  if (! -e $f) { &wbf($f, &getURL($url)); }
989
}
990

    
991
sub setOriginalAirDate {
992
  if (substr($cp,10,4) ne '0000') {
993
    if (!defined($programs{$cp}{originalAirDate})
994
        || ($schedule{$cs}{$sch}{time} < $programs{$cp}{originalAirDate})) {
995
      $programs{$cp}{originalAirDate} = $schedule{$cs}{$sch}{time};
996
    }
997
  }
998
}
999

    
1000
sub getZToken {
1001
  &login() if (!defined($zapToken));
1002
  return $zapToken;
1003
}
1004

    
1005
sub parseZFavs {
1006
  my $buffer = shift;
1007
  my $t = decode_json($buffer);
1008
  if (defined($t->{'channels'})) {
1009
    my $m = $t->{'channels'};
1010
    foreach my $f (@{$m}) {
1011
      if ($options{R}) {
1012
        my $r = $ua->post($urlRoot . "api/user/ChannelAddtofav", { token => $zapToken, prgsvcid => $f, addToFav => "false" }, 'X-Requested-With' => 'XMLHttpRequest');
1013
        if ($r->is_success) {
1014
          &pout("Removed favorite $f\n");
1015
        } else {
1016
          &perr("RF" . $r->status_line .  "\n");
1017
        }
1018
      } else {
1019
        $zapFavorites{$f} = 1;
1020
      }
1021
    }
1022
    if ($options{R}) {
1023
        &pout("Removed favorites, exiting\n");
1024
        exit;
1025
    };
1026
    &pout("Lineup favorites: " .  (keys %zapFavorites) . "\n");
1027
  }
1028
}
1029

    
1030
sub parseTVGFavs {
1031
  my $buffer = shift;
1032
  my $t = decode_json($buffer);
1033
  if (defined($t->{'message'})) {
1034
    my $m = $t->{'message'};
1035
    foreach my $f (@{$m}) {
1036
      my $source = $f->{"source"};
1037
      my $channel = $f->{"channel"};
1038
      $tvgfavs{"$channel.$source"} = 1;
1039
    }
1040
    &pout("Lineup $zlineupId favorites: " .  (keys %tvgfavs) . "\n");
1041
  }
1042
}
1043

    
1044
sub parseTVGIcons {
1045
  require GD;
1046
  $rc = Encode::encode('utf8', &getURL($tvgspritesurl . "$zlineupId\.css") );
1047
  if ($rc =~ /background-image:.+?url\((.+?)\)/) {
1048
    my $url = $tvgspritesurl . $1;
1049

    
1050
    if (! -d $iconDir) {
1051
      mkdir($iconDir) or die "Can't mkdir: $!\n";
1052
    }
1053

    
1054
    ($n,$_,$s) = fileparse($url, qr"\..*");
1055
    $f = $iconDir . "/sprites-" . $n . $s;
1056
    &wbf($f, &getURL($url));
1057

    
1058
    GD::Image->trueColor(1);
1059
    $im = new GD::Image->new($f);
1060

    
1061
    my $iconw = 30;
1062
    my $iconh = 20;
1063
    while ($rc =~ /listings-channel-icon-(.+?)\{.+?position:.*?\-(\d+).+?(\d+).*?\}/isg) {
1064
      my $cid = $1;
1065
      my $iconx = $2;
1066
      my $icony = $3;
1067

    
1068
      my $icon = new GD::Image($iconw,$iconh);
1069
      $icon->alphaBlending(0);
1070
      $icon->saveAlpha(1);
1071
      $icon->copy($im, 0, 0, $iconx, $icony, $iconw, $iconh);
1072

    
1073
      $stations{$cid}{logo} = "sprite-" . $cid;
1074
      $stations{$cid}{logoExt} = $s;
1075

    
1076
      my $ifn = $iconDir . "/" . $stations{$cid}{logo} . $stations{$cid}{logoExt};
1077
      &wbf($ifn, $icon->png);
1078
    }
1079
  }
1080
}
1081

    
1082
sub parseTVGD {
1083
  my $gz = gzopen(shift, "rb");
1084
  my $buffer;
1085
  $buffer .= $b while $gz->gzread($b, 65535) > 0;
1086
  $gz->gzclose();
1087
  my $t = decode_json($buffer);
1088

    
1089
  if (defined($t->{'program'})) {
1090
    my $prog = $t->{'program'};
1091
    if (defined($prog->{'release_year'})) {
1092
      $programs{$cp}{movie_year} = $prog->{'release_year'};
1093
    }
1094
    if (defined($prog->{'rating'}) && !defined($programs{$cp}{rating})) {
1095
      $programs{$cp}{rating} = $prog->{'rating'} if $prog->{'rating'} ne 'NR';
1096
    }
1097
  }
1098

    
1099
  if (defined($t->{'tvobject'})) {
1100
    my $tvo = $t->{'tvobject'};
1101
    if (defined($tvo->{'photos'})) {
1102
      my $photos = $tvo->{'photos'};
1103
      my %phash;
1104
      foreach $ph (@{$photos}) {
1105
        my $w = $ph->{'width'} * $ph->{'height'};
1106
        my $u = $ph->{'url'};
1107
        $phash{$w} = $u;
1108
      }
1109
      my $big = (sort {$b <=> $a} keys %phash)[0];
1110
      $programs{$cp}{imageUrl} = $phash{$big};
1111
    }
1112
  }
1113
}
1114

    
1115
sub parseTVGGrid {
1116
  my $gz = gzopen(shift, "rb");
1117
  my $buffer;
1118
  $buffer .= $b while $gz->gzread($b, 65535) > 0;
1119
  $gz->gzclose();
1120
  my $t = decode_json($buffer);
1121

    
1122
  foreach my $e (@{$t}) {
1123
    my $cjs = $e->{'Channel'};
1124
    my $src = $cjs->{'SourceId'};
1125
    my $num = $cjs->{'Number'};
1126

    
1127
    $cs = "$num.$src"; 
1128

    
1129
    if (%tvgfavs) {
1130
      next if (!$tvgfavs{$cs});
1131
    }
1132

    
1133
    if (!defined($stations{$cs}{stnNum})) {
1134
      $stations{$cs}{stnNum} = $src;
1135
      $stations{$cs}{number} = $num;
1136
      $stations{$cs}{name} = $cjs->{'Name'};
1137
      if (defined($cjs->{'FullName'}) && $cjs->{'FullName'} ne $cjs->{'Name'}) {
1138
        if ($cjs->{'FullName'} ne '') {
1139
          $stations{$cs}{fullname} = $cjs->{'FullName'};
1140
        }
1141
      }
1142

    
1143
      if (!defined($stations{$cs}{order})) {
1144
        if (defined($options{b})) {
1145
          $stations{$cs}{order} = $coNum++;
1146
        } else {
1147
          $stations{$cs}{order} = $stations{$cs}{number};
1148
        }
1149
      }
1150
    }
1151

    
1152
    my $cps = $e->{'ProgramSchedules'};
1153
    foreach my $pe (@{$cps}) {
1154
      next if (!defined($pe->{'ProgramId'}));
1155
      $cp = $pe->{'ProgramId'};
1156
      my $catid = $pe->{'CatId'};
1157

    
1158
      if ($catid == 1) { $programs{$cp}{genres}{movie} = 1 } 
1159
      elsif ($catid == 2) { $programs{$cp}{genres}{sports} = 1 } 
1160
      elsif ($catid == 3) { $programs{$cp}{genres}{family} = 1 } 
1161
      elsif ($catid == 4) { $programs{$cp}{genres}{news} = 1 } 
1162
      # 5 - 10?
1163
      # my $subcatid = $pe->{'SubCatId'}; 
1164

    
1165
      my $ppid = $pe->{'ParentProgramId'};
1166
      if ((defined($ppid) && $ppid != 0)
1167
        || (defined($options{j}) && $catid != 1)) {
1168
        $programs{$cp}{genres}{series} = 99; 
1169
      }
1170

    
1171
      $programs{$cp}{title} = $pe->{'Title'};
1172
      $tba = 1 if $programs{$cp}{title} =~ /$sTBA/i;
1173

    
1174
      if (defined($pe->{'EpisodeTitle'}) && $pe->{'EpisodeTitle'} ne '') {
1175
        $programs{$cp}{episode} = $pe->{'EpisodeTitle'};
1176
        $tba = 1 if $programs{$cp}{episode} =~ /$sTBA/i;
1177
      }
1178

    
1179
      $programs{$cp}{description} = $pe->{'CopyText'} if defined($pe->{'CopyText'}) && $pe->{'CopyText'} ne '';
1180
      $programs{$cp}{rating} = $pe->{'Rating'} if defined($pe->{'Rating'}) && $pe->{'Rating'} ne '';
1181

    
1182
      my $sch = $pe->{'StartTime'} * 1000;
1183
      $schedule{$cs}{$sch}{time} = $sch;
1184
      $schedule{$cs}{$sch}{endtime} = $pe->{'EndTime'} * 1000;
1185
      $schedule{$cs}{$sch}{program} = $cp;
1186
      $schedule{$cs}{$sch}{station} = $cs;
1187

    
1188
      my $airat = $pe->{'AiringAttrib'};
1189
      if ($airat & 1) { $schedule{$cs}{$sch}{live} = 1 }
1190
      elsif ($airat & 4) { $schedule{$cs}{$sch}{new} = 1 }
1191
      # other bits?
1192

    
1193
      my $tvo = $pe->{'TVObject'};
1194
      if (defined($tvo)) {
1195
        if (defined($tvo->{'SeasonNumber'}) && $tvo->{'SeasonNumber'} != 0) {
1196
          $programs{$cp}{seasonNum} = $tvo->{'SeasonNumber'};
1197
          if (defined($tvo->{'EpisodeNumber'}) && $tvo->{'EpisodeNumber'} != 0) {
1198
            $programs{$cp}{episodeNum} = $tvo->{'EpisodeNumber'};
1199
          }
1200
        }
1201
        if (defined($tvo->{'EpisodeAirDate'})) {
1202
          my $eaid = $tvo->{'EpisodeAirDate'}; # GMT @ 00:00:00
1203
          $eaid =~ tr/\-0-9//cd;
1204
          $programs{$cp}{originalAirDate} = $eaid if ($eaid ne '');
1205
        }
1206
        my $url;
1207
        if (defined($tvo->{'EpisodeSEOUrl'}) && $tvo->{'EpisodeSEOUrl'} ne '') {
1208
          $url = $tvo->{'EpisodeSEOUrl'};
1209
        } elsif(defined($tvo->{'SEOUrl'}) && $tvo->{'SEOUrl'} ne '') {
1210
          $url = $tvo->{'SEOUrl'};
1211
          $url = "/movies$url" if ($catid == 1 && $url !~ /movies/); 
1212
        }
1213
        $programs{$cp}{url} = substr($tvgurl, 0, -1) . $url if defined($url);
1214
      }
1215
  
1216
      if (defined($options{I}) 
1217
        || (defined($options{D}) && $programs{$cp}{genres}{movie}) 
1218
        || (defined($options{W}) && $programs{$cp}{genres}{movie}) ) {
1219
          &getDetails(\&parseTVGD, $cp, $tvgMapiRoot . "listings/details?program=$cp", "");
1220
      } 
1221
    }
1222
  }
1223
}
1224

    
1225
sub getDetails {
1226
  my ($func, $cp, $url, $prefix) = @_;
1227
  my $fn = "$cacheDir/$prefix$cp\.js\.gz";
1228
  if (! -e $fn) {
1229
    my $rs = &getURL($url);
1230
    if (length($rs)) {
1231
      $rc = Encode::encode('utf8', $rs);
1232
      &wbf($fn, Compress::Zlib::memGzip($rc));
1233
    }
1234
  }
1235
  if (-e $fn) {
1236
    my $l = length($prefix) ? $prefix : "D";
1237
    &pout("[$l] Parsing: $cp\n");
1238
    $func->($fn);
1239
  } else {
1240
    &pout("Skipping: $cp\n");
1241
  }
1242
}
1243

    
1244
sub parseJSON {
1245
  my $gz = gzopen(shift, "rb");
1246
  my $buffer;
1247
  $buffer .= $b while $gz->gzread($b, 65535) > 0;
1248
  $gz->gzclose();
1249
  my $t = decode_json($buffer);
1250

    
1251
  my $sts = $t->{'channels'};
1252
  my %zapStarred=();
1253
  foreach $s (@$sts) {
1254

    
1255
    if (defined($s->{'channelId'})) {
1256
      if (!$allChan && scalar(keys %zapFavorites)) {
1257
	if ($zapFavorites{$s->{channelId}}) {
1258
          if ($options{8}) {
1259
            next if $zapStarred{$s->{channelId}};
1260
	    $zapStarred{$s->{channelId}} = 1;
1261
          } 
1262
	} else {
1263
          next;
1264
	}
1265
      }
1266
      # id (uniq) vs channelId, but id not nec consistent in cache
1267
      $cs = $s->{channelNo} . "." . $s->{channelId};
1268
      $stations{$cs}{stnNum} = $s->{channelId};
1269
      $stations{$cs}{name} = $s->{'callSign'};
1270
      $stations{$cs}{number} = $s->{'channelNo'};
1271
      $stations{$cs}{number} =~ s/^0+//g;
1272

    
1273
      if (!defined($stations{$cs}{order})) {
1274
        if (defined($options{b})) {
1275
          $stations{$cs}{order} = $coNum++; 
1276
        } else {
1277
          $stations{$cs}{order} = $stations{$cs}{number};
1278
        }
1279
      }
1280

    
1281
      if ($s->{'thumbnail'} ne '') {
1282
        my $url = $s->{'thumbnail'};
1283
        $url =~ s/\?.*//;  # remove size
1284
        if ($url !~ /^http/) {
1285
          $url = "https:" . $url;
1286
        }
1287
        $stations{$cs}{logoURL} = $url;
1288
        &handleLogo($url) if defined($iconDir);
1289
      }
1290

    
1291
      my $events = $s->{'events'};
1292
      foreach $e (@$events) {
1293
        my $program = $e->{'program'};
1294
        $cp = $program->{'id'};
1295
        $programs{$cp}{title} = $program->{'title'};
1296
        $tba = 1 if $programs{$cp}{title} =~ /$sTBA/i;
1297
        $programs{$cp}{episode} = $program->{'episodeTitle'} if ($program->{'episodeTitle'} ne '');
1298
        $programs{$cp}{description} = $program->{'shortDesc'} if ($program->{'shortDesc'} ne '');
1299
        $programs{$cp}{duration} = $e->{duration} if ($e->{duration} > 0);
1300
        $programs{$cp}{movie_year} = $program->{releaseYear} if ($program->{releaseYear} ne '');
1301
        $programs{$cp}{seasonNum} = $program->{season} if ($program->{'season'} ne '');
1302
        if ($program->{'episode'} ne '') {
1303
          $programs{$cp}{episodeNum} = $program->{episode};
1304
        }
1305
        if ($e->{'thumbnail'} ne '') {
1306
          my $turl = $urlAssets;
1307
          $turl .= $e->{'thumbnail'}  . ".jpg";
1308
          $programs{$cp}{imageUrl} = $turl;
1309
        }
1310
        if ($program->{'seriesId'} ne '' && $program->{'tmsId'} ne '') {
1311
           $programs{$cp}{url} = $urlRoot . "/overview.html?programSeriesId=" 
1312
                . $program->{seriesId} . "&tmsId=" . $program->{tmsId};
1313
        }
1314

    
1315
        $sch = str2time1($e->{'startTime'}) * 1000;
1316
        $schedule{$cs}{$sch}{time} = $sch;
1317
        $schedule{$cs}{$sch}{endTime} = str2time1($e->{'endTime'}) * 1000;
1318
        $schedule{$cs}{$sch}{program} = $cp;
1319
        $schedule{$cs}{$sch}{station} = $cs;
1320

    
1321
        if ($e->{'filter'}) {
1322
          my $genres = $e->{'filter'};
1323
          my $i = 1;
1324
          foreach $g (@{$genres}) {
1325
            $g =~ s/filter-//i;
1326
            ${$programs{$cp}{genres}}{lc($g)} = $i++;
1327
          }
1328
        }
1329

    
1330
        $programs{$cp}{rating} = $e->{rating} if ($e->{rating} ne '');
1331

    
1332
        if ($e->{'tags'}) {
1333
          my $tags = $e->{'tags'};
1334
          if (grep $_ eq 'CC', @{$tags}) {
1335
            $schedule{$cs}{$sch}{cc} = 1
1336
          }
1337
        }
1338

    
1339
        if ($e->{'flag'}) {
1340
          my $flags = $e->{'flag'};
1341
          if (grep $_ eq 'New', @{$flags}) {
1342
            $schedule{$cs}{$sch}{new} = 'New'
1343
            &setOriginalAirDate();
1344
          }
1345
          if (grep $_ eq 'Live', @{$flags}) {
1346
            $schedule{$cs}{$sch}{live} = 'Live'
1347
            &setOriginalAirDate(); # live to tape?
1348
          }
1349
          if (grep $_ eq 'Premiere', @{$flags}) {
1350
            $schedule{$cs}{$sch}{premiere} = 'Premiere';
1351
          }
1352
          if (grep $_ eq 'Finale', @{$flags}) {
1353
            $schedule{$cs}{$sch}{finale} = 'Finale';
1354
          }
1355
        }
1356

    
1357
        if ($options{D} && !$program->{isGeneric}) {
1358
          &postJSONO($cp, $program->{seriesId});
1359
        }
1360
        if (defined($options{j}) && $cp !~ /^MV/) {
1361
          $programs{$cp}{genres}{series} = 99; 
1362
        }
1363
      }
1364
    }
1365
  }
1366
  return 0;
1367
}
1368

    
1369
sub postJSONO { 
1370
  my ($cp, $sid) = @_;
1371
  my $fn = "$cacheDir/O$cp\.js\.gz";
1372

    
1373
  if (! -e $fn && defined($sidCache{$sid}) && -e $sidCache{$sid}) {
1374
    copy($sidCache{$sid}, $fn);
1375
  }
1376
  if (! -e $fn) {
1377
    my $url = $urlRoot . 'api/program/overviewDetails';
1378
    &pout("[$treq] Post $sid: $url\n");
1379
    sleep $sleeptime; # do these rapid requests flood servers?
1380
    $treq++;
1381
    my %phash = &getZapPParams();
1382
    $phash{programSeriesID} = $sid;
1383
    $phash{'clickstream[FromPage]'} = 'TV%20Grid';
1384
    my $r = $ua->post($url, \%phash, 'X-Requested-With' => 'XMLHttpRequest'); 
1385
    my $cl = length($r->content);
1386
    $tb += $cl;
1387
    if ($r->is_success) {
1388
      $dc = Encode::encode('utf8', $r->decoded_content( raise_error => 1 ));
1389
      &wbf($fn, Compress::Zlib::memGzip($dc));
1390
      $sidCache{$sid} = $fn;
1391
    } else {
1392
      &perr($id . " :" . $r->status_line);
1393
    }
1394
  }
1395
  if (-e $fn) {
1396
    &pout("[D] Parsing: $cp\n");
1397
    my $gz = gzopen($fn, "rb");
1398
    my $buffer;
1399
    $buffer .= $b while $gz->gzread($b, 65535) > 0;
1400
    $gz->gzclose();
1401
    my $t = decode_json($buffer);
1402

    
1403
    if ($t->{seriesGenres} ne '') {
1404
      my $i = 2;
1405
      my %gh = %{$programs{$cp}{genres}};
1406
      if (keys %gh) {
1407
        my @genArr = sort { $gh{$a} <=> $gh{$b} } keys %gh;
1408
        my $max = $genArr[-1];
1409
        $i = $gh{$max} + 1;
1410
      }
1411
      foreach my $sg (split(/\|/, lc($t->{seriesGenres}))) {
1412
	if (!${$programs{$cp}{genres}}{$sg}) {
1413
          ${$programs{$cp}{genres}}{$sg} = $i++;
1414
        }
1415
      }
1416
    }
1417

    
1418
    my $i = 1;
1419
    foreach my $c (@{$t->{'overviewTab'}->{cast}}) {
1420
      my $n = $c->{name};
1421
      my $cn = $c->{characterName};
1422
      my $cr = lc($c->{role});
1423

    
1424
      if ($cr eq 'host') {
1425
        ${$programs{$cp}{presenter}}{$n} = $i++;
1426
      } else {
1427
        ${$programs{$cp}{actor}}{$n} = $i++;
1428
        ${$programs{$cp}{role}}{$n} = $cn if length($cn);
1429
      } 
1430
    } 
1431
    $i = 1;
1432
    foreach my $c (@{$t->{'overviewTab'}->{crew}}) {
1433
      my $n = $c->{name};
1434
      my $cr = lc($c->{role});
1435
      if ($cr =~ /producer/) {
1436
        ${$programs{$cp}{producer}}{$n} = $i++;
1437
      } elsif ($cr =~ /director/) {
1438
        ${$programs{$cp}{director}}{$n} = $i++;
1439
      } elsif ($cr =~ /writer/) {
1440
        ${$programs{$cp}{writer}}{$n} = $i++;
1441
      }
1442
    } 
1443
    if (!defined($programs{$cp}{imageUrl}) && $t->{seriesImage} ne '') {
1444
      my $turl = $urlAssets;
1445
      $turl .= $t->{seriesImage} . ".jpg";
1446
      $programs{$cp}{imageUrl} = $turl;
1447
    }
1448
    if ($cp =~ /^MV|^SH/ && length($t->{seriesDescription}) > length($programs{$cp}{description})) {
1449
      $programs{$cp}{description} = $t->{seriesDescription};
1450
    }
1451
    if ($cp =~ /^EP/) { # GMT @ 00:00:00
1452
      my $ue = $t->{overviewTab}->{upcomingEpisode};
1453
      if (defined($ue) && lc($ue->{tmsID}) eq lc($cp) 
1454
        && $ue->{originalAirDate} ne ''
1455
        && $ue->{originalAirDate} ne '1000-01-01T00:00Z') {
1456
          $oad = str2time2($ue->{originalAirDate}) ;
1457
          $oad *= 1000; 
1458
          $programs{$cp}{originalAirDate} = $oad;
1459
      } else {
1460
        foreach my $ue (@{$t->{upcomingEpisodeTab}}) {
1461
          if (lc($ue->{tmsID}) eq lc($cp) 
1462
		&& $ue->{originalAirDate} ne ''
1463
		&& $ue->{originalAirDate} ne '1000-01-01T00:00Z'
1464
	    ) {
1465
              $oad = str2time2($ue->{originalAirDate}) ;
1466
              $oad *= 1000; 
1467
              $programs{$cp}{originalAirDate} = $oad;
1468
              last;
1469
          }
1470
        }  
1471
      }
1472
    }
1473
  } else {
1474
    &pout("Skipping: $sid\n");
1475
  }
1476
}
1477

    
1478
sub str2time1 {
1479
  my $t = Time::Piece->strptime(shift, '%Y-%m-%dT%H:%M:%SZ');
1480
  return $t->epoch();
1481
}
1482

    
1483
sub str2time2 {
1484
  my $t = Time::Piece->strptime(shift, '%Y-%m-%dT%H:%MZ');
1485
  return $t->epoch();
1486
}
1487

    
1488
sub hourToMillis {
1489
  ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
1490
  if ($start == 0) {
1491
    $hour = int($hour/$gridHours) * $gridHours;
1492
  } else {
1493
    $hour = 0; 
1494
  }
1495
  $t = timegm(0,0,$hour,$mday,$mon,$year);
1496
  $t = $t - (&tz_offset * 3600) if !defined($options{g});
1497
  ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($t);
1498
  $t = timegm($sec, $min, $hour,$mday,$mon,$year);
1499
  return $t . "000";
1500
}
1501

    
1502
sub tz_offset {
1503
  my $n = defined $_[0] ? $_[0] : time;
1504
  my ($lm, $lh, $ly, $lyd) = (localtime $n)[1, 2, 5, 7];
1505
  my ($gm, $gh, $gy, $gyd) = (gmtime $n)[1, 2, 5, 7];
1506
  ($lm - $gm)/60 + $lh - $gh + 24 * ($ly - $gy || $lyd - $gyd)
1507
}
1508

    
1509
sub timezone {
1510
  my $tztime = defined $_[0] ? &_rtrim3(shift) : time; 
1511
  my $os = sprintf "%.1f", (timegm(localtime($tztime)) - $tztime) / 3600;
1512
  my $mins = sprintf "%02d", abs( $os - int($os) ) * 60;
1513
  return sprintf("%+03d", int($os)) . $mins;
1514
}
1515

    
1516
sub max ($$) { $_[$_[0] < $_[1]] }
1517
sub min ($$) { $_[$_[0] > $_[1]] }
1518

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