root/trunk/grabbers/ltd

Revision 1, 38.0 kB (checked in by max, 6 years ago)

Initial import

Line 
1#!/usr/bin/perl
2
3#
4# au tv guide grabber -
5#  written by ltd
6#  * uses yahoo7 widget for ABC/7/9/10/SBS
7#  * uses ABC TV Guide (http://www.abc.net.au/tv/guide/abc2/) for ABC2 data
8#  * can use yahoo7 portal for SBSNEWS/Foxtel/Optus data
9#  uses caching to reduce query load on server
10#   loosely based on Michael 'Immir' Smith's excellent 9MSN tv_grab_au
11#
12#   IMPORTANT NOTE:
13#    this does NOT use any config file
14#    all region/channel settings are hardcoded below - please set them!
15#
16
17#  changelog:
18#    1.30  03aug06      initial public release
19#    1.40  13sep06      figured out how to grab >1 hour of widget data at a time --
20#                       reduced 168 GETs to 1 GET request,
21#                       added abc2 fetchers to remove requirement for using
22#                       yahoo7 portal altogether
23#    1.41  13sep06      add support for ABC data from ABC website also (can get 30 days)
24#    1.50  22sep06      added support for "shepherd" master grabber script
25#    1.51  02oct06      --ready option
26
27$progname = "tv_grab_au_ltd";
28$version = "1.51_02oct06";
29
30use LWP::UserAgent;
31use Time::HiRes qw(gettimeofday tv_interval);
32use XMLTV;
33use XML::DOM;
34use XML::DOM::NodeList;
35use POSIX qw(strftime mktime);
36use Getopt::Long;
37use HTML::TreeBuilder;
38use Data::Dumper;
39
40# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
41
42# NOTE NOTE NOTE NOTE NOTE
43# NOTE NOTE NOTE NOTE NOTE   If you are using 'shepherd' to call this script, you don't need to
44# NOTE NOTE NOTE NOTE NOTE   configure _ANY_ of the options below - you can leave these as-is &
45# NOTE NOTE NOTE NOTE NOTE   shepherd will pass in all the correct stuff
46# NOTE NOTE NOTE NOTE NOTE
47
48# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
49
50# start of manual settings - ONLY SET THESE IF YOU AREN'T USING SHEPHERD OR A channels.conf file!
51
52sub do_manual_settings {
53
54# set region appropriate for where we want to get data:
55#    VIC: Melbourne(94), Geelong(93), Mildura/Sunraysia(95),
56#         Eastern Victoria(90), Western Victoria(98)
57#    NSW: Sydney(73), Broken Hill(63), Central Coast(66), Griffith(67),
58#         Northern NSW(69), Southern NSW(71), Remote and Central(106)
59#    QLD: Brisbane(75), Gold Coast(78), Regional QLD(79), Remote and Central(114)
60#    WA:  Perth(101), Regional WA(102)
61#    SA:  Adelaide(81), Renmark(82), Riverland(83), South East South Australia(85),
62#         Spencer Gulf(86), Remote and Central(107)
63#    NT:  Darwin(74), Remote and Central(108)
64#    ACT: ACT(73)
65#    TAS: Tasmania(88)
66# $region = 94;
67
68# 1. widget mappings:
69
70#    the channel names ABC, Seven, Nine, TEN, SBS are hardcoded in xml received from yahoo7 widget data
71#    the mappings here are to map to whatever xmltv tags you are using or leave blank if you don't want
72#    data for that channel
73# $y7w_channel_id{"ABC"} =                      "abcvic.free.au";
74# $y7w_channel_id{"Seven"} =                    "channelsevenmelbourne.free.au";
75# $y7w_channel_id{"Nine"} =                     "channelninemelbourne.free.au";
76# $y7w_channel_id{"TEN"} =                      "networktenmelbourne.free.au";
77# $y7w_channel_id{"SBS"} =                      "sbsmelbourne.free.au";
78
79# 2. ABC2 TV Guide (http://www.abc.net.au/tv/guide/)
80#    set xml id appropriately
81# (can grab ABC data from either widget or ABC site; ABC site means you can get up to 30 days of data but
82# ironically the data isn't quite as good)
83# $abc_xmlid =                                  "abcvic.free.au";
84# $abc2_xmlid =                                 "abc2.abc.gov.au";
85
86# 3. yahoo portal mappings
87#    leave blank if you don't want data for any of these channels
88# $y7p_channel_id{"272787,SBS NEWS"} =          "news.sbs.com.au";
89# $y7p_channel_id{"251261,FOX W"} =             "";                     # Foxtel
90# $y7p_channel_id{"267072,Fox Footy Channel"} = "";                     # Foxtel
91# $y7p_channel_id{"251349,Hallmark"} =          "";                     # Foxtel
92# $y7p_channel_id{"251262,FOX8"} =              "";                     # Foxtel
93# $y7p_channel_id{"251351,Fox Classics"} =      "";                     # Foxtel
94# $y7p_channel_id{"251264,The Lifestyle Channel"} = "";                 # Foxtel
95# $y7p_channel_id{"251352,National Geographic"} =   "";                 # Foxtel
96# add any more you wish, visit http://au.tv.yahoo.com/results.html and use
97# 'vn' (venue) number from channel column...
98
99}
100
101# end of settings
102# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
103
104#
105# some initial cruft
106#
107
108$script_start_time = [gettimeofday];
109
110# lets make sure we look exactly like the yahoo widget engine...
111my $ua;
112BEGIN {
113        $ua = LWP::UserAgent->new(
114                'timeout' => 30,
115                'keep_alive' => 1,
116                'agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-us)'
117                );
118        $ua->env_proxy;
119        # $ua->cookie_jar({});
120        $| = 1;
121}
122
123#
124# parse command line
125#
126
127my $opt_days =          7;                                      # default
128my $opt_offset =        0;                                      # default
129my $opt_timezone =      "1000";                                 # default
130my $opt_outputfile =    ""; # /var/local/tv_grab_au/guide.xml"; # default
131my $opt_cache_file =    "/var/local/tv_grab_au/cache.dat";      # default
132my $opt_configfile =    "";                                     # ignored
133my $opt_channels_file=  "";
134my $opt_fast =          0;
135my $opt_warper =        0;
136my $opt_obfuscate =     0;
137my $opt_no_cache =      0;
138my $opt_no_detail =     0;
139my $opt_help =          0;
140my $opt_version =       0;
141my $opt_desc =          0;
142my $opt_dont_fetch_widget = 0;
143my $opt_dont_fetch_portal = 0;
144my $opt_dont_fetch_abc_data = 0;
145my $opt_dont_fetch_abc_extra_days = 0;
146my $opt_dont_retry =    0;
147my $debug =             1;
148my $lang =              "en";
149my $opt_shepherd =      0;
150$region = 94;
151
152GetOptions(
153        'shepherd'      => \$opt_shepherd,
154        'region=i'      => \$region,
155        'days=i'        => \$opt_days,
156        'offset=i'      => \$opt_offset,
157        'timezone=s'    => \$opt_timezone,
158        'channels_file=s' => \$opt_channels_file,
159        'output=s'      => \$opt_outputfile,
160        'config-file=s' => \$opt_configfile,
161        'cache-file=s'  => \$opt_cache_file,
162        'no-cache'      => \$opt_no_cache,
163        'fast'          => \$opt_fast,
164        'debug+'        => \$debug,
165        'warper'        => \$opt_warper,
166        'no-widget-data' => \$opt_dont_fetch_widget,
167        'no-portal-data' => \$opt_dont_fetch_portal,
168        'no-abc-data'   => \$opt_dont_fetch_abc_data,
169        'no-abc-extra-days' => \$opt_dont_fetch_abc_extra_days,
170        'lang=s'        => \$lang,
171        'obfuscate'     => \$opt_obfuscate,
172        'no-detail'     => \$opt_no_detail,
173        'no-retry'      => \$opt_dont_retry,
174        'help'          => \$opt_help,
175        'verbose'       => \$opt_help,
176        'version'       => \$opt_version,
177        'ready'         => \$opt_version,
178        'desc'          => \$opt_desc,
179        'v'             => \$opt_help);
180
181$random_sleep = ($opt_fast ? 0 : 1);
182&help if ($opt_help);
183
184if ($opt_version || $opt_desc) {
185        printf "%s %s\n",$progname,$version;
186        printf "tv_grab_au_ltd is a details-aware grabber that collects data using the Yahoo7 widget, Yahoo7 web portal and ABC websites. The data collected via the widget (ABC/7/9/10/SBS) is of very high quality with full title/subtitle/description/genre and year/cast/credits data.  PayTV and ABC2/SBSNEWS data is of lower quality due to the web portals used." if $opt_desc;
187        exit(0);
188}
189
190#
191# go go go!
192#
193
194($starttime,$endtime) = &calc_dates(time,$opt_offset,$opt_days);
195
196my $startstring = sprintf "going to grab %d days%s of data into %s (%s%s%s%s) timezone %s region %s",
197        $opt_days,
198        ($opt_offset ? " (skipping first %d days)" : ""),
199        $opt_outputfile,
200        ($opt_fast ? "with haste" : "slowly"),
201        ($opt_no_detail == 0 ? ", with details" : ""),
202        ($opt_no_cache ? ", without caching" : ", with caching"),
203        ($opt_warper ? ", anonymously" : ""),
204        $opt_timezone,
205        $region;
206&log($startstring);
207
208if ($opt_timezone ne "1000") {
209        # set y7w_time_offset appropriately..
210        $y7w_time_offset = (($opt_timezone / 100)-10) + (0.1 * (($opt_timezone % 100) / 60));
211}
212
213if ($opt_channels_file) {
214        &log("using channels as defined in $opt_channels_file");
215        &read_channels_file($opt_channels_file);
216} else {
217        &log("using manual settings");
218        &do_manual_settings;
219}
220
221&read_cache if ($opt_no_cache == 0);
222&get_y7w_data($starttime,$endtime,$region) if ($opt_dont_fetch_widget == 0);
223
224if ($opt_dont_fetch_abc_data == 0) {
225        my ($abc_starttime,$abc_endtime) = &calc_dates(time,$opt_offset,($opt_dont_fetch_abc_extra_days ? $opt_days : 30));
226        &get_abc_data($abc_starttime,$abc_endtime,$abc_xmlid,"http://www.abc.net.au/tv/guide/netw") if ($abc_xmlid ne "");
227        &get_abc_data($abc_starttime,$abc_endtime,$abc2_xmlid,"http://www.abc.net.au/tv/guide/abc2") if ($abc2_xmlid ne "");
228}
229
230&get_y7p_data($starttime,$endtime,$region) if ($opt_dont_fetch_portal == 0);
231&write_cache if ($opt_no_cache == 0);
232
233&write_data;
234&print_stats;
235exit(0);
236
237######################################################################################################
238# help
239
240sub help
241{
242        print<<EOF
243$progname $version
244
245options are as follows:
246        --help                  show these help options
247        --days=N                fetch 'n' days of data (default: $opt_days)
248        --output=file           send xml output to file (default: "$opt_outputfile")
249        --config-file=file      (ignored - historically used by grabbers not not this one)
250        --no-cache              don't use a cache to optimize (reduce) number of web queries
251        --cache-file=file       where to store cache (default "$opt_cache_file")
252        --fast                  don't run slow - get data as quick as you can - not recommended
253        --debug                 increase debug level
254        --warper                fetch data using WebWarper web anonymizer service
255        --obfuscate             pretend to be a proxy servicing multiple clients
256        --no-widget-data        don't fetch data using yahoo7 widget
257        --no-portal-data        don't fetch data using yahoo7 portal
258        --no-abc-data           don't fetch data using ABC website
259        --no-abc-extra-days     don't fetch extra (30 days) from ABC website
260        --no-retry              if webserver is rejecting our request, don't retry (default: do retry)
261        --lang=[s]              set language of xmltv output data (default $lang)
262
263        --shepherd              set if being called from the shepherd script
264        --region=N              set region for where to collect data from (default: $region)
265        --channels_file=file    where to get channel data from (if not set manually)
266        --timezone=HHMM         timezone for channel data (default: $opt_timezone)
267EOF
268;
269
270        exit(0);
271}
272
273######################################################################################################
274# given a start date, end date & region, fetch via yahoo widget data
275
276sub get_y7w_data
277{
278        local($starttime,$endtime,$region) = @_;
279        local($currtime, $data);
280
281        for ($currtime = $starttime; $currtime < $endtime; $currtime += 86400) {
282                my $url = sprintf "http://au.tv.yahoo.com/widget.html?rg=$region&st=%d&et=%d",$currtime,($currtime+86400);
283                my $status = sprintf "yahoo7 widget data: %d of %d",((($currtime-$starttime)/86400)+1),(($endtime-$starttime)/86400);
284
285                $data = &get_url($url,$status);
286                &parse_xml_data($data);
287        }
288}
289
290######################################################################################################
291# transcode ywe-octet-stream back into text
292
293sub transform_output
294{
295        local($datasize, $data) = @_;
296
297        local(@xform_map) = (
298          0x39, 0x9E, 0x05, 0x72, 0x6C, 0x06, 0x38, 0x15, 0x42, 0x1E, 0xB9, 0xFD, 0x4D, 0x08, 0x0C, 0x2E,
299          0x57, 0xC7, 0x62, 0x6E, 0xC5, 0x3A, 0x3C, 0xA4, 0x1D, 0xC6, 0x3D, 0x18, 0x2D, 0x1B, 0x83, 0x20,
300          0x78, 0xFC, 0xA5, 0xDE, 0x28, 0xE8, 0x3E, 0x9B, 0x7C, 0x22, 0x1C, 0x89, 0xFF, 0x52, 0x54, 0x43,
301          0x51, 0x7F, 0x71, 0x40, 0x7A, 0xCF, 0x65, 0xE4, 0x36, 0xEB, 0xC9, 0x1F, 0x80, 0x9A, 0x31, 0x4A,
302          0x45, 0xD4, 0x2B, 0x02, 0x4C, 0xF4, 0x53, 0xBD, 0xA8, 0xF9, 0x50, 0x61, 0x8A, 0xD5, 0xBF, 0x81,
303          0xC0, 0xDB, 0xFE, 0xF7, 0xBA, 0xEC, 0xFA, 0x73, 0xA9, 0x8F, 0xB1, 0x70, 0x33, 0xCE, 0x60, 0xAC,
304          0xB2, 0x58, 0x26, 0x85, 0x6B, 0x7D, 0x93, 0x03, 0x64, 0x47, 0x04, 0x88, 0x01, 0xA6, 0x3B, 0x90,
305          0x98, 0xF5, 0x97, 0x3F, 0xF6, 0xD3, 0x94, 0xB7, 0x29, 0x07, 0x96, 0x6F, 0x14, 0x35, 0x8D, 0x2A,
306          0x16, 0x17, 0x8B, 0xD1, 0x48, 0xD6, 0xF1, 0xE2, 0x79, 0x2C, 0x41, 0x5B, 0xBC, 0xB5, 0x68, 0xDC,
307          0x49, 0xD2, 0x6A, 0xCC, 0x25, 0xB4, 0xAA, 0x63, 0x9C, 0x56, 0x4B, 0xB8, 0x87, 0x5E, 0x86, 0x09,
308          0xC4, 0x95, 0xB6, 0x12, 0xF8, 0x84, 0x4E, 0x21, 0x32, 0xCA, 0x66, 0xC3, 0xBB, 0x27, 0xEE, 0xE0,
309          0x1A, 0xD8, 0x6D, 0x4F, 0xAF, 0x82, 0xEF, 0xCD, 0x5F, 0x8C, 0x67, 0xA2, 0xCB, 0xED, 0xAB, 0xB0,
310          0xA7, 0x92, 0x75, 0x5A, 0xF2, 0x0A, 0x0E, 0xE6, 0x7E, 0xC8, 0xE9, 0x19, 0x24, 0x37, 0x11, 0xA0,
311          0xE3, 0xDD, 0xD7, 0x23, 0x9F, 0x00, 0xA1, 0xC1, 0x74, 0xF0, 0x99, 0x77, 0xAE, 0x91, 0x7B, 0xFB,
312          0xD9, 0xDA, 0xC2, 0x44, 0x0D, 0x76, 0x10, 0x9D, 0xEA, 0xE7, 0xE5, 0x59, 0xF3, 0xD0, 0x5D, 0x2F,
313          0x69, 0xAD, 0x34, 0x0F, 0x5C, 0x8E, 0xBE, 0x13, 0x30, 0x55, 0xE1, 0xDF, 0x0B, 0xB3, 0x46, 0xA3
314          );
315        local($xlate_pos1,$xlate_pos2,$xlate_pos3,$xlate_pos4);
316        local($pos);
317        local($outputdata);
318
319        if (($datasize < 1) || (ord(substr($data,0,1)) != 1)) {
320                # not valid
321                return(undef);
322        }
323
324        for ($pos = 1; $pos < $datasize; $pos++) {
325                $xlate_pos1 = ($xlate_pos1 + 1) % 256;
326                $xlate_pos3 = $xform_map[$xlate_pos1];
327                $xlate_pos4 = ($xlate_pos2 + $xlate_pos3) % 256;
328                $xlate_pos2 = $xform_map[$xlate_pos4];
329                $xform_map[$xlate_pos1] = $xlate_pos2;
330                $xlate_pos2 += $xlate_pos3;
331                $xform_map[$xlate_pos4] = $xlate_pos3;
332                $xlate_pos2 = $xlate_pos2 % 256;
333                $xlate_pos3 = $xform_map[$xlate_pos2];
334                $xlate_pos2 = $xlate_pos4;
335                $outputdata .= chr((((ord(substr($data,$pos,1))) % 256) ^ ($xlate_pos3 % 256)) % 256);
336        }
337        return($outputdata);
338}
339
340######################################################################################################
341# normalize starttime to an hour..
342
343sub calc_dates
344{
345        local($starttime,$skip_days,$days) = @_;
346
347        # normalize starttime to an hour..
348        my ($sec,$min,@rest) = localtime($starttime);
349        $starttime -= ((60 * $min) + $sec);
350        my $endtime = $starttime + ($days * 24 * 3600);
351        $starttime += (86400 * $skip_days);
352        return($starttime,$endtime);
353}
354
355######################################################################################################
356# logic to fetch a page via http
357#  retries up to 3 times to get a page with 5 second pauses inbetween
358
359sub get_url
360{
361        local($url,$status,$dontretry) = (@_);
362        my $response;
363        my $attempts = 0;
364        my ($raw, $page, $base);
365
366        $url =~ s#^http://#http://webwarper.net/ww/# if $opt_warper;
367        my $request = HTTP::Request->new(GET => $url);
368        $request->header('Accept-Encoding' => 'gzip');
369
370        if ($opt_obfuscate) {
371                my $randomaddr = sprintf "203.%d.%d.%d",rand(255),rand(255),(rand(254)+1);
372                $request->header('Via' => '1.0 proxy:81 (Squid/2.3.STABLE3)');
373                $request->header('X-Forwarded-For' => $randomaddr);
374        }
375
376        printf STDERR "[%d] fetching %s%s: %s\n",time,$status,($opt_obfuscate ? "[obfuscate]" : ""),$url;
377
378        $max_retries = 1 if ($dontretry);
379
380        for (1..3) {
381                $response = $ua->request($request);
382                last if ($response->is_success || $dontretry);
383
384                $stats{http_failed_requests}++;
385                $attempts++;
386                &sleepy("attempt $attempts failed (url $url), sleeping for 10 seconds",10);
387        }
388        if (!($response->is_success)) {
389                if ($dontretry == 0) {
390                        &log("aborting after $attempts attempts to fetch url $url") if $debug;
391                        printf STDERR "ERROR: could not open url %s in %d attempts\n",$url,$attempts;
392                }
393                return undef;
394        }
395
396        $stats{bytes_fetched} += do {use bytes; length($response->content)};
397        $stats{http_successful_requests}++;
398
399        if ($random_sleep) {
400                $sleeptimer = int(rand(5)) + 1;  # sleep anywhere from 1 to 5 seconds
401                &sleepy("feeling sleepy for $sleeptimer seconds..",$sleeptimer);
402        }
403
404        if ($response->header('Content-Encoding') &&
405            $response->header('Content-Encoding') eq 'gzip') {
406                $stats{compressed_pages} += do {use bytes; length($response->content)};
407                $response->content(Compress::Zlib::memGunzip($response->content));
408        }
409
410        if ($response->header('Content-type') eq 'xapplication/ywe-octet-stream') {
411                $stats{transformed_pages}++;
412                $base = &transform_output(length($response->content), $response->content);
413        } else {
414                $base = $response->content;
415        }
416        return $base; 
417}
418
419######################################################################################################
420
421sub log
422{
423        local($entry) = @_;
424        printf STDERR "[%d] %s\n",time,$entry;
425}
426
427######################################################################################################
428# Convert date time to 'yyyymmddhhmm +hhmm' format as expected by xmltv
429
430sub timestring {
431        local($t) = @_;
432        return strftime "%Y%m%d%H%M %z", localtime($t);
433}
434
435######################################################################################################
436
437sub print_stats
438{
439        local($key);
440        printf STDERR "completed in %0.2f seconds",tv_interval($script_start_time);
441        foreach $key (sort keys %stats) {
442                printf STDERR ", %d %s",$stats{$key},$key;
443        }
444        printf STDERR "\n";
445}
446
447######################################################################################################
448# given yahoo7 xml data, parse it into 'shows' ..
449# parse it into $tv_guide->{$channel}->{data}->{$event_id}-> structures..
450
451sub parse_xml_data
452{
453        local($data) = @_;
454
455        my $parser = new XML::DOM::Parser;
456        $tree = $parser->parse($data);
457        my $channels = $tree->getElementsByTagName("venue");
458        for (my $i = 0; $i < $channels->getLength; $i++) {
459                my $channel = $channels->item($i)->getAttributeNode("co_short")->getValue;
460
461                # are we interested in this channel?
462                if ($y7w_channel_id{$channel} eq "") {
463                        $stats{skipped_programmes}++;
464                        next;
465                }
466
467                # for this channel get every programme ('event')
468                my $events = $channels->item($i)->getElementsByTagName("event");
469                for (my $j = 0; $j < $events->getLength; $j++) {
470                        my $event = $events->item($j);
471                        my $event_id = $event->getElementsByTagName("event_id")->item(0)->getFirstChild->getNodeValue;
472
473                        # mandatory fields
474                        my $event_start =       $event->getElementsByTagName("event_date")->item(0)->getFirstChild->getNodeValue;
475                        my $event_end =         $event->getElementsByTagName("end_date")->item(0)->getFirstChild->getNodeValue;
476
477                        my $event_title = $event_subtitle = $event_desc1 = $event_desc2 = $event_maincast = $event_year = undef;
478                        my $event_rating = $event_genre = $event_runtime = $event_repeatflag = $event_country = undef;
479
480                        # event_id actually isn't unique - so make it so
481                        $event_id .= $event_start . $event_end;
482
483                        # wrap these non-mandatory fields in an eval so if they don't exist the script doesn't barf out
484                        eval { $event_title =           $event->getElementsByTagName("title")->item(0)->getFirstChild->getNodeValue; };
485                        eval { $event_subtitle =        $event->getElementsByTagName("subtitle")->item(0)->getFirstChild->getNodeValue; };
486                        eval { $event_desc1 =           $event->getElementsByTagName("description_1")->item(0)->getFirstChild->getNodeValue; };
487                        eval { $event_desc2 =           $event->getElementsByTagName("description_2")->item(0)->getFirstChild->getNodeValue; };
488                        eval { $event_maincast =        $event->getElementsByTagName("main_cast")->item(0)->getFirstChild->getNodeValue; };
489                        eval { $event_year =            $event->getElementsByTagName("year_released")->item(0)->getFirstChild->getNodeValue; };
490                        eval { $event_rating =          $event->getElementsByTagName("rating")->item(0)->getFirstChild->getNodeValue; };
491                        eval { $event_genre =           $event->getElementsByTagName("genre")->item(0)->getFirstChild->getNodeValue; };
492                        eval { $event_runtime =         $event->getElementsByTagName("running_time")->item(0)->getFirstChild->getNodeValue; };
493                        eval { $event_repeatflag =      $event->getElementsByTagName("repeat")->item(0)->getFirstChild->getNodeValue; };
494                        eval { $event_country =         $event->getElementsByTagName("country")->item(0)->getFirstChild->getNodeValue; };
495                        # other fields we dont pick up but exist in source xml data include:
496                        #  other_title, movie, live, premiere, final, captions, warnings, colour
497                        #  language, genre_id, sub_category, director, highlight
498                        #  ext_url, y7_url
499
500                        # add some additional info into description
501                        $event_desc1 .= "\n$event_desc2\n" if ($event_desc2 ne "");
502                        $event_desc1 .= "\n\n";
503                        $event_desc1 .= "(Repeat)\n" if ($event_repeatflag > 0);
504                        $event_desc1 .= "Rating: $event_rating\n" if ($event_rating ne "");
505                        $event_desc1 .= "Year: $event_year\n" if ($event_year > 0);
506                        $event_desc1 .= "Credits/Cast: $event_maincast\n" if ($event_maincast ne "");
507                        $event_desc1 .= "Genre/Category: $event_genre\n" if ($event_genre ne "");
508                        $event_desc1 .= "Running Time: $event_runtime\n" if ($event_runtime > 0);
509                       
510                        $stats{programmes}++;
511                        $stats{duplicate_programmes}++ if ($tv_guide->{$channel}->{data}->{$event_id});
512
513                        $tv_guide->{$channel}->{data}->{$event_id}->{'channel'} =       $y7w_channel_id{$channel};
514                        $tv_guide->{$channel}->{data}->{$event_id}->{'start'} =         timestring($event_start-($y7w_time_offset*3600));
515                        $tv_guide->{$channel}->{data}->{$event_id}->{'stop'} =          timestring($event_end-($y7w_time_offset*3600));
516                        $tv_guide->{$channel}->{data}->{$event_id}->{'title'} =         [[ $event_title, $lang ]] if $event_title;
517                        $tv_guide->{$channel}->{data}->{$event_id}->{'sub-title'} =     [[ $event_subtitle, $lang ]] if $event_subtitle;
518                        $tv_guide->{$channel}->{data}->{$event_id}->{'desc'} =          [[ $event_desc1, $lang ]] if $event_desc1;
519                        $tv_guide->{$channel}->{data}->{$event_id}->{'category'} =      [[ $event_genre, $lang ]] if $event_genre;
520                        $tv_guide->{$channel}->{data}->{$event_id}->{'country'} =       [[ $event_country, $lang ]] if $event_country;
521                }
522        }
523        $tree->dispose;
524}
525
526######################################################################################################
527
528# descend a structure and clean up various things, including stripping
529# leading/trailing spaces in strings, translations of html stuff etc
530#   -- taken & modified from Michael 'Immir' Smith's excellent tv_grab_au
531
532my %amp;
533BEGIN { %amp = ( nbsp => ' ', qw{ amp & lt < gt > apos ' quot " } ) }
534
535sub cleanup {
536        my $x = shift;
537        if    (ref $x eq "REF")   { cleanup($_) }
538        elsif (ref $x eq "HASH")  { cleanup(\$_) for values %$x }
539        elsif (ref $x eq "ARRAY") { cleanup(\$_) for @$x }
540        elsif (defined $$x) {
541                $$x =~ s/&(#(\d+)|(.*?));/ $2 ? chr($2) : $amp{$3}||' ' /eg;
542                # $$x =~ s/[^\x20-\x7f]/ /g;
543                $$x =~ s/(^\s+|\s+$)//g;
544        }
545}
546
547######################################################################################################
548
549sub write_data
550{
551        my %writer_args = ( encoding => 'ISO-8859-1' );
552        if ($opt_outputfile) {
553                my $fh = new IO::File(">$opt_outputfile")  or die "can't open $opt_outputfile: $!";
554                $writer_args{OUTPUT} = $fh;
555        }
556
557        my $writer = new XMLTV::Writer(%writer_args);
558
559        $writer->start
560          ( { 'source-info-url'    => "about:blank",
561              'source-info-name'   => "$progname $version",
562              'generator-info-name' => "$progname $version"} );
563
564        $writer->write_channel( { 'display-name' => [[ "ABC", $lang ]], 'id' => $abc_xmlid } ) if (($opt_dont_fetch_abc_data == 0) && ($abc_xmlid ne ""));
565        $writer->write_channel( { 'display-name' => [[ "ABC2", $lang ]], 'id' => $abc2_xmlid } ) if (($opt_dont_fetch_abc_data == 0) && ($abc2_xmlid ne ""));
566
567        # write channels collected via y7 widget
568        if ($opt_dont_fetch_widget == 0) {
569                for my $channel (sort keys %y7w_channel_id) {
570                        $writer->write_channel( {
571                                'display-name' => [[ $channel, $lang ]],
572                                'id' => $y7w_channel_id{$channel}
573                                } );
574                }
575        }
576
577        # write channels collected via y7 portal
578        if ($opt_dont_fetch_portal == 0) {
579                for my $channel (sort keys %y7p_channel_id) {
580                        my ($venue_id,$channame) = split(/,/,$channel);
581                        $writer->write_channel( {
582                                'display-name' => [[ $channame, $lang ]],
583                                'id' => $y7p_channel_id{$channel}
584                                } );
585                }
586        }
587
588        # ABC
589        if ($opt_dont_fetch_abc_data == 0) {
590                if ($abc_xmlid ne "") {
591                        for my $event_id (sort {$a <=> $b} keys %{($tv_guide->{$abc_xmlid}->{data})}) {
592                                $show = $tv_guide->{$abc_xmlid}->{data}->{$event_id};
593                                &cleanup($show);
594                                $writer->write_programme($show);
595                        }
596                }
597                if ($abc2_xmlid ne "") {
598                        for my $event_id (sort {$a <=> $b} keys %{($tv_guide->{$abc2_xmlid}->{data})}) {
599                                $show = $tv_guide->{$abc2_xmlid}->{data}->{$event_id};
600                                &cleanup($show);
601                                $writer->write_programme($show);
602                        }
603                }
604        }
605
606        # write programmes collected via y7 widget
607        if ($opt_dont_fetch_widget == 0) {
608                for my $channel (sort keys %y7w_channel_id) {
609                        for my $event_id (sort {$a <=> $b} keys %{($tv_guide->{$channel}->{data})}) {
610                                $show = $tv_guide->{$channel}->{data}->{$event_id};
611                                &cleanup($show);
612                                $writer->write_programme($show);
613                        }
614                }
615        }
616
617        # write programmes collected via y7 portal
618        if ($opt_dont_fetch_portal == 0) {
619                for my $channel (sort keys %y7p_channel_id) {
620                        for my $event_id (sort keys %{($tv_guide->{$channel}->{data})}) {
621                                $show = $tv_guide->{$channel}->{data}->{$event_id};
622                                &cleanup($show);
623                                $writer->write_programme($show);
624                        }
625                }
626        }
627
628        $writer->end();
629}
630
631######################################################################################################
632# given yahoo7 portal data, parse it into 'shows' ..
633
634sub get_y7p_data
635{
636        local($starttime,$endtime,$region) = @_;
637        local($currtime, $try_to_add_y7p_detail, $want_to_add_detail);
638
639        foreach my $channel (sort { $y7p_channel_id{$b} <=> $y7p_channel_id{$a} } keys %y7p_channel_id) {
640                my ($venue,$channenname) = split(/,/,$channel);
641                my $unprocessed_programmes = 0;
642                my @unprocessed_progname = undef;
643                my @unprocessed_starttime = undef
644                my @unprocessed_url = undef;
645
646                for ($currtime = $starttime; $currtime < $endtime; $currtime += 86400) {
647                        my $attempts = 1;
648                        my @timeattr = localtime($currtime); # 0=sec,1=min,2=hour,3=day,4=month,5=year,6=wday,7=yday,8=isdst
649
650                        my $url = sprintf "http://au.tv.yahoo.com/venueresults.html?dt=%s&vn=%d",
651                                (strftime "%Y-%m-%d",localtime($currtime)), $venue;
652                        my $seen_programmes = 0;
653
654                        do {
655                                my $status = sprintf "yahoo7 portal data: %s: %d of %d%s", $channenname,
656                                        ((($currtime-$starttime)/86400)+1),(($endtime-$starttime)/86400),
657                                        ($attempts > 1 ? " (attempt $attempts)" : "");
658
659                                my $data = &get_url($url,$status);
660
661                                my $tree = HTML::TreeBuilder->new_from_content($data);
662                                my $seen_am = 0;
663
664                                for ($tree->look_down('_tag' => 'table', 'width' => '100%', 'border' => '1', 'bordercolor' => '#efefef')) {
665                                        for ($_->look_down('_tag' => 'tr')) {
666                                                my $found_time = 0;
667                                                my $ignore_line = 0;
668                                                foreach my $tree_td ($_->look_down('_tag' => 'td')) {
669                                                        if ($ignore_line == 0) {
670                                                                if ($found_time == 0) {
671                                                                        # looking for time
672                                                                        if ($tree_td->as_text() =~ /^(\d+):(\d+)(.)M$/) {
673                                                                                $timeattr[2] = $1; # hour
674                                                                                $timeattr[2] += 12 if ($3 eq "P"); # pm
675                                                                                $timeattr[1] = $2; # min
676                                                                                $found_time = mktime(@timeattr);
677       
678                                                                                # if entry is PM and we haven't seen any AM entries, ignore - its from the previous day
679                                                                                $ignore_line = 1 if (($3 eq "P") && ($seen_am == 0));
680                                                                                $seen_am++ if ($3 eq "A");
681                                                                        }
682                                                                } else {
683                                                                        # looking for name
684                                                                        if ($_ = $tree_td->look_down('_tag' => 'a')) {
685                                                                                my $programme = $_->as_text();
686                                                                                my $progurl = $_->attr('href');
687                                                                                $progurl = sprintf "http://au.tv.yahoo.com/%s",$1 if ($progurl =~ /^javascript:popup\(\"(.*)\"/);
688       
689                                                                                $unprocessed_progname[$unprocessed_programmes] = $programme;
690                                                                                $unprocessed_starttime[$unprocessed_programmes] = $found_time;
691                                                                                $unprocessed_url[$unprocessed_programmes] = $progurl;
692                                                                                $unprocessed_programmes++;
693                                                                                $seen_programmes++;
694                                                                        }
695                                                                }
696                                                        }
697                                                }
698                                        }
699                                }
700                                if ($seen_programmes == 0) {
701                                        printf STDERR "WARNING: failed to parse any programme data from %s - blocked/rate-limited/format-changed?\n",$url;
702                                        $stats{failed_to_parse_portal_daily_page}++;
703                                        $attempts++;
704                                } else {
705                                        $stats{portal_daily_pages}++;
706                                }
707                                &sleepy("sleeping for 10 seconds",10);
708                        } until (($seen_programmes > 0) || ($attempts > 5))
709                }
710
711                # have 'n' days of this channel unprocessed - process it!
712                for (my $i = 0; $i < ($unprocessed_programmes-1); $i++) {
713                        $stats{programmes}++;
714                        my $starttime = $unprocessed_starttime[$i];
715                        my $endtime = $unprocessed_starttime[$i+1];
716                        $tv_guide->{$channel}->{data}->{$starttime}->{'channel'} =      $y7p_channel_id{$channel};
717                        $tv_guide->{$channel}->{data}->{$starttime}->{'start'} =        timestring($starttime);
718                        $tv_guide->{$channel}->{data}->{$starttime}->{'stop'} =         timestring($endtime);
719                        $tv_guide->{$channel}->{data}->{$starttime}->{'title'} =        [[ $unprocessed_progname[$i], $lang ]];
720
721                        # schedule a detailed data lookup for each programme if we need to
722                        # ideally we can use our cached data if it hasn't changed..
723
724                        # search cache
725                        my $cache_key = sprintf "%d,%d,%s,%s", $starttime, $endtime, $channel, $unprocessed_progname[$i];
726                        if ($data_cache->{$cache_key}) {
727                                $stats{used_cached_data}++;
728                                &add_cached_data($channel,$starttime,$cache_key);
729                                &log("used cache data for programme $unprocessed_progname[$i] on channel $channel") if $debug;
730                        } else {
731                                if ($opt_no_detail == 0) {
732                                        $try_to_add_y7p_detail{$unprocessed_url[$i]} = $cache_key;
733                                        $want_to_add_detail++;
734                                }
735                        }
736                }
737
738                $unprocessed_programmes = 0;
739        }
740
741        foreach my $url (sort keys %try_to_add_y7p_detail) {
742                &get_one_y7p_event($try_to_add_y7p_detail{$url},$url,"Yahoo7Portal detail pages ($want_to_add_detail remaining)");
743                $want_to_add_detail--;
744
745                &sleepy("sleeping for 16+ seconds",(16+rand(5))) if ($opt_fast == 0);
746        }
747}
748
749######################################################################################################
750# given one yahoo7 portal page, parse it into $tv_guide->{$channel}->{data}->{$event_id}-> structures..
751
752sub get_one_y7p_event
753{
754        local($cache_key, $url, $status) = @_;
755        my $seen_programme = 0;
756        my ($starttime, $endtime, $channel, $progname) = split(/,/,$cache_key);
757
758        do {
759                my $data = &get_url($url,$status);
760
761                my $tree = HTML::TreeBuilder->new_from_content($data);
762                if (my $inner_tree = $tree->look_down('_tag' => 'div', 'class' => 'inner')) {
763                        my $event_title = undef, $event_subtitle = undef, $event_description = undef;
764                        my $event_genre = undef, $event_duration = undef;
765
766                        $event_title = $_->as_text() if ($_ = $inner_tree->look_down('_tag' => 'h1'));
767                        $event_subtitle = $_->as_text() if ($_ = $inner_tree->look_down('_tag' => 'h2'));
768
769                        foreach my $para ($inner_tree->look_down('_tag' => 'p')) {
770                                if ($para->as_HTML() =~ /<p>Genre:&nbsp; (.*)$/) {
771                                        $event_genre = $1;
772                                } else {
773                                        $event_description .= $para->as_text();
774                                        $event_duration = ($1 * 60) if ($para->as_HTML() =~ /(\d+)&nbsp;mins/);
775                                }
776                        }
777
778                        if (($event_title) && ($event_duration)) {
779                                $stats{portal_detail_pages}++;
780                                $seen_programme++;
781
782                                $data_cache->{$cache_key}->{subtitle} = $event_subtitle if $event_subtitle;
783                                $data_cache->{$cache_key}->{desc} = $event_description if $event_description;
784                                $data_cache->{$cache_key}->{genre} = $event_genre if $event_genre;
785                                &add_cached_data($channel,$starttime,$cache_key);
786                        }
787                }
788                if ($seen_programme == 0) {
789                        printf STDERR "WARNING: failed to parse any programme data from '%s' - blocked/rate-limited/format-changed?\n",$url;
790                        $stats{failed_to_parse_portal_detail_page}++;
791                        &sleepy("waiting for 3 minutes, will retry then",(180+rand(10))) if ($opt_dont_retry == 0);
792                }
793        } until (($seen_programme> 0) || ($opt_dont_retry>0));
794}
795
796######################################################################################################
797# populate cache
798
799sub read_cache
800{
801        if (-r $opt_cache_file) {
802                local (@ARGV, $/) = ($opt_cache_file);
803                no warnings 'all'; eval <>; die "$@" if $@;
804        } else {
805                printf STDERR "WARNING: no programme cache $opt_cache_file - have to fetch all details\n";
806
807                # try to write to it - if directory doesn't exist this will then cause an error
808                &write_cache;
809        }
810}
811
812######################################################################################################
813# write out updated cache
814
815sub write_cache
816{
817        if (!(open(F,">$opt_cache_file"))) {
818                printf STDERR "WARNING: could not write cache file $opt_cache_file: $!\n";
819                printf STDERR "Please fix this in order to reduce the number of queries for data!\n";
820                sleep 10;
821        } else {
822                # cleanup old entries from cache
823                for my $cache_key (keys %{$data_cache}) {
824                        my ($starttime, $endtime, $channel, $progname) = split(/,/,$cache_key);
825                        delete $data_cache->{$cache_key} if ($starttime < (time-86400));
826                        $stats{removed_items_from_cache}++;
827                }
828
829                print F Data::Dumper->Dump([$data_cache], ["data_cache"]);
830                close F;
831        }
832}
833
834######################################################################################################
835
836sub add_cached_data
837{
838        local($channel,$starttime,$cache_key) = @_;
839        $tv_guide->{$channel}->{data}->{$starttime}->{'sub-title'} =    [[ $data_cache->{$cache_key}->{subtitle}, $lang ]] if $data_cache->{$cache_key}->{subtitle};
840        $tv_guide->{$channel}->{data}->{$starttime}->{'desc'} =         [[ $data_cache->{$cache_key}->{desc}, $lang ]] if $data_cache->{$cache_key}->{desc};
841        $tv_guide->{$channel}->{data}->{$starttime}->{'category'} =     [[ $data_cache->{$cache_key}->{genre}, $lang ]] if $data_cache->{$cache_key}->{genre};
842}
843
844######################################################################################################
845
846sub sleepy
847{
848        local($logmsg,$sleeptimer) = @_;
849
850        $stats{slept_for} += $sleeptimer;
851        &log($logmsg);
852        sleep($sleeptimer);
853}
854
855######################################################################################################
856
857sub get_abc_data
858{
859        local($starttime,$endtime,$xmlid,$urlbase) = @_;
860        local($try_to_add_abc_detail);
861        local($unprocessed_programmes) = 0;
862        local($stop_fetching) = 0;
863
864        for (my $currtime = $starttime; $currtime < $endtime; $currtime += 86400) {
865                # for abc portal data, treat a faulure as a hint that there is no further data.
866                # sometimes they have as much as 30 days of data ahead.  sometimes much less...
867                if ($stop_fetching == 0) {
868                        my @timeattr = localtime($currtime); # 0=sec,1=min,2=hour,3=day,4=month,5=year,6=wday,7=yday,8=isdst
869
870                        my $url = sprintf "%s/%s.htm",$urlbase,(strftime "%Y%m/%Y%m%d",localtime($currtime));
871                        my $status = sprintf "%s data: %d of %d", $xmlid, ((($currtime-$starttime)/86400)+1),(($endtime-$starttime)/86400);
872                        my $data = &get_url($url,$status,1);
873                        my $seen_programmes = 0;
874       
875                        my $tree = HTML::TreeBuilder->new_from_content($data);
876                        for ($tree->look_down('_tag' => 'div', 'class' => 'scheduleDiv')) {
877                                foreach my $tree_tr ($_->look_down('_tag' => 'tr')) {
878                                        if (my $tree_row = $tree_tr->look_down('_tag' => 'th', 'scope' => 'row')) {
879                                                if ($tree_row->as_text() =~ /^(\d+):(\d+)(.)m/) {
880                                                        $timeattr[2] = $1; # hour
881                                                        $timeattr[2] += 12 if ($3 eq "p"); # pm
882                                                        $timeattr[1] = $2; # min
883                                                        $found_time = mktime(@timeattr);
884       
885                                                        if ($tree_tr->look_down('_tag' => 'td')) {
886                                                                if ($_ = $tree_tr->look_down('_tag' => 'a')) {
887                                                                        my $programme = $_->as_text();
888                                                                        my $progurl = $_->attr('href');
889       
890                                                                        if ($progurl =~ /^\/tv\/guide\//) {
891                                                                                $unprocessed_progname[$unprocessed_programmes] = $programme;
892                                                                                $unprocessed_starttime[$unprocessed_programmes] = $found_time;
893                                                                                $unprocessed_url[$unprocessed_programmes] = "http://www.abc.net.au".$progurl;
894                                                                                $unprocessed_programmes++;
895                                                                                $seen_programmes++;
896                                                                        }
897                                                                }
898                                                        }
899                                                }
900                                        }
901                                }
902                        }
903       
904                        if ($seen_programmes == 0) {
905                                $stop_fetching = 1;
906                        } else {
907                                $stats{abc_daily_pages}++;
908                        }
909                }
910        }
911
912        # have 'n' days of this channel unprocessed - process it!
913        for (my $i = 0; $i < ($unprocessed_programmes-1); $i++) {
914                $stats{programmes}++;
915                my $starttime = $unprocessed_starttime[$i];
916                my $endtime = $unprocessed_starttime[$i+1];
917                my $cache_key = sprintf "%d,%d,%s,%s", $starttime, $endtime, $xmlid, $unprocessed_progname[$i];
918
919                $tv_guide->{$xmlid}->{data}->{$starttime}->{'channel'} =        $xmlid;
920                $tv_guide->{$xmlid}->{data}->{$starttime}->{'start'} =  timestring($starttime);
921                $tv_guide->{$xmlid}->{data}->{$starttime}->{'stop'} =   timestring($endtime);
922                $tv_guide->{$xmlid}->{data}->{$starttime}->{'title'} =  [[ $unprocessed_progname[$i], $lang ]];
923
924                if ($data_cache->{$cache_key}) {
925                        $stats{used_cached_data}++;
926                        &add_cached_data($xmlid,$starttime,$cache_key);
927                } else {
928                        if ($opt_no_detail == 0) {
929                                $try_to_add_abc_detail{$unprocessed_url[$i]} = $cache_key;
930                                $want_to_add_detail++;
931                        }
932                }
933        }
934
935        foreach my $url (keys %try_to_add_abc_detail) {
936                &get_one_abc_event($try_to_add_abc_detail{$url},$url,"$xmlid detail pages ($want_to_add_detail remaining)");
937                delete $try_to_add_abc_detail{$url};
938                $want_to_add_detail--;
939        }
940}
941
942######################################################################################################
943
944sub get_one_abc_event
945{
946        local($cache_key, $url, $status) = @_;
947        my $seen_programme = 0;
948        my ($starttime, $endtime, $channel, $progname) = split(/,/,$cache_key);
949
950        do {
951                my $data = &get_url($url,$status);
952
953                my $tree = HTML::TreeBuilder->new_from_content($data);
954                if (my $inner_tree = $tree->look_down('_tag' => 'div', 'class' => 'column2')) {
955                        my $event_title = undef, $event_subtitle = undef, $event_description = undef, $event_genre = undef;
956
957                        if (my $prog_h2 = $inner_tree->look_down('_tag' => 'h2')) {
958                                my $full_title = $prog_h2->as_HTML();
959                                ($event_title,$event_subtitle) = split(/<br>/,$full_title);
960
961                                $event_title =~ s/(<[a-zA-Z0-9]+\>)//g; # remove html tags
962                                $event_title =~ s/(^\n|\n$)//g;         # strip trailing/leading blank lines
963
964                                $event_subtitle =~ s/(<[\/a-zA-Z0-9]+\>)//g;    # remove html tags
965                                $event_subtitle =~ s/(^\n|\n$)//g;              # strip trailing/leading blank lines
966                        }
967                       
968                        my $paranum = 0;
969                        foreach my $para ($inner_tree->look_down('_tag' => 'p')) {
970                                $paranum++;
971
972                                if (($paranum > 1) && (!($para->as_text() =~ /^Go to website/)) && (!($para->as_text() =~ /^Send to a Friend/))) {
973                                        $event_description .= $para->as_text() . "\n";
974
975                                        if (my $try_genre = $para->look_down('_tag' => 'a')) {
976                                                $event_genre = $try_genre->as_text();
977                                        }
978                                }
979                        }
980                        $stats{portal_detail_pages}++;
981                        $seen_programme++;
982
983                        $data_cache->{$cache_key}->{subtitle} = $event_subtitle if $event_subtitle;
984                        $data_cache->{$cache_key}->{desc} = $event_description if $event_description;
985                        $data_cache->{$cache_key}->{genre} = $event_genre if $event_genre;
986                        &add_cached_data($channel,$starttime,$cache_key);
987                }
988                if ($seen_programme == 0) {
989                        printf STDERR "WARNING: failed to parse any programme data from '%s' - blocked/rate-limited/format-changed?\n",$url;
990                        $stats{failed_to_parse_portal_detail_page}++;
991                }
992        } until (($seen_programme> 0) || ($opt_dont_retry>0));
993}
994
995######################################################################################################
996
997sub read_channels_file {
998        local($chfile) = @_;
999
1000        if (-r $chfile) {
1001                local (@ARGV, $/) = ($chfile);
1002                no warnings 'all'; eval <>; die "$@" if $@;
1003        } else {
1004                printf STDERR "WARNING: channels file $chfile could not be read\n";
1005                exit(1);
1006        }
1007
1008        # this should have populated %channels
1009        # now 'intelligently' decide which portal to use for each channel
1010        # default is to use:
1011        #   Yahoo7 Widget: ABC/Seven/Nine/TEN/SBS
1012        #   ABC Website:   ABC2
1013        #   Yahoo7 Portal: none
1014
1015        foreach $ch (sort keys %{$channels}) {
1016                my $xmlid = $channels->{$ch};
1017                if ($ch =~ /^(ABC|Seven|Nine|TEN|SBS)$/) {
1018                        $y7w_channel_id{$ch} = $xmlid;
1019                        &log("  $ch ($xmlid) via yahoo7 widget");
1020                } elsif ($ch =~ /^(ABC2)$/) {
1021                        $abc2_xmlid = $xmlid;
1022                        &log("  $ch ($xmlid) via ABC website");
1023                } else {
1024                        &log("  ch ($xmlid) SKIPPED");
1025                        # XXX could try using y7p_channel_id perhaps?
1026                }
1027        }
1028}
1029
1030######################################################################################################
Note: See TracBrowser for help on using the browser.