#!/usr/bin/perl =pod =head1 NAME tv_grab_il - Grab TV listings for Israel. =head1 SYNOPSIS tv_grab_il --help tv_grab_il --version tv_grab_il --capabilities tv_grab_il --description tv_grab_il [--config-file FILE] [--days N] [--offset N] [--slow] [--output FILE] [--quiet] tv_grab_il --configure [--config-file FILE] tv_grab_il --configure-api [--stage NAME] [--config-file FILE] [--output FILE] tv_grab_il --list-channels [--config-file FILE] [--output FILE] [--quiet] =head1 DESCRIPTION Output TV listings in XMLTV format for many channels available in Israel. The data comes from tv-guide.walla.co.il. 5 days of listings (including the current day) are available. First you must run B to choose which channels you want to receive. Then running B 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_il.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<--days N> When grabbing, grab N days rather than all available days. B<--offset N> Start grabbing at today + N days. B<--quiet> Suppress the progress-bar normally shown on standard error. B<--list-channels> Write output giving elements for every channel available (ignoring the config file), but no programmes. B<--capabilities> Show which capabilities the grabber supports. For more information, see L B<--version> Show the version of the grabber. B<--help> Print a help message and exit. =head1 ERROR HANDLING If the grabber fails to download data for some channel on a specific day, it will print an error message 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 https://tv-guide.walla.co.il/ =head1 AUTHOR The original author was lightpriest. This documentation and parts of the code are based on various other grabbers from the XMLTV project. =head1 SEE ALSO L. =cut use warnings; use strict; use DateTime; use DateTime::Duration; use Encode; use JSON; use POSIX qw(strftime); use XMLTV; use XMLTV::Options qw/ParseOptions/; use XMLTV::ProgressBar; use XMLTV::Configure::Writer; use XMLTV::Get_nice qw(get_nice_tree); my ($opt, $conf) = ParseOptions({ grabber_name => "tv_grab_il", version => "$XMLTV::VERSION", capabilities => [qw/baseline manualconfig apiconfig/], stage_sub => \&config_stage, listchannels_sub => \&write_channels, description => "Israel (tv-guide.walla.co.il)", }); sub config_stage { my ($stage, $conf) = @_; die "Unknown stage $stage" unless $stage eq "start"; my $result; my $writer = new XMLTV::Configure::Writer(OUTPUT => \$result, encoding => 'utf-8'); $writer->start({'generator-info-name' => 'tv_grab_il'}); $writer->end('select-channels'); return $result; } sub fetch_channels { my ($opt, $conf) = @_; my $channels = {}; my $bar = new XMLTV::ProgressBar({ name => "Fetching channels", count => 1, }) unless ($opt->{quiet} || $opt->{debug}); # Get the page containing the list of channels my $tree = XMLTV::Get_nice::get_nice_tree('https://tv-guide.walla.co.il', undef); my @channels = $tree->look_down("_tag", "a", "class", "tv-guide-channels-logos-title", "href", qr{^/channel/\d+$}, ); $bar->update() && $bar->finish && undef $bar if defined $bar; $bar = new XMLTV::ProgressBar({ name => "Parsing result", count => scalar @channels, }) unless ($opt->{quiet} || $opt->{debug}); # Browse through the downloaded list of channels and map them to a hash XMLTV::Writer would understand foreach my $channel (@channels) { if ($channel->as_text()) { my ($id) = $channel->attr('href') =~ m{^/channel/(\d+)$}; $channels->{"$id.tv-guide.walla.co.il"} = { id => "$id.tv-guide.walla.co.il", 'display-name' => [[ encode( 'utf-8', $channel->as_text()) ]], url => [ $channel->attr('href') ] }; my $icon = $channel->look_down('_tag', 'img'); if ($icon) { $icon = $icon->attr('src'); $channels->{"$id.tv-guide.walla.co.il"}->{icon} = [ {src => ($icon || '')} ]; } } $bar->update() if defined $bar; } $bar->finish() && undef $bar if defined $bar; # Notifying the user :) $bar = new XMLTV::ProgressBar({ name => "Reformatting", count => 1, }) unless ($opt->{quiet} || $opt->{debug}); $bar->update() && $bar->finish() if defined $bar; return $channels; } sub write_channels { my $channels = fetch_channels($opt, $conf); # Let XMLTV::Writer format the results as a valid xmltv file my $result; my $writer = new XMLTV::Writer(OUTPUT => \$result, encoding => 'utf-8'); $writer->start({'generator-info-name' => 'tv_grab_il'}); $writer->write_channels($channels); $writer->end(); return $result; } # Fetch the channels again to see what's available my $channels = fetch_channels($opt, $conf); # Configure initial elements for XMLTV::Writer # # Create a new hash for the channels so that channels without programmes # won't appear in the final XML my $encoding = 'UTF-8'; my $credits = {'generator-info-name' => 'tv_grab_il'}; my $w_channels = {}; my $programmes = []; # Progress Bar :) my $bar = new XMLTV::ProgressBar({ name => "Fetching channels listings", count => (scalar @{$conf->{channel}}) * $opt->{days} }) unless ($opt->{quiet} || $opt->{debug}); # Fetch listings per channel foreach my $channel_id (@{$conf->{channel}}) { # Check each channel still exists in walla's channels page if ($channels->{$channel_id}) { my ($walla_id) = ($channel_id =~ /^(\d+)\..*$/); # Now grab listings for each channel on each day, according to the options in $opt # N.B.: only 5 days available, including today # Each channel's 5-day listings are presented on a single page # my $url = "https://tv-guide.walla.co.il/channel/$walla_id"; my $tree = XMLTV::Get_nice::get_nice_tree($url, undef); use Data::Dumper; if ($tree) { for (my $i=$opt->{offset}; $i < ($opt->{offset} + $opt->{days}); $i++) { my $channel_line = $tree->look_down('_tag', 'li', 'class', qr/channel-line/, 'data-id', "$i"); my @shows = $channel_line->look_down('_tag', 'li', 'style', qr/flex/, sub { !defined($_[0]->attr('class')) }); if (@shows) { SHOW: foreach my $show (@shows) { my $prog_data_obj = $show->attr("data-obj"); my $json = decode_json $prog_data_obj; my $title = $json->{'name'}; next SHOW unless $title; my $desc = $json->{'description'}; my ($start_hr, $start_min) = $json->{'start_time'} =~ m/(\d\d):(\d\d)/; my ($end_hr, $end_min) = $json->{'end_time'} =~ m/(\d\d):(\d\d)/; my $show_duration = DateTime::Duration->new( minutes => $json->{'duration'} ); my $current_day = DateTime->today()->add(days => $i)->set_time_zone('Asia/Jerusalem'); my $start_time = $current_day->clone->set(hour => $start_hr, minute => $start_min, second => 0); my $end_time = $start_time + $show_duration; my $prog = { start => $start_time->strftime("%Y%m%d%H%M%S %z"), title => [[ encode('utf-8', $title) ]], channel => $channel_id, }; $prog->{'stop'} = $end_time->strftime("%Y%m%d%H%M%S %z") if defined $end_time; $prog->{'desc'} = [[ encode('utf-8', $desc) ]] if $desc ne ''; push @{$programmes}, $prog; } $w_channels->{$channel_id} = $channels->{$channel_id} unless $w_channels->{$channel_id}; } } } $bar->update if defined $bar; } } $bar->finish() && undef $bar if defined $bar; my %w_args; if (($opt->{offset} != 0) || ($opt->{days} != -999)) { $w_args{offset} = $opt->{offset}; $w_args{days} = ($opt->{days} == -999) ? 100 : $opt->{days}; $w_args{cutoff} = '000000'; } my $data = []; $data->[0] = $encoding; $data->[1] = $credits; $data->[2] = $w_channels; $data->[3] = $programmes; XMLTV::write_data($data, %w_args);