This file is indexed.

/usr/bin/tv_grab_uk_atlas is in xmltv-util 0.5.67-0.1.

This file is owned by root:root, with mode 0o755.

The actual contents of the file can be viewed below.

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
#!/usr/bin/perl -w

eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}'
    if 0; # not running under some shell
#
# Copyright G. Westcott - September 2013
#
# This code is distributed under the GNU General Public License v2 (GPLv2) .
#
#   For extended help information run
#         tv_grab_uk_atlas  --info
# 

# NOTE 2015-06-20
#  It seems Metabroadcast have (unannounced) switched off the anonymous bbc.co.uk feed. This feed was used by the XMLTV nightly tester. 
#  The only way to get Atlas data now is via a personal API key. This means the nightly automated tester will report failure although 
#  this grabber is working no problem.

my $_version 	= '$Id: tv_grab_uk_atlas,v 1.30 2015/06/22 23:46:39 knowledgejunkie Exp $';


eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}'
    if 0; # not running under some shell

use strict;
use warnings;
use constant { true => 1, false => 0 };
use Data::Dumper;

use XMLTV;
# perl-version safe $VERSION check
my ($v1,$v2,$v3) = XMLTV->VERSION =~ /^(.*?)\.(.*?)\.(.*?)$/;
if ( ($v1)+($v2/1000)+($v3/1000000) < 0.005064 ) {	
	print STDERR 'XMLTV version 0.5.64 required -- this is only version '.XMLTV->VERSION."\n";
	exit(1);
	# 0.5.64 is required for fix to 'role' attribute in credits
}

use XMLTV::ProgressBar;
use XMLTV::Options qw/ParseOptions/;
use XMLTV::Supplement qw/GetSupplement SetSupplementRoot/;
use XMLTV::Configure::Writer;
use XMLTV::Ask;
use XMLTV::Get_nice 0.005065;
use XMLTV::Date 0.005066;

use File::Path;
use POSIX qw(strftime);
use DateTime;
use Date::Parse;
use Encode;
use URI::Escape;

# Atlas can provide data in JSON and XML formsts - we use the JSON format (it's much faster than using xmltree)
use JSON::PP;

use HTTP::Cookies;
use LWP::UserAgent;
my $lwp = initialise_ua();

use subs qw(t warning);
my $warnings = 0;



# ------------------------------------------------------------------------------------------------------------------------------------- #
# Grabber details
my $VERSION 								= $_version;
my $GRABBER_NAME 						= 'tv_grab_uk_atlas';
my $GRABBER_DESC 						= 'UK - Atlas (atlas.metabroadcast.com)';
my $GRABBER_URL 						= 'http://wiki.xmltv.org/index.php/XMLTVProject';
my $ROOT_URL								= 'http://atlas.metabroadcast.com/3.0/';
my $SOURCE_NAME							= 'MetaBroadcast Atlas';
my $SOURCE_URL							= 'http://atlas.metabroadcast.com/';
#
my $generator_info_name 		= $GRABBER_NAME;
my $generator_info_url 			= $GRABBER_URL;
my $source_info_name				= $SOURCE_NAME;
my $source_info_url					= $SOURCE_URL;



# ------------------------------------------------------------------------------------------------------------------------------------- #
# Grabber limits

# Atlas now produces an error if you ask for more than 14 days' worth of data (a strict 14*24 policy; i.e. with no accounting for BST->GMT transition)
my $MAX_DAYS_TO_GRAB = 14;



# Options.pm hi-jacks the --help arg and creates its own POD synopsis!  This means we can't tell people about our added
#  parameters.  I would posit that's a bug.  Let's redefine the PrintUsage to our POD.
my ($opt, $conf);
{
	no warnings 'redefine';
	local *XMLTV::Options::PrintUsage = sub {  
		use Pod::Usage;  pod2usage(-verbose => 0)
	};
	use warnings 'redefine';
	
# ------------------------------------------------------------------------------------------------------------------------------------- #
# Use XMLTV::Options::ParseOptions to parse the options and take care of the basic capabilities that a tv_grabber should
 ($opt, $conf) = ParseOptions({ 
			grabber_name 			=> $GRABBER_NAME,
			capabilities 			=> [qw/baseline manualconfig apiconfig lineups cache/],
			stage_sub 				=> \&config_stage,
			listchannels_sub 	=> \&list_channels,
			list_lineups_sub  => \&list_lineups,
			get_lineup_sub    => \&get_lineup,
			version 					=> $VERSION,
			description 			=> $GRABBER_DESC,
			extra_options			=> [qw/hours=i date=s dst channel=s/],
			defaults					=> {'hours'=>0, 'channel'=>''}
});
}
#print Dumper($conf, $opt); exit;

# any overrides?
if (defined( $conf->{'generator-info-name'} )) { $generator_info_name = $conf->{'generator-info-name'}->[0]; }
if (defined( $conf->{'generator-info-url'} ))  { $generator_info_url  = $conf->{'generator-info-url'}->[0]; }
if (defined( $conf->{'source-info-name'} )) 	 { $source_info_name 		= $conf->{'source-info-name'}->[0]; }
if (defined( $conf->{'source-info-url'} ))  	 { $source_info_url 		= $conf->{'source-info-url'}->[0]; }



# ------------------------------------------------------------------------------------------------------------------------------------- #
# Let's play nice and use a short-term cache to reduce load on Atlas site
# Initialise the web page cache
use HTTP::Cache::Transparent;
init_cachedir( $conf->{cachedir}->[0] );
HTTP::Cache::Transparent::init( { 
    BasePath => $conf->{cachedir}->[0],
    NoUpdate => 60*60,			# cache time in seconds
		MaxAge => 4,						# flush time in hours
    Verbose => $opt->{debug},
} );


# ------------------------------------------------------------------------------------------------------------------------------------- #
# Used by the configure sub
my @platforms; my %regions;
my $selected_platform; my $selected_region;
my $platform_title; my $region_title;


# ------------------------------------------------------------------------------------------------------------------------------------- #
# Check we have all our required conf params
config_check();

# Load the conf file containing mapped channels and categories information
my %mapchannelhash;
my %mapcategoryhash;
loadmapconf();

# Load the category (genre) mappings for Press Association data
my %mapgenrehash;
loadmapgenre();



# ------------------------------------------------------------------------------------------------------------------------------------- #
# Progress Bar :)
my $bar = new XMLTV::ProgressBar({
  name => "Fetching listings",
  count => ( $opt->{'channel'} ne '' ? 1 : (scalar @{$conf->{channel}}) )
}) unless ($opt->{quiet} || $opt->{debug});



# ------------------------------------------------------------------------------------------------------------------------------------- #
# Data store before being written as XML
my $programmes = ();
my $channels = ();

# Start/Stop times for grabbing
my $starttime;
my $stoptime;

# Get the schedule(s) from Atlas
fetch_listings();

#print Dumper($programmes);

# Progress Bar
$bar->finish() && undef $bar if defined $bar;



# ------------------------------------------------------------------------------------------------------------------------------------- #
# Filter duplicate programmes
if (defined $programmes) {

$bar = new XMLTV::ProgressBar({
  name => "Filtering duplicates",
  count => scalar @{$programmes}
}) unless ($opt->{quiet} || $opt->{debug});

# Remove any duplicate programmes and set clumps where necessary
filter_listings();

# Progress Bar
$bar->finish() && undef $bar if defined $bar;

}


# ------------------------------------------------------------------------------------------------------------------------------------- #
# Generate the XML
my $encoding = 'UTF-8';
my $credits = { 'generator-info-name' => $generator_info_name,
								'generator-info-url' 	=> $generator_info_url,
								'source-info-name' 		=> $source_info_name,
								'source-info-url' 		=> $source_info_url };
	
XMLTV::write_data([ $encoding, $credits, $channels, $programmes ]);
# Finished!



# ------------------------------------------------------------------------------------------------------------------------------------- #
# Signal that something went wrong if there were warnings.
exit(1) if $warnings;

# All data fetched ok.
t "Exiting without warnings.";
exit(0);


# #############################################################################
# # THE MEAT #####################################################################
# ------------------------------------------------------------------------------------------------------------------------------------- #

sub fetch_listings {
		# Fetch listings per channel
		
		# Specific channel requested on commandline?  Else use normal conf file.
		if ($opt->{'channel'} ne '') {
				undef @{$conf->{channel}};
				push @{$conf->{channel}}, $opt->{'channel'};
		}
		
		foreach my $channel_id (@{$conf->{channel}}) {
			# 
			# Construct the url
			# http://atlas.metabroadcast.com/3.0/schedule.json?apiKey=*****************&publisher=pressassociation.com&from=now&to=now.plus.6h&channel_id=cbbh&annotations=channel,brand_summary,series_summary,extended_description,broadcasts
			# https://atlas.metabroadcast.com/3.0/schedule.json?channel_id=cbbh&publisher=bbc.co.uk&annotations=channel,description,broadcasts,brand_summary&from=2013-09-08T00:00:00.000Z&to=2013-09-09T00:00:00.000Z
			
			# -------------------------------------------------------------------------------------------------------------------------------- #
			# Get the 'from'/'to' times
			#
			# Atlas accepts from/to params of the form  "2013-09-08T00:00:00.000Z"  or like  "now.plus.6h"  or epoch times
			# This grabber accepts either  (i) --days and  --offset   or   (ii) --hours  and  --offset   or  (iii) --date YYYYMMDD
			#			(in (i) the --offset is in days; in (ii) it's hours)
			#
			my $from = '';
			my $to = '';		
			if ($opt->{offset} eq '') { $opt->{offset} = 0; }
			
			if ($opt->{date}) {
				$from = str2time( $opt->{date} );
				$to 	= $from + 86400;		# this will be wrong the days that DST changes :(   
				# if the 'clocks have gone back' we need to fetch an extra hour
				$to += 3600 if ((localtime($from))[8] == 1 && (localtime($to))[8] == 0);
				# if the 'clocks have goneforward' the day is 1 hour shorter
				$to -= 3600 if ((localtime($from))[8] == 0 && (localtime($to))[8] == 1);
			
			} elsif ($opt->{hours}) {		# test 'hours' first since 'days' has a default
				$from = DateTime->now->add( hours => $opt->{offset} )->set_time_zone('UTC')->epoch();
				$to 	= $from + ($opt->{hours} * 3600);		# todo: adjust for DST?
				
			} elsif ($opt->{days}) {
				$from = DateTime->today->add( days => $opt->{offset} )->set_time_zone('UTC')->epoch();
				$to 	= $from + ($opt->{days} * 86400);
			
				# we need to fetch a 'calendar' day so we must adjust the times for DST
				$from -= 3600 if ((localtime($from))[8] == 1);
				$to -= 3600   if ((localtime($to))[8] == 1);
	
			} else {										# unlikely to get here since 'days' has a default
				# default to today only  (todo: adjust for DST)
				$from = DateTime->today->set_time_zone('UTC')->epoch();
				$to 	= DateTime->today->add( days => 1 )->set_time_zone('UTC')->epoch();
			}
			
			# Adjust for --dst param
			$to += 3600  if ($opt->{dst});
			
			# Check we are within source site's max limit
			$to = min($to, $from + ($MAX_DAYS_TO_GRAB * 86400));
			
			# Store the Start/Stop times for grabbing
			$starttime = $from;
			$stoptime = $to;
			
			# testing above code for DST handling
			#  we're aiming for epoch times which cover a *calendar* day irrespective of DST
			if (0) {
				foreach (qw/20130330 20130331 20130401 20131026 20131027 20131028/) {
					$from = DateTime->from_epoch( epoch=>str2time( $_ )+7200 )->truncate( to => 'day' )->add( days => $opt->{offset} )->set_time_zone('UTC')->epoch();
					$to 	= $from + ($opt->{days} * 86400);
					print STDERR "$from --> $to \n";
					$from -= 3600 if ((localtime($from))[8] == 1);
					$to -= 3600   if ((localtime($to))[8] == 1);
					print STDERR "$from --> $to \n";
					print STDERR (localtime($from))[8] ." ".(localtime($to))[8] ."\n";
					print STDERR "$from --> $to \n" . DateTime->from_epoch( epoch=>$from )->set_time_zone('Europe/London')->strftime('%F %T %z').' -- '.DateTime->from_epoch( epoch=>$to )->set_time_zone('Europe/London')->strftime('%F %T %z')."\n----------\n"
				} exit;
			}
			# debug
			#print STDERR "$from --> $to \n" . DateTime->from_epoch( epoch=>$from )->set_time_zone('Europe/London')->strftime('%F %T %z').' -- '.DateTime->from_epoch( epoch=>$to )->set_time_zone('Europe/London')->strftime('%F %T %z')."\n" if $opt->{debug}; exit;
			
			
			# -------------------------------------------------------------------------------------------------------------------------------- #
			# translate the channel-id to Atlas' if it's a 'local' one
		  $channel_id = unmap_channel_id($channel_id);
			
			my $baseurl = $ROOT_URL.'schedule.json';
			my $publisher = $conf->{'publisher'}->[0] || 'pressassociation.com';	# (undocumented option needed for xmltv automatic testing)
			my $apiKey = $conf->{'api-key'}->[0];
			chomp($apiKey); chop($apiKey) if ($apiKey =~ m/\r$/);
			if ($apiKey eq '' && $publisher ne 'bbc.co.uk') {
				print STDERR 'You must obtain a free API key from http://atlas.metabroadcast.com/ before you can use this grabber'."\n";
				print STDERR 'Instructions are available at http://metabroadcast.com/blog/create-and-manage-your-atlas-api-key'."\n";
				exit(1);
			}
			$apiKey = '&apiKey='.$apiKey  if ($apiKey ne '');
			my $annotations = 'extended_description,broadcasts,series_summary,brand_summary,people,channel';

			my $url = $baseurl.'?'."channel_id=$channel_id&from=$from&to=$to&annotations=$annotations&publisher=$publisher$apiKey";
			print $url ."\n" 	if ($opt->{debug});
			#print STDERR "$url \n";
			
			if (1) {
		
				# If we need to map the fetched channel_id to a different value
				my $xmlchannel_id = $channel_id;
				if (defined(&map_channel_id)) { $xmlchannel_id = map_channel_id($channel_id); }
				my $channelname = $xmlchannel_id;
				my $channelicon = '';
				
				# Fetch the page
				my $res = $lwp->get( $url );
				
				if ($res->is_success) {
						get_schedule_from_json($xmlchannel_id, $res->content, \$channelname, \$channelicon);
				} else {
						# get the specific error if poss
						my $error = '';
						if ($res->content) {
							my $data = JSON::PP->new()->utf8(1)->decode($res->content);
							$error = $data->{'error'}->{'message'} if defined $data->{'error'}->{'message'};
						}
						# error - format as a valid http status line for cgi script
						print STDERR "Status: ".$res->status_line.($error ne ''?" ($error)":'')."\n";
				}
					
				# Add to the channels hash
				$channels->{$channel_id} = { 'id'=> $xmlchannel_id , 'display-name' => [[ codify( $channelname), 'en']]  };
				$channels->{$channel_id}->{'icon'} = [{'src' => $channelicon }]   if $channelicon;
				
				$bar->update if defined $bar;
			}
		}
}

# ------------------------------------------------------------------------------------------------------------------------------------- #

sub get_schedule_from_json {
		#  Extract the schedule for this channel.
		#
		#  Credit: Gordon M.Lack (http://birdman.dynalias.org/xmltv-from-Atlas/) for some of the original data abstraction principles used here.
		#

		my( $channel_id, $input, $channelname, $channelicon ) = @_;
		my $data = JSON::PP->new()->utf8(1)->decode($input);
		$input = undef;

		${$channelname} = $data->{'schedule'}[0]->{'channel_title'};
		${$channelicon} = $data->{'schedule'}[0]->{'channel'}->{'image'};
		
		my $prog_item = $data->{'schedule'}[0]->{'items'};
		foreach my $p (@$prog_item) {
				my %prog = %$p;
		
				my %item = ();
				
				# "What is on the item is the episode title. To get the brand title (which is normally what you will want to display in a schedule) 
				#  	you need to take the title of the parent container (which you can include using the brand_summary annotation).
				#	 	Where an item is not in a container, the item title should be used.
				#  Title is: container.title or item.title if no container
				#	 Subtitle is: item.title if container, otherwise empty"  (Jonathan Tweed)
				#
				# e.g. "title": "Ford's Dagenham Dream",  (with no "brand" container)
				#		gives title = Ford's Dagenham Dream   episode = 
				# but
				#      	"title": "Fatal Attraction",
				#			"container": { "title": "The Sky at Night", ...  "type": "brand" }
				# 	gives title = The Sky at Night   episode = Fatal Attraction
				#
				# But https://docs.metabroadcast.com/display/ATLAS/Display+title  refers to a "series" entity.  I've never seen one 
				#   (not sure if they actually mean "series_summary")  but let's try and allow for one anyway:
				#
				if ( (exists $prog{'container'}) && ( $prog{'container'}->{'type'} eq 'brand' ) && $prog{'container'}->{'title'} ) {
						$item{'title'} = $prog{'container'}->{'title'};
						$item{'episodetitle'} = '';
						
						if ( (exists $prog{'series'}) && $prog{'series'}->{'title'} && ( $prog{'series'}->{'title'} ne $prog{'container'}->{'title'} ) ) {
								$item{'episodetitle'} = $prog{'series'}->{'title'} . ': ';
						}
						if ( (exists $prog{'series_summary'}) && $prog{'series_summary'}->{'title'} && ( $prog{'series_summary'}->{'title'} ne $prog{'container'}->{'title'} ) ) {
								$item{'episodetitle'} = $prog{'series_summary'}->{'title'} . ': ';
						}
				
						if ( $prog{'title'} && ($prog{'title'} ne $prog{'container'}->{'title'} ) ) {
							$item{'episodetitle'} .= $prog{'title'};
						} elsif ( $prog{'first_broadcast'} ) {				# never seen one of these - what format is it?
							$item{'episodetitle'} .=  
									DateTime->from_epoch( epoch => str2time( $prog{'first_broadcast'} ) )->set_time_zone('Europe/London')->strftime("%d/%m/%Y");
						}
						
				} else {
						$item{'title'} = defined($prog{'title'}) ? $prog{'title'} : '';
						$item{'episodetitle'} = '';
				}

				$item{'desc'}							= defined($prog{'description'}) ? $prog{'description'} : '';
				$item{'epno'}							= defined($prog{'episode_number'}) ? $prog{'episode_number'} : '';
				$item{'seriesno'}					= defined($prog{'series_number'}) ? $prog{'series_number'} : '';
				$item{'totaleps'} 				= '';
				if ( (exists $prog{'series_summary'}) && ($prog{'series_summary'}->{'type'} eq 'series') ) {
						$item{'totaleps'} 		= defined($prog{'series_summary'}->{'total_episodes'}) ? $prog{'series_summary'}->{'total_episodes'} : '';
				}

				# get the Atlasobject "id"s
				$item{'itemid'}						= defined($prog{'id'}) ? $prog{'id'} : '';
				$item{'seriesid'}					= ( defined($prog{'series_summary'}->{'id'})  && ( $prog{'series_summary'}->{'type'} eq 'series' ) ) ? $prog{'series_summary'}->{'id'} : '';
				$item{'brandid'}					= ( defined($prog{'container'}->{'id'})  && ( $prog{'container'}->{'type'} eq 'brand' ) ) ? $prog{'container'}->{'id'} : '';
				
				$item{'image'}						= defined($prog{'image'}) ? $prog{'image'} : '';
				$item{'media'}						= defined($prog{'media_type'}) ? $prog{'media_type'} : '';
				$item{'year'}							= defined($prog{'year'}) ? $prog{'year'} : '';
				$item{'film'}							= (defined($prog{'type'}) && $prog{'type'} eq 'film') ? true : false;
				$item{'black_and_white'}	= (defined($prog{'black_and_white'}) && $prog{'black_and_white'} eq 'false') ? true : false;
				$item{'star_rating'}			= '';			# sadly not available in Atlas  :-(
				$item{'certificate'}			= ''; 
				$item{'certificate_code'}	= '';
				if (exists $prog{'certificates'}) {
						$item{'certificate'} 	= defined($prog{'certificates'}[0]->{'classification'}) ? $prog{'certificates'}[0]->{'classification'} : '';
						$item{'certificate_code'} = defined($prog{'certificates'}[0]->{'code'}) ? $prog{'certificates'}[0]->{'code'} : '';
				}
				
				# Store all the (unique) genres (aka categories). Map them to alternative name if requested.
				#
				# 6-Mar-2014
				# We use a hash to store the genres to avoid manually de-duping the list. However, using a hash means the 
				# output order of the categories is indeterminate (& varies).
				# To assist those PVRs which can only handle 1 category/genre per programme we will have to sort the keys.
				#
				# It seems the categories ("genres") added by Atlas are intended to be a top-level category (the PA ones are quite 
				# detailed (and unnecessarily so in some cases). So we'll output the Atlas genres first and then the PA ones.
				# PVRs which can handle multiple categories won't care, and those which can handle only 1 category will get a
				# top-level category (assuming Atlas have added one) rather than a sub-category.  
				# (Aside: I don't know how those single-cat PVRs cope with films; they will all come out as "Film" with no clue 
				# whatsoever as to what the film is about! That seems like a tremendous oversight IMO.)
				#
				# In an ideal world the XMLTV DTD would differentiate between top-level categories and sub-categories; but it doesn't.
				#
				# Atlas don't say if the order of the Atlas genres (when there's more than 1) is important - I think it merely reflects 
				# the order of the PA codes in their database (which in itself is random?)
				# 
				# Although the PA genres usually come first this isn't always the case (I've seen them jumbled up e.g. "Daily Politics" on BBC2)
				# Also note there might not be an Atlas category at all!
				# 
				# Atlas genres are here: https://github.com/atlasapi/atlas-model/blob/master/src/main/java/org/atlasapi/genres/AtlasGenre.java
				# PA genres are here: https://docs.metabroadcast.com/display/ATLAS/PA+Genres
				#
				#
				$item{'genres'} = {};				# use a hash so we can auto-ignore duplicate values
				my $i = 0;
				foreach my $gtext (@{$prog{'genres'}}) {
						$i++;
						if ($gtext =~ m|^http://pressassociation.com/genres/(.*)|) {
								foreach ( map_category( uc_words( map_PA_category($1) ) ) ) {
									# $item{'genres'}->{ $_ } = 1; 
									$item{'genres'}->{ $_ } = $i | 32  if !defined $item{'genres'}->{ $_ } ; 
								}
								# (nb: if genre not found then the code will be passed through to XML - this way we can spot any which are missing
						}
						elsif ( $gtext =~ m|^http://ref.atlasapi.org/genres/atlas/(.*)|) {
								foreach ( map_category( uc_words( $1 ) ) ) {
									# $item{'genres'}->{ $_ } = 1; 
									$item{'genres'}->{ $_ } = $i  if ( !defined $item{'genres'}->{ $_ } || $item{'genres'}->{ $_ } > (0 | 32) );  
								}
						}
				}
				
				
				# Get the people information
				#		(note: Although Presenter is a defined <role> it seems Commentator and Presenter are defined with <role> = 'actor' 
				#			e.g.  <character>Presenter</character> <displayRole>Actor</displayRole> <name>Suzi Perry</name> <role>actor</role>
				#					<character>Commentator</character> <displayRole>Actor</displayRole> <name>David Coulthard</name> <role>actor</role>
				#   "CrewMember" - https://github.com/atlasapi/atlas-model/blob/master/src/main/java/org/atlasapi/media/entity/CrewMember.java
				#
				# <!ELEMENT credits (director*, actor*, writer*, adapter*, producer*, composer*, editor*, presenter*, commentator*, guest* )>
				#			 
				foreach my $person (@{$prog{'people'}}) {
					SWITCH: {
							$person->{'role'} eq 'director' 			&& do { push @{$item{'directors'}}, 		$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'co-director' 		&& do { push @{$item{'directors'}}, 		$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'writer' 				&& do { push @{$item{'writers'}}, 			$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'dramatised_by'	&& do { push @{$item{'writers'}}, 			$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'adapted_by' 		&& do { push @{$item{'adapters'}}, 			$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'abridged_by' 		&& do { push @{$item{'adapters'}}, 			$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'producer'				&& do { push @{$item{'producers'}}, 		$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'composer'				&& do { push @{$item{'composers'}}, 		$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'editor'					&& do { push @{$item{'editors'}}, 			$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'presenter'			&& do { push @{$item{'presenters'}}, 		$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'commentator'		&& do { push @{$item{'commentators'}}, 	$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'participant'		&& do { push @{$item{'guest'}}, 				$person->{'name'}; last SWITCH; };
							$person->{'role'} eq 'expert'					&& do { push @{$item{'guest'}}, 				$person->{'name'}; last SWITCH; };
							
							$person->{'role'} eq 'narrator'				&& do { push @{$item{'actors'}}, 				[$person->{'name'},$person->{'character'}]; last SWITCH; };
							$person->{'role'} eq 'reporter'				&& do { push @{$item{'actors'}}, 				[$person->{'name'},$person->{'character'}]; last SWITCH; };
							$person->{'role'} eq 'actor' 					&& do { push @{$item{'actors'}}, 				[$person->{'name'},$person->{'character'}]; last SWITCH; };
					}
				}


				# Now we process all of the broadcasts of the programme and add each one to the schedule along with its per-broadcast info.
				# Atlas allow for multiple broadcasts per programme (although this seems unused at present).
				#
				foreach my $b (@{$prog{'broadcasts'}}) {
						my %bdc = %$b;			
						my %bcast = ();
						
						$bcast{'repeat'}      	  = (defined($bdc{'repeat'}) && $bdc{'repeat'} eq true) ? true : false;
						$bcast{'subtitles'}			  = (defined($bdc{'subtitled'}) && $bdc{'subtitled'} eq true) ? true : false;
						$bcast{'deaf_signed'}		  = (defined($bdc{'signed'}) && $bdc{'signed'} eq true) ? true : false;
						$bcast{'audio_described'}	= (defined($bdc{'audio_described'}) && $bdc{'audio_described'} eq true) ? true : false;
						$bcast{'high_def'}			  = (defined($bdc{'high_definition'}) && $bdc{'high_definition'} eq true) ? true : false;
						$bcast{'widescreen'}		  = (defined($bdc{'widescreen'}) && $bdc{'widescreen'} eq true) ? true : false;
						$bcast{'surround'}			  = (defined($bdc{'surround'}) && $bdc{'surround'} eq true) ? true : false;
						$bcast{'live'}      		  = (defined($bdc{'live'}) && $bdc{'live'} eq true) ? true : false;
						$bcast{'premiere'}        = (defined($bdc{'premiere'}) && $bdc{'premiere'} eq true) ? true : false;
						$bcast{'new_series'}		  = (defined($bdc{'new_series'}) && $bdc{'new_series'} eq true) ? true : false;
						$bcast{'new_episode'}		  = (defined($bdc{'new_episode'}) && $bdc{'new_episode'} eq true) ? true : false;

						$bcast{'start'} = str2time( $bdc{'transmission_time'} );
						$bcast{'stop'}  = str2time( $bdc{'transmission_end_time'} );
						
						# Note: we may need to do something about e.g. "Weather" - this comes in with a zero duration and also overlaps the previous programme
						#  
						#  "title": "BBC News", "transmission_time": "2013-10-14T17:00:00Z", "transmission_end_time": "2013-10-14T17:30:00Z",
						#  "title": "Weather", "transmission_time": "2013-10-14T17:30:00Z", "transmission_end_time": "2013-10-14T17:30:00Z",
						#  "title": "South Today",	"transmission_time": "2013-10-14T17:30:00Z", "transmission_end_time": "2013-10-14T18:00:00Z",	
						#
						# Some PVRs are going to barf on either (or both) these 
						# According to MetaBroadcast it's what PA use to signify a "follow-on" programme.  Hmm....
						
						
						# By default XMLTV grabbers should start with the first programme which *starts* within the requested period
						#   (i.e. ignore any programme already running but which started before the requested period)
						#
						if ( !defined $conf->{'overlaps'} || $conf->{'overlaps'}->[0] ne 'allow' ) {
							next if ( $bcast{'start'} < $starttime );
						}
						
						# Convert the broadcast/programme to XMLTV format
						add_programme_to_xml($channel_id, \%item, \%bcast);
				}
		}
}

# ------------------------------------------------------------------------------------------------------------------------------------- #

sub add_programme_to_xml {
		# Add a programme to the XML hash
		#
		# <!ELEMENT programme (title+, sub-title*, desc*, credits?, date?,
		#										 category*, language?, orig-language?, length?,
		#										 icon*, url*, country*, episode-num?, video?, audio?,
		#										 previously-shown?, premiere?, last-chance?, new?,
		#										 subtitles*, rating*, star-rating? )>
		# <!ATTLIST programme start     CDATA #REQUIRED
		#										stop      CDATA #IMPLIED
		#										pdc-start CDATA #IMPLIED
		#										vps-start CDATA #IMPLIED
		#										showview  CDATA #IMPLIED
		#										videoplus CDATA #IMPLIED
		#										channel   CDATA #REQUIRED
		#										clumpidx  CDATA "0/1" >
		# <!ELEMENT credits (director*, actor*, writer*, adapter*, producer*,
		#								      presenter*, commentator*, guest* )>
		# <!ELEMENT video (present?, colour?, aspect?)>
		#
	
		my ($channel_id, $item, $bcast) = @_;
		my %item = %$item;
		my %bcast = %$bcast;
		my %xmlprog = ();		
				
		$xmlprog{'channel'} 				= $channel_id;
		$xmlprog{'start'} 					= DateTime->from_epoch( epoch => $bcast{'start'} )->set_time_zone('Europe/London')->strftime("%Y%m%d%H%M%S %z");
		$xmlprog{'stop'} 						= DateTime->from_epoch( epoch => $bcast{'stop'} )->set_time_zone('Europe/London')->strftime("%Y%m%d%H%M%S %z");

		$xmlprog{'title'} 					= [[ codify( $item{'title'} ), 'en' ]];
		$xmlprog{'sub-title'} 			= [[ codify( $item{'episodetitle'} ), 'en' ]] 	if ($item{'episodetitle'});
		$xmlprog{'desc'} 						= [[ codify( $item{'desc'} ), 'en' ]] 					if ($item{'desc'});
		
		my $showepnum = make_ns_epnum($item{'seriesno'}, $item{'epno'}, $item{'totaleps'});
		$xmlprog{'episode-num'} 		= [[ $showepnum, 'xmltv_ns' ]]									if ($showepnum && $showepnum ne '..');

		# Output an additional 'episode_num' with the Atlas ids for brand, series and item.  
		# These can be used to match existing objects in your EPG (& so avoid having to do title matching etc.).
		# However some poorly written EPG processors may not allow for multiple 'episode_num' tags, so we'll let the user
		# disable this additional tag.
		if ( (not defined( $conf->{writeids} )) || ( not $conf->{writeids}->[0] =~ /no?/i ) ) {
			my $showids = $item{'brandid'} .'.'. $item{'seriesid'} .'.'. $item{'itemid'};
			push @{$xmlprog{'episode-num'}}, [ $showids, 'brand.series.episode' ]			if ($showids && $showids ne '..');
		}
	
		foreach my $role ( qw/director actor writer adapter producer composer editor presenter commentator guest/ ) {
			if ($item{($role.'s')} && scalar @{$item{($role.'s')}} > 0) {
				foreach my $showperson ( @{$item{$role.'s'}}) {
					if (ref($showperson) eq 'ARRAY') { 
						push @{$xmlprog{'credits'}{$role}}, [ codify( @{$showperson}[0] ), codify( @{$showperson}[1] ) ];
					} else {
						push @{$xmlprog{'credits'}{$role}}, codify( $showperson );
					}
				}
			}
		}
		
		$xmlprog{'date'} = $item{'year'} 														if $item{'year'};
		push @{$xmlprog{'icon'}}, {'src' => $item{'image'}} 				if $item{'image'};

		# add 'Film' genre if it's a film
		if ($item{'film'}) {
			foreach ( map_category( 'Film' ) ) {
				$item{'genres'}->{ $_ } = (0 | 32)  if ( !defined $item{'genres'}->{ $_ } || $item{'genres'}->{ $_ } > (0 | 32) );  
			}
		}
		if (scalar (keys %{$item{'genres'}}) > 0) {		
			# 6-Mar-2014:   while (my ($key, $value) = each %{$item{'genres'}}) {
			foreach ( sort { $item{'genres'}{$a} <=> $item{'genres'}{$b} } keys %{$item{'genres'}} ) {
				push @{$xmlprog{category}}, [ codify( $_ ), 'en' ];
			}
		}
			
		push @{$xmlprog{'subtitles'}}, {'type' => 'teletext'} 			if $bcast{'subtitles'};
		push @{$xmlprog{'subtitles'}}, {'type' => 'deaf-signed'} 		if $bcast{'deaf_signed'};
		# bug 489 audio described is different to deaf signed
		##push @{$xmlprog{'subtitles'}}, {'type' => 'deaf-signed'} 		if $bcast{'audio_described'};
		$xmlprog{'premiere'} = []																		if $bcast{'premiere'};
		$xmlprog{'previously-shown'} = {} 													if $bcast{'repeat'};
		$xmlprog{'new'} = {} 																				if $bcast{'new_series'} || $bcast{'new_episode'};
		$xmlprog{'video'}->{'present'} = 1 													if $item{'media'} && $item{'media'} eq 'video';
		$xmlprog{'video'}->{'present'} = 0 													if $item{'media'} && $item{'media'} eq 'audio';
		$xmlprog{'video'}->{'colour'} = 0 													if $item{'black_and_white'};
		# (v1.16) remove these tags - the Atlas database displays them for *every* 'broadcast' irrespective
		#  of whether the showing is on a HD or SD channel, etc. This is misleading.
		# $xmlprog{'video'}->{'aspect'} = '16:9' 											if $bcast{'widescreen'};
		# $xmlprog{'video'}->{'quality'} = 'HDTV' 										if $bcast{'high_def'};
		# $xmlprog{'audio'}->{'stereo'} = 'surround' 									if $bcast{'surround'};
		$xmlprog{'rating'} = [[ $item{'certificate'}, $item{'certificate_code'} ]]	if $item{'certificate'};
		$xmlprog{'star-rating'} =  [ $item{'star_rating'} . '/5' ]	if $item{'star_rating'};

		#print Dumper \%xmlprog;
		push(@{$programmes}, \%xmlprog);
		
		return;
}
						
# ------------------------------------------------------------------------------------------------------------------------------------- #

sub filter_listings {
		# Given a hash ready to be fed into XMLTV writer, perform some last minute work on the programmes:
		#   1) Remove any duplicate programmes
		#   2) Create clumps where necessary (i.e. where programmes overlap)
		#
		
		# Test scenarios (list of prog times) for clumpidx generation
		#   1800-1830  1820-1830  1830-1900
		#   1800-1830  1830-1835  1830-1900
		#   1800-1830  1800-1805  1830-1900
		#   1800-1830  1810-1815  1830-1900
		#   1800-1830  1825-1835  1830-1900
		#	
		## For author's testing purposes:
		##       require './filter_test.pl';   ($channels, $programmes) = set_test_data();
		
			
		# Walk the array (note: this assumes, (i) the programmes are stored in channel+starttime order, (ii) they have stop times )
		my ($clumpidx, $clumptot, @curr_clumps) = (0, 2, ());
		for (my $i=0; $i<scalar @{$programmes}; $i++) {
			my ($this, $next) = ($i, $i+1);			
			
			FILTER:
			
			# any more progs after this one?
			last  if ($next >= scalar @{$programmes} );
						
			# get programme's times as epoch seconds
			my $this_start = time_xmltv_to_epoch( @$programmes[$this]->{'start'} );
			my $this_stop  = time_xmltv_to_epoch( @$programmes[$this]->{'stop'} );
			my $next_start = time_xmltv_to_epoch( @$programmes[$next]->{'start'} );
			my $next_stop  = time_xmltv_to_epoch( @$programmes[$next]->{'stop'} );
			
			
			# (Task #1)
			# Is prog a duplicate with next
			#   (duplicate = same channel + same start & stop times + same title
			if ( @$programmes[$next]->{'channel'}     eq @$programmes[$this]->{'channel'}
			 &&  @$programmes[$next]->{'start'}       eq @$programmes[$this]->{'start'}
			 &&  @$programmes[$next]->{'stop'}        eq @$programmes[$this]->{'stop'}
			 &&  @$programmes[$next]->{'title'}[0][0] eq @$programmes[$this]->{'title'}[0][0] ) {
					# delete the duplicate
					splice(@{$programmes}, $next, 1);
					goto FILTER;
			}
			
			
			# (Task #2) 
			# Check times of next prog on this channel; is there an overlap?
			if ( @$programmes[$next]->{'channel'} eq @$programmes[$this]->{'channel'}
			 &&  $next_start < $this_stop ) {
			 
			  if ( !scalar @curr_clumps ) {
					@$programmes[$this]->{'clumpidx'} = $clumpidx++ .'/'. $clumptot;
					@$programmes[$next]->{'clumpidx'} = $clumpidx .'/'. $clumptot;
					push @curr_clumps, $this;		# remember the current array index
					
				} else {   # current prog is already part of a clump :(   
					# adjust rest of current clump
					$clumptot++;
					$clumpidx = 0;
					foreach (@curr_clumps) {
						@$programmes[$_]->{'clumpidx'} = $clumpidx++ .'/'. $clumptot;
					}
					@$programmes[$this]->{'clumpidx'} = $clumpidx++ .'/'. $clumptot;
					@$programmes[$next]->{'clumpidx'} = $clumpidx .'/'. $clumptot;
					push @curr_clumps, $this;		# remember the current array index
				}
						
				
			} else {
				# reset vars ready for next pass
			  ($clumpidx, $clumptot, @curr_clumps) = (0, 2, ());
			}
			
			$bar->update if defined $bar;
		}
}
# ------------------------------------------------------------------------------------------------------------------------------------- #
						
									
# #############################################################################
# # THE VEG ######################################################################
# ------------------------------------------------------------------------------------------------------------------------------------- #

sub make_ns_epnum {
		# Convert an episode number to its xmltv_ns compatible - i.e. reset the base to zero
		# Input = series number, episode number, total episodes,  part number, total parts,
		#  e.g. "1, 3, 6, 2, 4" >> "0.2/6.1/4",    "3, 4" >> "2.3."
		#
		my ($s, $e, $e_of, $p, $p_of) = @_;
		#print Dumper(@_);

		# "Part x of x" may contain integers or words (e.g. "Part 1 of 2", or "Part one")
		$p = text_to_num($p) if defined $p;
		$p_of = text_to_num($p_of) if defined $p_of;
		
		# validation check
		undef($s) if defined $s && $s eq '0';
		undef($e) if defined $e && $e eq '0';
		undef($p) if defined $p && $p eq '0';
		undef($p_of) if defined $p_of && $p_of eq '0';
		
		# re-base the series/episode/part numbers
		$s-- if (defined $s && $s ne '');
		$e-- if (defined $e && $e ne '');
		$p-- if (defined $p && $p ne '');
		
		# make the xmltv_ns compliant episode-num
		my $episode_ns = '';
		$episode_ns .= $s if (defined $s && $s ne '');
		$episode_ns .= '.';
		$episode_ns .= $e if (defined $e && $e ne '');
		$episode_ns .= '/'.$e_of if (defined $e_of && $e_of ne '');
		$episode_ns .= '.';
		$episode_ns .= $p if (defined $p && $p ne '');
		$episode_ns .= '/'.$p_of if (defined $p_of && $p_of ne '');
		
		#print "--$episode_ns--";
		return $episode_ns;
}

sub text_to_num {
		# Convert a word number to int e.g. 'one' >> '1'
		#
		my ($text) = @_;
		if ($text !~ /^[+-]?\d+$/) {	# standard test for an int
			my %nums = (one => 1, two => 2, three => 3, four => 4, five => 5, six => 6, seven => 7, eight => 8, nine => 9);
			return $nums{$text} if exists $nums{$text};
		}
		return $text
}

sub unmap_channel_id {
		# Map the requested channel_id to an Atlas value
		#
		# Since the user could ask for *any* channel (using the --channel option) we can't be
		# certain whether the channel is an 'Atlas' one or a 'mapped' one 
		# (without maintaining a list of all the channels known to Atlas which is too error-prone).
		#
		# But that's not important - we only need to check the 'mapped' list and reverse-map the id if found.
		#
		# To cater for the situation where an id exists in the map file as both an Atlas id *and* a mapped id 
		# (!!! - does this even make sense?) we'll check for an Atlas id first and *not* translate if found.
		#
		# Can't handle situation where mapped id occurs > once in map file - will just pick up the first one (alphabetically).
		#
		# Thus: (i) Check if channel_id is in 'fromchan' = return;
		#         (ii) Check if channel_id is in 'tochan' = return 'fromchan'
		#         (iii) else return
		#
		# ( c.f. map_channel_id() )
		#
		my ($channel_id) = @_;
		if (%mapchannelhash && exists $mapchannelhash{$channel_id}) { 
			return $channel_id; 
		}
		if (%mapchannelhash && ( grep { $_ eq $channel_id } values %mapchannelhash ) ) {
			while (my ($key, $value) = each %mapchannelhash) {
				if ($value eq $channel_id) {
					return $key;
				}
			}
		}
		return $channel_id;
}

sub map_channel_id {
		# Map the fetched channel_id to a different value (e.g. our PVR needs specific channel ids)
		# mapped channels should be stored in a file called  tv_grab_uk_atlas.map.conf
		# containing lines of the form:  map==fromchan==tochan  e.g. 'map==5-star==5STAR'
		#
		my ($channel_id) = @_;
		if (%mapchannelhash && exists $mapchannelhash{$channel_id}) { 
			return $mapchannelhash{$channel_id} ; 
		}
		return $channel_id;
}

sub map_category {
		# Map the fetched category to a different value (e.g. our PVR needs specific genres)
		# mapped categories should be stored in a file called  tv_grab_uk_atlas.map.conf
		# containing lines of the form:  cat==fromcategory==tocategory  e.g. 'cat==General Movie==Film'
		#
		# If the 'tocategory' is blank then the category will be removed from the programme
		#    e.g.  cat==General Movie==
		# Multiple 'tocategory' can be specified separated by ~
		#    e.g.   cat==Adventure/War==Adventure~War
		#
		my ($category) = @_;
		if (%mapcategoryhash && exists $mapcategoryhash{$category}) {  
			return split('~', $mapcategoryhash{$category} );
		}
		return split('~', $category);   # force input to be an array
}

sub map_PA_category {
		# Press Association uses codes for categories
		#		e.g. '1400' means 'Comedy'
		# Map the fetched category code to its genre
		#
		my ($category) = @_;
		if (%mapgenrehash && exists $mapgenrehash{$category}) { 
			return $mapgenrehash{$category} ; 
		}
		return $category;
}

sub loadmapconf {
		# Load the files containing mappings for channels and genres (categories)
		#
		# There are 3 files:
		#     grabber specified channel mappings
		#     grabber specified genre mappings
		#     user specified channel & genre mappings  (optional file)
		#
		# Any user specified mappings will override the corresponding grabber specified mapping.
		# 
		# There are 2 record types:
		# 	lines starting with "map" are used to 'translate' the Atlas channel id to those required by your PVR
		#			e.g. 	map==cbjc==DAVE     will output "DAVE" in your XML file instead of "cbjc"
		# 	lines starting with "cat" are used to translate categories (genres) in the Atlas data to those required by your PVR
		# 		e.g.  cat==Science Fiction==Sci-fi			will output "Sci-Fi" in your XML file instead of "Science Fiction"
		#

		my $mapchannels = \%mapchannelhash;
		my $mapcategories = \%mapcategoryhash;
		#		
		foreach ( qw/channels genres/ ) {
			
			# Retrieve grabber map file via XMLTV::Supplement
			my $supplement = GetSupplement($GRABBER_NAME, 'tv_grab_uk_atlas.map.'.$_.'.conf');
			die "Error: XMLTV $_ map data is missing, exiting"
				if (! defined $supplement || $supplement eq '');
			my @lines = split /[\r\n]+/, $supplement;
			foreach my $line (@lines) {		
				chomp $line;  chop($line) if ($line =~ m/\r$/);  trim($line);
				next if $line =~ /^#/ || $line eq '';
				
				my ($type, $mapfrom, $mapto, $trash) = $line =~ /^(.*)==(.*)==(.*?)([\s\t]*#.*)?$/;
				SWITCH: {
						lc($type) eq 'map' && do { $mapchannels->{$mapfrom} = $mapto; last SWITCH; };
						lc($type) eq 'cat' && do { $mapcategories->{$mapfrom} = $mapto; last SWITCH; };
						warning("Unknown type in map file: \n $line");
				}
			}
			
		}
		
		# Retrieve user map file from 'supplement' dir (*not* using XMLTV::Supplement)	
		#  and overwrite any corresponding grabber mappings		
		my $fn = get_supplement_dir() . '/'. $GRABBER_NAME . '.user.map.conf';
		if ( -e $fn ) {
			my $fhok = open my $fh, '<', $fn or warning("Cannot open conf file $fn");
			if ($fhok) {
				while (my $line = <$fh>) { 
					chomp $line;  chop($line) if ($line =~ m/\r$/);  trim($line);
					next if $line =~ /^#/ || $line eq '';
					
					my ($type, $mapfrom, $mapto, $trash) = $line =~ /^(.*)==(.*)==(.*?)([\s\t]*#.*)?$/;
					SWITCH: {
							lc($type) eq 'map' && do { $mapchannels->{$mapfrom} = $mapto; last SWITCH; };
							lc($type) eq 'cat' && do { $mapcategories->{$mapfrom} = $mapto; last SWITCH; };
							warning("Unknown type in map file: \n $line");
					}
				}
				close $fh;
			}
		}
		# print Dumper ($mapchannels, $mapcategories);
}

sub loadmapgenre {
		# Load the file containing mappings for Press Association categories (genres)
		#
		# This is used to convert the PA category codes into textual genre names.
		# Note these are then fed through map_category() to convert them to your personal wants, so keep
		#  the PA file generic (it will be easier to update in the future that way).
		#
		
		my $mapgenrehash = \%mapgenrehash;
		#		
		# Retrieve PA genres file via XMLTV::Supplement
    my $supplement = GetSupplement($GRABBER_NAME, 'tv_grab_uk_atlas.pa.genres.conf');
    die "Error: XMLTV PA genres data is missing, exiting"
			if (! defined $supplement || $supplement eq '');
    my @lines = split /[\r\n]+/, $supplement;
		foreach my $line (@lines) {
			chomp $line;  chop($line) if ($line =~ m/\r$/);  trim($line);
			next if $line =~ /^#/ || $line eq '';
			
			my ($mapfrom, $mapto, $trash) = $line =~ /^(.*)==(.*?)([\s\t]*#.*)?$/;
			$mapgenrehash->{$mapfrom} = $mapto;
		}
		 #print Dumper ($mapgenrehash);
}

sub list_lineups ( $ ) {
		# Returns an xml-string containing a list of all the  channel lineups for which the grabber can deliver data (in xmltv-lineups.xsd format).
		
		my $nb = 'Note: list-lineups and get-lineup is still unofficial in XMLTV, and the format and content of this xml is liable to change.';  print STDERR $nb."\n";
		
		my $opts = $_;
		fetch_platforms();
		
		# There doesn't seem to be a proc in XMLTV for writing the xmltv-lineups.xml
		#  so we'll have to roll our own
		#  (to validate use  " xmllint -noout -schema xmltv-lineups.xsd  filename " )
		
		# The lineups xml doesn't map well to the Atlas database but we'll try

		use XML::Writer;

		my $w = XML::Writer->new( 'ENCODING' => 'UTF-8', 'DATA_MODE' => 1, 'DATA_INDENT' => 1 );		# OUTPUT to stdout
		$w->xmlDecl('UTF-8');
		$w->comment($nb);
		$w->startTag('xmltv-lineups',
											'modified'						=> strftime("%FT%T %z", localtime),
											'source-info-name' 		=> $source_info_name,
											'source-info-url' 		=> $source_info_url,
											'generator-info-name' => $generator_info_name,
											'generator-info-url' 	=> $generator_info_url );
	
			foreach my $platform (@platforms) {	
				$w->startTag('xmltv-lineup', 'id' => $platform->{'id'} );
					$w->startTag('type');
					$w->characters('STB');
					$w->endTag('type');
					$w->startTag('display-name', 'lang' => 'en' );
					$w->characters( codify( $platform->{'title'} ) );
					$w->endTag('display-name');
					foreach my $country (@{$platform->{'countrieslist'}}) {					
						$w->startTag('availability', 'area' => 'country' );
						$w->characters( codify( $country ) );
						$w->endTag('availability');
					}
				$w->endTag('xmltv-lineup');
				foreach my $region (@{$platform->{'regions'}}) {		
					$w->startTag('xmltv-lineup', 'id' => $region->{'id'} );
						$w->startTag('type');
						$w->characters('STB');
						$w->endTag('type');
						$w->startTag('display-name', 'lang' => 'en' );
						$w->characters( codify( $platform->{'title'} .' - '. $region->{'title'} ) );
						$w->endTag('display-name');
						$w->startTag('availability', 'area' => 'region' );
						$w->characters( codify( $region->{'title'} ) );
						$w->endTag('availability');
					$w->endTag('xmltv-lineup');
				}
			}
		
		$w->endTag('xmltv-lineups');		
		$w->end();
		return;
}

sub get_lineup ( $$ ) {
		# Returns an xml-string describing the configured lineup (in xmltv-lineups.xsd format).
		
		my $nb = 'Note: list-lineups and get-lineup is still unofficial in XMLTV, and the format and content of this xml is liable to change.';  print STDERR $nb."\n";
		
		my ($conf, $opt) = @_;
		my @channels = fetch_channels ($conf, $opt);
		
		use XML::Writer;

		my $w = XML::Writer->new( 'ENCODING' => 'UTF-8', 'DATA_MODE' => 1, 'DATA_INDENT' => 1 );		# OUTPUT to stdout
		$w->xmlDecl('UTF-8');
		$w->comment($nb);
		$w->startTag('xmltv-lineups',
											'modified'						=> strftime("%FT%T %z", localtime),
											'source-info-name' 		=> $source_info_name,
											'source-info-url' 		=> $source_info_url,
											'generator-info-name' => $generator_info_name,
											'generator-info-url' 	=> $generator_info_url );
											
			$w->startTag('xmltv-lineup', 'id' => $conf->{'platform'}[0] );
				$w->startTag('type');
				$w->characters('STB');
				$w->endTag('type');
				$w->startTag('display-name', 'lang' => 'en' );
				$w->characters( codify( $conf->{'platform-title'}[0] ) );
				$w->endTag('display-name');
				
				foreach my $channel (@channels) {
					$w->startTag('lineup-entry');
						$w->startTag('preset');
						$w->characters($channel->{'num'});
						$w->endTag('preset');
						$w->startTag('station', 'rfc2838' => map_channel_id($channel->{'id'}) );
							$w->startTag('name', 'lang' => 'en' );
							$w->characters( codify( $channel->{'title'} ) );
							$w->endTag('name');
							$w->emptyTag('logo', 'url' => $channel->{'image'} ) if $channel->{'image'};
							if ($channel->{'media_type'} && $channel->{'media_type'} eq 'video') {
								if (defined $channel->{'high_definition'}) {
									$w->startTag('video');
										$w->startTag('format');
										$w->characters( ($channel->{'high_definition'} eq 1 ? 'HDTV' : 'SDTV') );
										$w->endTag('format');
									$w->endTag('video');
								} else {
									$w->emptyTag('video');
								}
							}
							if ($channel->{'media_type'} && $channel->{'media_type'} eq 'audio') {
								$w->emptyTag('audio');
							}
						$w->endTag('station');
						$w->startTag('stb-channel');						
							$w->startTag('stb-preset');
							$w->characters($channel->{'num'});
							$w->endTag('stb-preset');
						$w->endTag('stb-channel');
					$w->endTag('lineup-entry');
				}
		
			$w->endTag('xmltv-lineup');
		$w->endTag('xmltv-lineups');		
		$w->end();
		return;
}
		
sub list_channels ( $$ ) {
		# List all available channels on the configured 'region' (in xmltv.dtd format).
		
		my @channels = fetch_channels (@_);
		
		# We must return an xml-string (c.f. Options.pm), E.g.:
		#  	<channel id="cbbR">
    #			<display-name lang="en">BBC News Channel</display-name>
		#		</channel>
		#		<channel id="cbbT">
    #			<display-name lang="en">BBC Parliament</display-name>
		#		</channel>
		#
		# Map the list of channels to a hash XMLTV::Writer will understand
		my $channels_conf = {};
		foreach my $c (@channels) {
			my %channel = %$c;
			$channels_conf->{$channel{'num'}} = {
				'id' => $channel{'id'},
				'display-name' => [[ codify( $channel{'title'} ), 'en' ]],
			};
			$channels_conf->{$channel{'num'}}->{'icon'} = [{'src' => $channel{'image'} }]   if $channel{'image'};
		}
		#
		# Let XMLTV::Writer format the results as xml. 
		my $result;
		my $writer = new XMLTV::Writer(OUTPUT => \$result, encoding => 'UTF-8');
		$writer->start({'generator-info-name' => $generator_info_name});
		$writer->write_channels($channels_conf);
		$writer->end();
		return $result;
}

sub fetch_channels ( $$ ) {
		# Fetch Atlas' channels for a Region for a Platform
		
		# This sub is used by both --configure and --list-channels (and --get-lineup)
		# For --configure we have a Platform & Region from config_stage()
		# For --list-channels we have a Platform & Region from %conf
		# For --list-channels it's not practical to list all channels for all regions for all platforms
		#  (this would take too long (over 10 mins) and would place an unnecessary load on the Atlas server -
		#   if you really want to see all 38,242 (!) records this would generate then please see the static files
		#   in the 'data' directory on github)
		#
		
		my ($conf, $opt) = @_;

		if ($opt->{'configure'}) { 
				# temporary diversion...
				# Store some extra data in the conf file (just for info)
				#
				# Ideally we would do this in config_stage but that will only write data captured va 'Ask'
				# 	(i.e. we can't add our own data).  Neither does it have the $opt array with the config_file 
				#		name so we can't even write it manually!  The only place we can do that is here.
				#
				open OUT, ">> ".$opt->{'config-file'}
						or die "Failed to open $opt->{'config-file'} for writing";
				print OUT "platform-title=$platform_title\n";
				print OUT "region-title=$region_title\n";
				if ($conf->{'lineupcode'}[0] ne '') { 
					# if user selected a lineup then we need to manually write the platform/region (since there's no configure_stage for them)
					print OUT "platform=$selected_platform\n";
					print OUT "region=$selected_region\n";
				}
				close OUT;
				#  ...now back to the normal listchannels_sub
		}
		else { 
				$selected_platform = $conf->{'platform'}[0];
				$selected_region = $conf->{'region'}[0];
				
				# Need to load our 'map' file.  --list-channels doesn't reach that part of the code
				loadmapconf();
		}
		

		#		http://atlas.metabroadcast.com/3.0/channel_groups/cbhN.json?annotations=channels 
		#
		# Channels are a concatenation of platform-wide channels + region-specific channels 
		#
		# Platform code is $selected_platform  (captured by select-platform in config_stage()
		# Region code is $selected_region  (captured by select-region in config_stage()
		#
		my @urls = ();
		push @urls, $ROOT_URL.'channel_groups/'.$selected_platform.'.json?annotations=channels';
		push @urls, $ROOT_URL.'channel_groups/'.$selected_region.'.json?annotations=channels'  if $selected_region;

		my @channels = ();

		my $bar = new XMLTV::ProgressBar({
			name => "Fetching channels",
			count => 1
		}) unless ($opt->{quiet} || $opt->{debug});

		foreach my $url (@urls) {
			print STDERR $url ."\n" 	if ($opt->{debug});
			
			# Fetch the page
			my $res = $lwp->get( $url );
			
			if ($res->is_success) {
					#print $res->content;
					
					# Extract the available channels
					my $data = JSON::PP->new()->utf8(1)->decode($res->content);
					$res = undef;

					my $channels = $data->{'channel_groups'}[0]->{'channels'};
					foreach my $c (@$channels) {
							my %chan = %$c;
							next unless ($chan{'channel'}->{'type'} eq 'channel');
					
							my %channel = ();
					
							$channel{'num'} 	= $chan{'channel_number'};
							$channel{'id'} 		= $chan{'channel'}->{'id'};
							$channel{'title'} = $chan{'channel'}->{'title'};
							$channel{'image'} = $chan{'channel'}->{'image'};
							$channel{'media_type'} = $chan{'channel'}->{'media_type'};
							$channel{'high_definition'} = $chan{'channel'}->{'high_definition'};
							
							if ($opt->{'list-channels'}) { 
								# if the user has a 'map' file then map the Atlas channel_id to the user's one (since this is the one which will be
								#  displayed in xml listings)
								if (defined(&map_channel_id)) { $channel{'id'} = map_channel_id($channel{'id'}); }
							}
					
							push @channels, \%channel;
					}
					
			} else {
					print $res->status_line . "\n";
			}
			
		}
	
		$bar->update() && $bar->finish && undef $bar if defined $bar;
		
		#print Dumper(@channels);exit;
		return @channels;
}

sub fetch_platforms () {
		# Fetch Atlas' channel_groups
		
		# (note: if called during --configure then $opt & $conf have not been returned by ParseOptions() yet, since that hasn't exited yet)
		
		#		http://atlas.metabroadcast.com/3.0/channel_groups.json?type=platform
		my $url = $ROOT_URL.'channel_groups.json?type=platform';
		#print STDERR $url ."\n";

		@platforms = ();
		undef %regions;

		my $bar = new XMLTV::ProgressBar({
			name => "Fetching platforms",
			count => 1
		}) unless ($opt->{quiet} || $opt->{debug});

		# Fetch the page
		my $res = $lwp->get( $url );
		
		if ($res->is_success) {
				#print $res->content;
				
				# Extract the available platforms
				my $data = JSON::PP->new()->utf8(1)->decode($res->content);
				$res = undef;

				my $channel_group = $data->{'channel_groups'};
				foreach my $g (@$channel_group) {
						my %group = %$g;
						next unless ($group{'type'} eq 'platform');
				
						my %platform = ();
				
						$platform{'id'} = $group{'id'};
						$platform{'title'} = $group{'title'};
						
						$platform{'countrieslist'} = $group{'available_countries'};
						$platform{'countries'} = '';
						foreach my $country (@{$group{'available_countries'}}) {
							$platform{'countries'} .= '(' . $country . ')';
						}
						
						$platform{'regions'} = ();
						foreach my $region (@{$group{'regions'}}) {
							push @{$platform{'regions'}},  { 'id' => $region->{'id'}, 'title' =>  $region->{'title'} };
							$regions{$region->{'id'}} = { 'title' => $region->{'title'}, 'platform_id' => $platform{'id'}, 'platform_title' => $platform{'title'} };
						}
						
						push @platforms, \%platform;
				}
				
		} else {
				print $res->status_line . "\n";
		}
	
		$bar->update() && $bar->finish && undef $bar if defined $bar;
		
		#print Dumper(@platforms);exit; 
		#print Dumper(\%regions);exit;
		return;
}

sub config_stage ( $$ ) {
		my ( $stage, $conf ) = @_;				# note that $conf is mostly empty at this stage of course

		my $result;
		my $writer = new XMLTV::Configure::Writer( OUTPUT => \$result, encoding => 'UTF-8' );
		$writer->start( { grabber => $GRABBER_NAME } );
			
		# ------------------------------------------------------------------ #
		if ( ($stage eq 'start') ||
			 ($stage eq 'select-cache') ) {
			 
        $writer->write_string( {
						id => 'cachedir', 
						title => [ [ 'Directory to store the cache', 'en' ] ],
						description => [ [ $GRABBER_NAME.' uses a cache with files that it has already downloaded. Please specify a location for this cache.', 'en' ] ],
						default => get_default_cachedir(),
        } );
				
        $writer->end('select-apikey');
				
    }	
		# ------------------------------------------------------------------ #
		elsif ($stage eq 'select-apikey') {
			 
        $writer->write_string( {
						id => 'api-key', 
						title => [ [ 'Enter your Atlas API key', 'en' ] ],
						description => [ [ $GRABBER_NAME.' requires an API key which you must obtain from MetaBroadcast.com', 'en' ] ],
						default => '',
        } );
				
        $writer->end('select-option');
				
    }	
		# ------------------------------------------------------------------ #
		elsif ($stage eq 'select-option') {
			 
        $writer->write_string( {
						id => 'writeids', 
						title => [ [ 'Output brand/series/summary ids? (yes/no)', 'en' ] ],
						description => [ [ $GRABBER_NAME.' normally outputs an episode-num tag with brand.series.episode ids from the Atlas database.', 'en' ] ],
						default => 'yes',
        } );
				
        $writer->end('select-lineup');
				
    }	
		# ------------------------------------------------------------------ #
    elsif ($stage eq 'select-lineup') {

				# I don't like the way the "lineup" functionality is supposed to work:
				#    If user selects a lineup then we are supposed to get ALL channels for that lineup.  Every time.  No exception
				#    Not only does that mean (i) we have to do a fetch of the channels for the lineup EVERY time the grabber is run,
				#    (since the channels for the lineup would *not* be stored in the config file)
				#    but (ii) we are potentially grabbing 600 channels *just because* the user selected a lineup -- not only does this
				#    place a load on the data source, but it might not even be what the user needs.
				#    AIUI "lineup" is so a user's PVR can grab data in a (semi-) automated way.  But that doesn't mean that just because
				#    I am using MythTV that I want to get all 600 channels!  (Especially if it's a paid for service where you pay per channel
				#    downloaded.)
				#    Therefore I'm going to still run the channels selector.  If a user/PVR software really wants all 600 channels then all
				#    they have to do is send "all" in response to the subsequent question.
				
				$writer->write_string( {
						id => 'lineupcode', 			# don't use 'lineup' or else we don't run the channels selector
						title => [ [ 'Enter your viewing region', 'en' ] ],
						description => [ [ 'Enter the region code you want or leave blank to select from a list', 'en' ] ],
						default => '',
        } );
				
				$writer->end('check-lineup');
				
    }		
		# ------------------------------------------------------------------ #
    elsif ($stage eq 'check-lineup') {
				
				fetch_platforms();
				
				if ($conf->{'lineupcode'}[0] ne '') {
					# selected 'code' could be a platform OR a region
					my $lineupcode = $conf->{'lineupcode'}[0];
					{ LOOP:
						foreach my $platform (@platforms) {
							if ($platform->{'id'} eq $lineupcode) {
								$selected_platform = $platform->{'id'};							
								$platform_title = $platform->{'title'};
								$selected_region = '';	
								$region_title = '';
								last LOOP;
							}
							foreach my $region (@{$platform->{'regions'}}) {
								if ($region->{'id'} eq $lineupcode) {
									$selected_platform = $platform->{'id'};							
									$platform_title = $platform->{'title'};
									$selected_region = $region->{'id'};
									$region_title = $region->{'title'};
									last LOOP;
								}
							}
						}
					}
				
					if (defined $selected_platform && $selected_platform ne '') {		
						$writer->end('select-channels');
					}
				}
				else 
				{
        $writer->end('select-platform');
				}
				
    }	
		# ------------------------------------------------------------------ #
    elsif ($stage eq 'select-platform') {

				#fetch_platforms();
				
        $writer->start_selectone( {
            id => 'platform',
            title => [ [ 'Choose your viewing platform', 'en' ] ],
            description => [ [ $GRABBER_NAME.' selects channels to download based on your viewing platform.', 'en' ] ],
        } );
				
				foreach my $p (@platforms) {
						my %platform = %$p;
		
						$writer->write_option( {
								value => $platform{'id'},
								text => [ [ $platform{'title'} . ' ' . $platform{'countries'}, 'en' ] ],
						} );
				}
				
        $writer->end_selectone();
				$writer->end('select-region');
				
    }		
		# ------------------------------------------------------------------ #
    elsif ($stage eq 'select-region') {
				
				# store platform selected in previous stage				
				$selected_platform = $conf->{'platform'}[0];
				
        $writer->start_selectone( {
            id => 'region',
            title => [ [ 'Choose your viewing region', 'en' ] ],
            description => [ [ $GRABBER_NAME.' selects channels to download based on your TV region.', 'en' ] ],
        } );
			
				foreach my $p (@platforms) {
						my %platform = %$p;
						next unless $platform{'id'} eq $conf->{'platform'}[0];
						
						# may not be a regionalised platform (e.g. Saorview)
						if ( (!defined $platform{'regions'}) || (scalar @{$platform{'regions'}} == 0) ) {		
			
									$writer->write_option( {
											value => $platform{'id'},
											text => [ [ $platform{'title'}, 'en' ] ],
									} );
						
						} else {
						
							foreach my $r (@{$platform{'regions'}}) {
									my %region = %$r;
			
									$writer->write_option( {
											value => $region{'id'},
											text => [ [ $region{'title'}, 'en' ] ],
									} );
									
							}
							
						}
				}
				
        $writer->end_selectone();
				$writer->end('clean-up');

    }
		# ------------------------------------------------------------------ #
    elsif ($stage eq 'clean-up') {
		
				# Store some extra data in the conf file (just for info)
				#
				# Can't use $conf for this since configure_stage() only writes values it collects (i.e. it doesn't write the hash itself)
				#
				
				foreach my $p (@platforms) {
						my %platform = %$p;
						next unless $platform{'id'} eq $conf->{'platform'}[0];
						
						#$conf->{'platform-title'} = [ $platform{'title'} ];
						$platform_title = $platform{'title'};
						$region_title = '';
						
						foreach my $r (@{$platform{'regions'}}) {
								my %region = %$r;
								next unless $region{'id'} eq $conf->{'region'}[0];
							
								#$conf->{'region-title'} = [ $region{'title'} ];
								$region_title = $region{'title'};
								last;
						}
						
						last;
				}

				# Store the selected platform & region for use by 'select-channels'
				$selected_platform = $conf->{'platform'}[0];
				$selected_region = $conf->{'region'}[0];
				$writer->end('select-channels');
				
		}
		# ------------------------------------------------------------------ #
    else {
        die "Unknown stage $stage";
    }

		# ------------------------------------------------------------------ #
		return $result;
}

sub config_check {
		if (not defined( $conf->{cachedir} )) {
				print STDERR "No cachedir defined in config file " . 
										 $opt->{'config-file'} . "\n" .
										 "Please run the grabber with --configure.\n";
				exit 1;
		}

		if (not defined( $conf->{'channel'} )) {
				print STDERR "No channels selected in config file " .
										 $opt->{'config-file'} . "\n" .
										 "Please run the grabber with --configure.\n";
				exit 1;
		}

		if (not defined( $conf->{'api-key'} )) {
				print STDERR "No api-key defined in config file " .
										 $opt->{'config-file'} . "\n" .
										 "Please run the grabber with --configure.\n";
				exit 1;
		}
}

sub get_default_dir {
    my $winhome = $ENV{HOMEDRIVE} . $ENV{HOMEPATH} 
			if defined( $ENV{HOMEDRIVE} ) 
					and defined( $ENV{HOMEPATH} ); 
    
    my $home = $ENV{HOME} || $winhome || ".";
    return $home;
}

sub get_supplement_dir {
		return $ENV{XMLTV_SUPPLEMENT} . "/" . $GRABBER_NAME  if defined( $ENV{XMLTV_SUPPLEMENT} );
    return get_default_dir() . "/.xmltv/supplement/" . $GRABBER_NAME;
}

sub get_default_cachedir {
    return get_default_dir() . "/.xmltv/cache";
}

sub init_cachedir {
    my( $path ) = @_;
    if( not -d $path ) {
        mkpath( $path ) or die "Failed to create cache-directory $path: $@";
    }
}

sub initialise_ua {
		my $cookies = HTTP::Cookies->new;
		#my $ua = LWP::UserAgent->new(keep_alive => 1);
		my $ua = LWP::UserAgent->new;
		# Cookies
		$ua->cookie_jar($cookies);
		# Define user agent type
		$ua->agent('Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0');
		# Define timouts
		$ua->timeout(240);
		# Use proxy if set in http_proxy etc.
		$ua->env_proxy;
		
		return $ua;
}

sub codify ( $ ) {
		# Encode the text as UTF-8
		return encode( 'UTF-8', $_[0] );
}

sub uc_words {
	# Uppercase the first letter of each word
	my ($string) = @_;
	$string =~ s/\b(\w)/\U$1/g;
	return $string;
}

sub trim {
	# Remove leading & trailing spaces
	$_[0] =~ s/^\s+|\s+$//g;       
}

sub max ($$) { 
  return $_[$_[0] < $_[1]];
}

sub min ($$) { 
  return $_[$_[0] > $_[1]];
}

sub t {
    my( $message ) = @_;
    print STDERR $message . "\n" if $opt->{debug};
}

sub warning {
    my( $message ) = @_;
    print STDERR $message . "\n";
    $warnings++;
}

# #############################################################################

__END__

=pod

=head1 NAME

tv_grab_uk_atlas - Grab TV and radio programme listings for UK from MetaBroadcast website (Atlas database).

=head1 SYNOPSIS

tv_grab_uk_atlas --help

tv_grab_uk_atlas --info
  
tv_grab_uk_atlas --version

tv_grab_uk_atlas --capabilities

tv_grab_uk_atlas --description

tv_grab_uk_atlas 
           [--days N] [--offset N] [--dst]
					 [--channel S]
					 [--config-file FILE]
           [--output FILE] [--quiet] [--debug]

tv_grab_uk_atlas 
           --hours N [--offset N]
					 [--channel S]
					 [--config-file FILE]
           [--output FILE] [--quiet] [--debug]

tv_grab_uk_atlas 
           --date DATE [--dst]
					 [--channel S]
					 [--config-file FILE]
           [--output FILE] [--quiet] [--debug]

tv_grab_uk_atlas --configure [--config-file FILE]

tv_grab_uk_atlas --configure-api [--stage NAME]
           [--config-file FILE]
           [--output FILE]

tv_grab_uk_atlas --list-channels [--config-file FILE]
           [--output FILE] [--quiet] [--debug]

tv_grab_uk_atlas --list-lineups [--output FILE]
           [--quiet] [--debug]

tv_grab_uk_atlas --get-lineup [--config-file FILE] [--output FILE]
           [--quiet] [--debug]
					 
=head1 DESCRIPTION

Output TV listings in XMLTV format for many channels available in UK.
The data come from L<http://atlas.metabroadcast.com>

First you must run B<tv_grab_uk_atlas --configure> to choose which channels
you want to receive.

Then running B<tv_grab_uk_atlas> with no arguments will get a listings in XML
format for the channels you chose for available days including today.

=head1 OPTIONS

B<--configure> Prompt for which channels to download and write the
configuration file.

B<--config-file FILE> Set the name of the configuration file, the
default is B<~/.xmltv/tv_grab_uk_atlas.conf>.  This is the file written by
B<--configure> and read when grabbing.

B<--output FILE> When grabbing, write output to FILE rather than
standard output.

B<--hours N> When grabbing, grab N hours of data.

B<--days N> When grabbing, grab N days rather than all available days.

B<--offset N> Start grabbing at today/now + N days.  When B<--hours> is used
this is number of hours instead of days.  N may be negative.

B<--date N> Grab just this date (instead of days/offset).

B<--dst> Some PVRs have trouble with BST times and "lose" an hour at the end 
of the day schedule.  This adds an extra hour to the schedule fetched.

B<--channel S> Grab just this channel (ignore the channels in the config file). 
Can be specified either as Atlas channel id (e.g. "cbbw") or mapped 
channel name (e.g. "south.bbc1.bbc.co.uk").

B<--quiet> Suppress the progress-bar normally shown on standard error.

B<--debug> Provide more information on progress to stderr to help in
debugging.

B<--list-channels> Write output giving <channel> elements for every
channel available in the current configuration.

B<--list-lineups> Write output giving list of available viewing regions.

B<--get-lineup> Write output giving <channel> elements for every
channel available in the current lineup.

B<--capabilities> Show which capabilities the grabber supports. For more
information, see L<http://wiki.xmltv.org/index.php/XmltvCapabilities>

B<--version> Show the version of the grabber.

B<--help> Print a help message and exit.

B<--info> Print a help page and exit.

=head1 INSTALLATION

1.
First you must obtain an Atlas API key from MetaBroadcast.  This is free 
(at the present time) for personal use and is necessary to allow you to 
access the full schedule published by the Press Association.  Any commercial 
use of the data obtained from Atlas will require a paid-for license. 

Instructions are available here: 
L<http://metabroadcast.com/blog/create-and-manage-your-atlas-api-key>

You will need to enter your API key during the --configure stage.

Remember to request Press Association (PA) as the content provider on your
Atlas API key. 

2.
Grabber configuration consists of the usual:  
B<tv_grab_uk_atlas --configure>

Atlas have regionalised data which means you can obtain listings specific to your 
TV region.  When you run the --configure option you will be asked which viewing 
platform (e.g. Freeview; Sky HD) and region (e.g. London; South East) you wish 
to access.  (You can select only 1 region!).

Then you select which channels you want to fetch.

3.
The file F<tv_grab_uk_atlas.user.map.conf> has two purposes.  Firstly you can map 
the channel ids used by Atlas into something more meaningful to your PVR.  E.g.

      map==cbdm==FILM4

will change "cbdm" to "FILM4" in the output XML.

Note: the lines are of the form "map=={channel id}=={my name}".

The second purpose is to likewise translate genre names.  So if your PVR doesn"t 
have a category for "Science Fiction" but uses "Sci-fi" instead, then you can 
specify

      cat==Science Fiction==Sci-fi

and the output XML will have "Sci-fi".


IMPORTANT: the downloaded "tv_grab_uk_atlas.user.map.conf" contains example lines 
to illustrate the format - you should edit this file to suit your own purposes!

=head1 USAGE

All the normal XMLTV capabilities are included but there is an additional parameter 
"--hours".  Atlas allows schedule data to be retrieved either by number-of-days or 
by number-of-hours.

Where possible you should use this "hours" facility to reduce unnecessary load on 
the Atlas server.

When --hours is specified the --offset is interpreted as hours.

For example:

   - to retrieve a schedule for the next 12 hours:

        tv_grab_uk_atlas --hours 12

   - to retrieve a schedule for the next 12 hours starting tomorrow:

        tv_grab_uk_atlas --hours 12 --offset 24


Alternatively you can use the familiar "days" format:

        tv_grab_uk_atlas --days 1

        tv_grab_uk_atlas --days 1 --offset 1


Negative numbers are allowed, so for example the following are valid:

        tv_grab_uk_atlas --offset -4 --hours 12

        tv_grab_uk_atlas --offset -1 --days 1


Note that Atlas only has data for a maximum 14 days ahead and it varies; some 
channels have less than this.


An additional parameter "--date YYYYMMDD" allows you to fetch the schedule 
just for this date. (This is obviously similar to --days 1 with an appropriate 
--offset but avoids you having to calculate the offset; this is easier for 
some automated fetchers.)  E.g.

        tv_grab_uk_atlas --date 20130923


A new parameter "--dst" allows you to add an extra hour to the schedule fetched 
from Atlas.  Some PVRs have trouble with BST times and "lose" an hour at the 
end of the day"s schedule.  This parameter might help to alleviate that.

        tv_grab_uk_atlas --days 1 --dst


A new parameter "--channel" allows you to override the config file and retrieve 
data for a specific channel:

        tv_grab_uk_atlas --offset -4 --hours 12 --channel cbdm


=head1 BROWSER INTERFACE

If you wish to run the fetcher via a browser then you can install the supplied 
cgi script.

(Note you must obviously have a webserver installed or be using a web-host for 
this to work.)

Copy the file F<getatlas.pl> into the cgi-enabled directory on your web space.  
(Hint: This directory is often called "cgi-bin".)  Ensure the file has execute 
permission.

(Tech note: use a "normal" cgi handler to run this file; fastcgi may not work.)

Specify the parameters on the URI as follows:
   offset=xxxx
   hours=xxxx
   days=xxxx
   date=YYYYMMDD
   channel=xxxx
   dst

   e.g.   
      http://my.webspace.com/cgi-bin/getatlas.pl?hours=12
      http://my.webspace.com/cgi-bin/getatlas.pl?hours=12&offset=6
      http://my.webspace.com/cgi-bin/getatlas.pl?date=20130930
      http://my.webspace.com/cgi-bin/getatlas.pl?days=1&dst

Valid combinations are:
      "offset" and "hours"
  or  "offset" and "days"     - in which case the offset is "days" also (otherwise 
it"s "hours")
  or  "date"                  - fetch just this day


=head1 ERROR HANDLING

If the grabber fails to download data for some channel on a specific day, 
it will print an errormessage to STDERR and then continue with the other
channels and days. The grabber will exit with a status code of 1 to indicate 
that the data is incomplete. 

=head1 ENVIRONMENT VARIABLES

The environment variable HOME can be set to change where configuration
files are stored. All configuration is stored in $HOME/.xmltv/. On Windows,
it might be necessary to set HOME to a path without spaces in it.

=head1 SUPPORTED CHANNELS

For information on supported channels, see the tv_grab_uk_atlas.map.channels.conf
file in your $HOME/.xmltv/supplement/tv_grab_uk_atlas/ directory after you have
run the grabber at least once.

=head1 XMLTV VALIDATION

B<tv_validate_grabber> may report an error similar to:

      "Line 5 Invalid channel-id BBC 1"

This is a because ValidateFile.pm insists the channel-id adheres to RFC2838 
despite the xmltv.dtd only saying "preferably" not "SHOULD".  
(Having channel ids of the form "bbc1.bbc.co.uk" will be rejected by many PVRs 
since they require the data to match their own list.)

This error can be ignored.

=head1 FAQs

1.  What does "Enter your Atlas API key" mean?

You must obtain an API key free from Atlas MetaBroadcast before you can use 
this grabber. Instructions are available here: 
L<https://metabroadcast.com/blog/create-and-manage-your-atlas-api-key>

2.  I"m getting the error "Status: 400 Bad Request"

Typically this is because you haven"t entered your API key during the 
--configure stage.
Or your API key does not allow access to Press Association data (log-in to your 
account at http://atlas.metabroadcast.com/admin and "Request Access" to "PA" 
source data).
Or if you requested a particular channel with the --channel option but the 
channel cannot be found.
    
 
=head1 DISCLAIMER

The MetaBroadcast free license for these data does not allow non-personal use.

Certainly any commercial use of listings data obtained by using this grabber 
will breach copyright law, but if you are just using the data for your own 
personal use then you are probably fine.

By using this grabber you aver you are using the listings data for your own 
personal use only and you absolve the author(s) from any liability under 
copyright law or otherwise.

=head1 AUTHOR

Geoff Westcott. This documentation and parts of the code
based on various other tv_grabbers from the XMLTV-project.

=head1 COPYRIGHT

Copyright (c) 2013 Geoff Westcott.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
(version 2) as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

=head1 SEE ALSO

L<xmltv(5)>.

=cut