#!/usr/bin/perl -w
#
# tvguide - Gets the current TVGuide listings and displays them in a
#	nice text format.
#
# Version : 1.6.1 - November 19, 2000
#		(Kurt Hindenburg <khindenburg at cherrynebula.net>)
#
# Copyright (C) 2000 Kurt Hindenburg <khindenburg at cherrynebula.net>
# This program comes with NO WARRANTY, to the extent permitted by law.
#
# The original version by Clifford 'Buddy' Smith is under the BSD license.
# This code is also under the BSD license.
#
# Requires : wget (http://www.gnu.org/software/wget)
#
# If you notice any odd output or such, please let me know.  Often times
#	everything looks O.K. on my end but not on yours.... :-)
#
use diagnostics;
use strict;

package tvg;

######################################################################
# USER CONFIGURATION BEGIN
# In version 1.5+, leave the ServiceID blank if you want this script to check
#	$HOME/.netscape/cookies for the ServiceID.  An error will be displayed
#	if tvguide entry is not in your cookies.
# If the ServiceID is not found, then turn off cookies in your browser
#	and then go to tvguide.com to find your ServiceID (may have to 
#	http://www.tvguide.com/listings/setup/localize.asp).
#$tvg::ServiceID="100182";
#$tvg::ServiceID="70740";
$tvg::ServiceID="";

# Set this option to 1, if you want --channels to be the default
$tvg::use_channels = 0;

# List of channels to output when using the --channels option.
#  Now HBO will ONLY match HBO, nothing else.
@tvg::channels = (
"ABC","NBC","CBS","PBS","FOXNET"
);

# Set this to 1 for color, 0 for no color
$tvg::use_color = 0;

# Colors (only if you have Term::ANSIColor on your system)
#	Change these depending on your background color
#	foreground (shows), info text (Time zone, Date), times, channels
@tvg::colors = ( "white","red","green","yellow" );

# If you have a channel with 6+ chars, change this line
$tvg::channelwidth = 6+1;           # Any channel more than 6 chars?

# Debugging / testings purposes...
# If you have 'Save As...' the tvguide listings and want to use that
#   file change the 1 to 0 below.  If the listings is not named
#   index.asp (the default) change it below.
$tvg::use_web = 1;
#$tvg::file_name = "index-70740.asp";
$tvg::file_name = "index.asp";

# USER CONFIGURATION END
######################################################################

######################################################################
#
### Ok start now ###
#
#
# Code provided by Alexander Kourakos <Alexander at Kourakos.com>
#----------------------------------------------------------------------------- 
my $nc = ($ENV{HOME} || (getpwuid $<)[7]) . '/.netscape/cookies';
if (!$tvg::ServiceID && open(C, "<$nc")) {
  while (<C>) {
    /^\S*tvguide\.com.*\sServiceID\s+(\d+)/ and $tvg::ServiceID=$1 and last;   
  }
  close C;
}
$tvg::ServiceID or die "\nPlease set \$tvg::ServiceID manually.\n";
#-----------------------------------------------------------------------------

use Getopt::Long;

GetOptions(
	"channels"	=> \$tvg::o_channels,
	"color" 		=> \$tvg::o_color,
	"date=s" 	=> \$tvg::o_date,
	"nocolor"	=> \$tvg::o_nocolor,
	"time=s"		=> \$tvg::o_time,
	"help"		=> \&help,
	"version"	=> \&version
);

# time / date
if ($tvg::o_time || $tvg::o_date) {
	if ( ! $tvg::use_web ) {
		print "\nYou are using $tvg::file_name for your listings, and thus ";
		print "you can't change the time or date.\n\n";
		exit;
	}
	if ($tvg::o_time) {
		compute_timedate();		# Compute the ST= number
	} else {
		print "You must supply a time, not just a date (Try --help).\n\n";
		exit;
	}
}

# channels
$tvg::o_channels && ( $tvg::use_channels = 1 );

# Evaluate whether user has Term::ReadKey on their system
eval { require Term::ReadKey; import Term::ReadKey; };
if ($@) {	# no such module
	$tvg::term_width = 80;          # Change the 80 if not correct
} else {		# yep
	($tvg::term_width) = GetTerminalSize();
}

# color / nocolor
$tvg::o_color && ( $tvg::use_color = 1 );
$tvg::o_nocolor && ( $tvg::use_color = 0 );

# Evaluate whether user has Term::ANSIColor on their system
eval { require Term::ANSIColor; import Term::ANSIColor };
if ($@) {	# no such module
	$tvg::use_color = 0;		# Regard of options, no color
};									# else use_color if they asked for it

@tvg::times = ();			# Clear out times
@tvg::listings = ();		# Clear out listings

$tvg::lynx_cmd="wget -q -O- ";
$tvg::url="http://www.tvguide.com/listings/index.asp?I=$tvg::ServiceID";

if ($tvg::tv_st) {		# Is there an ST= to add? (time/date)
	$tvg::url .= "&ST=";
	$tvg::url .= $tvg::tv_st;
}

# $tvg::url must be in quotes or else won't work correctly
if ($tvg::use_web == 1) {		# use the web
	open(tvg::INPUT,"$tvg::lynx_cmd '$tvg::url' |");
	if (!(<tvg::INPUT>)) {
		die "Are you connected to the Net??? : $!\n";
	};
} else {						# use a local file
	open(tvg::INPUT,$tvg::file_name) or die "Can't open $tvg::file_name: $!\n";
}

read_info();

read_times();

read_shows();						# Read the show times

#tvg::times or die "Error: Apparently I couldn't read the times...\n";

$tvg::use_channels && get_channels();

$tvg::cells = $#tvg::times+1;		# Number of time cells

print_info();
print_layout();						# And print them

print "\n";

# Reset colors
$tvg::use_color && print color("reset");

exit;
#
### Subroutines below... ###
#
######################################################################
#
sub version
{
	print "tvguide v1.6.1 - November 19, 2000\n";
	print "Copyright (C) 2000 Kurt Hindenburg.\n";
	print "tvguide comes with NO WARRANTY, to the extent permitted by law.\n";
	print "This program is released under the BSD license.\n";
	exit;
}

sub help
{
	print "Usage : tvguide [OPTION]...\n";
	print "Output TVGuide.com listings in text format.\n";
	print "The defaults are to use the current time and date and list all channels.\n";
	print "  --channels          output only specified channels.\n";
	print "  --color             use color in the output.\n";
	print "  --date=<mm/dd>      use this date for listings.\n";
	print "  --nocolor           do NOT use color in the output.\n";
	print "  --time=<hh:mm>      use this time for listings (military time).\n";
	print "  --help              display this help and exit.\n";
	print "  --version           display version information and exit.\n";
	print "\nReport bugs to <khindenburg at cherrynebula.net>\n";
	exit;
};

# User must supply time; if date missing assume today
# date : 11/09 (month/day)
# time : 22:00 (military time - 30 minute increments)
#
# At 12:00 AM on November 09, 2000 - I=36839.2083333
sub compute_timedate
{
	use Time::Local;

	$tvg::o_time && ( $tvg::o_time !~ /:/ && help() );
	$tvg::o_date && ( $tvg::o_date !~ /\// && help() );

	my ($u_day,$u_mon) = (localtime)[3,4];
	$u_mon++;	# localtime - months start at 0

	# Starting point - Nov 09, 2000 00:00:00
	# Remember for localtime(), months start at 0, and year starts at 1900
	my $start_date = timelocal("0","0","0","9","10","100");

	my $tv_start = 36839.20833333333;	# &ST= for $start_date
	my $tv_interval = 0.020833333;		# 30 minutes interval 1/48

	my ($u_hour,$u_min) = split(/\:/,$tvg::o_time);
	if ( ($u_hour !~ /\d+/ ) || ( $u_min !~ /\d+/) ) {
		die "Not a valid number for --time : ($u_hour):($u_min)\n";
	}
	if ( ($u_min != 0) && ($u_min != 30) ) { $u_min = 0; };  # 00 or 30 mins
	if ( ($u_hour < 0) || ($u_hour > 23) ) { $u_hour = 0; }; # 0-23 hours

	$tvg::o_date && (($u_mon, $u_day) = split(/\//,$tvg::o_date));
	if ( ($u_mon !~ /\d+/ ) || ( $u_day !~ /\d+/) ) {
		die "Not a valid number for --date : ($u_mon)/($u_day)\n";
	}
	if ( ($u_mon < 1) || ($u_mon > 12) ) { $u_mon = 1; };		# 1-12 months
# If user enters invalid day for a month, the listings will be wrong (duh!).
	if ( ($u_day < 1) || ($u_day > 31) ) { $u_day = 1; };		# 1-31 days

	my $year = (localtime)[5];	# current year - 1900

# For timelocal, months start at 0, year at 1900
	my $u_date = timelocal("0",$u_min,$u_hour,$u_day,$u_mon-1,$year);

# diff_date = difference between tv_start (11/9/2000) and desired listings
	my $diff_date = $u_date - $start_date;	# Seconds
	$diff_date = $diff_date / 60;				# Minutes
	$diff_date = $diff_date / 30;				# 30 minutes interval

	$tvg::tv_st = $tv_start + ($diff_date * $tv_interval) + 0.000001;
}

# Print top info - Time zone and date
sub print_info
{
	print "\n";
	print " " x (($tvg::term_width - length($tvg::TimeZone)) / 2);
	$tvg::use_color && print color($tvg::colors[1],"bold");
	print "$tvg::TimeZone";
	print " " x (($tvg::term_width - length($tvg::CurrentDate)) / 2);
	print "$tvg::CurrentDate\n";
	$tvg::use_color && print color("reset");
}

# Print the times
sub print_times
{
	$tvg::cells or die "Bad, bad... $tvg::cells == 0\n";

	print "\n";
	print " " x $tvg::channelwidth;		# Print spaces for channel

	my $cellwidth = ($tvg::term_width-$tvg::channelwidth) / $tvg::cells;
	foreach $_ (@tvg::times) {
		$tvg::use_color && print color($tvg::colors[2],"bold");
		print $_;
		print " " x ($cellwidth-length($_));
	}
	print "\n";
	$tvg::use_color && print color("reset");
	print " " x $tvg::channelwidth;		# Print spaces for channel
	for (my $i = 0; $i < (scalar (@tvg::times)); $i++ ) {
		print "+","-" x (($cellwidth-1)/2),"+","-" x (($cellwidth-2)/2);
	}
}

sub print_color_show_title
{
	my $l1 = substr($_[0],0,1);
	my $l2 = substr($_[0],1);
	print color($tvg::colors[0],"underline"),$l1;
	print color("reset"),color($tvg::colors[0]),$l2;
	print color("reset");
}

# Ok, this is where one channel's shows are printed out.  If you wanted
#	to alter the output (something other than a table) this is where you do it.
sub print_channel_line
{

	my $line = $_[0];
	# Print the channel
	print "\n";
	if ($tvg::ol > 10)  { print_times(); $tvg::ol = 0; }

	$tvg::ol++;
	my ($l,$slots) = split(/#/,$line->[0]);
	$tvg::use_color && print color($tvg::colors[3]);
	print "\n",$l," " x ($tvg::channelwidth-(length $l));

	my $movedown = 0;
	my $prev_incr = 0;

	# Skip the channel (start at 1)
	for (my $i = 1; $i < @$line; $i++) {
		($l,$slots) = split(/#/,$line->[$i]);

		if ( $movedown == 1 ) {
			print " " x ($tvg::channelwidth);
			print " " x ($prev_incr);
			$movedown = 0; $tvg::ol++;
		}
		if ( length($l) > (($tvg::increment * $slots)-1) ) {
			$tvg::use_color && print_color_show_title($l);
			! $tvg::use_color && print "$l";
			print "\n"; $movedown = 1;
		} else {
			$tvg::use_color && print_color_show_title($l);
			! $tvg::use_color && print "$l";
			print " " x ( ($slots * $tvg::increment) - (length $l) );
			$movedown = 0;
		}
		$prev_incr += $slots * $tvg::increment;
	}
}

# Print the layout of times and shows
# Slots are in 5 minute increments
# Cellwidth is based on 30minutes increments
sub print_layout
{
	return if ($#tvg::listings < 0);

	my $x;

	$tvg::cellwidth = ($tvg::term_width-$tvg::channelwidth) / $tvg::cells;
	$tvg::increment = $tvg::cellwidth / 6;
	$tvg::ol = 0;

	print_times();

	my @channel_line = ();
	foreach $x (@tvg::listings) {
		my ($l,$slots) = split(/#/,$x);

		if ($slots == 0) {		# Channel
			if ( $#channel_line > 0 ) {
				print_channel_line(\@channel_line);
				@channel_line = ();
			}
		}
		push (@channel_line, $x);
	}
	if ( $#channel_line > 0 ) {
		print_channel_line(\@channel_line);		# last channel
	}
	print "\n";
	$tvg::use_color && print color("reset");
}
			
# When using --channels option
# Get channels - parse channels and save only those channels
sub get_channels
{
	my ($c,$foundone,$x,$li,$l,$slots);
	my @tvlist = @tvg::listings;
	@tvg::listings = ();
	my $within = 0;

	for ($li = 0; $li <= $#tvlist; $li++) {
		$x = $tvlist[$li];
		# Only interested in channels
		next unless (substr($x,1,length($x) - 1) =~ /0/);

		($l,$slots) = split(/#/,$x);

		$foundone = 0;
		foreach $c (@tvg::channels) {
			if ($c =~ /$l/) {
				$foundone = 1;
				push(@tvg::listings,$x);
				while ( ($foundone == 1) && ($li <= $#tvlist) ) {
					$li++;
					$x = $tvlist[$li];
					($l,$slots) = split(/#/,$x);
					if ($slots == 0) {		# next channel
						$foundone = 0; $li--;
					} else {
						push(@tvg::listings,$x);
					}
				}
			}
		}
	}
}

##########################################################################
# PARSING BEGIN
#
# Skip over garbage and read the show times (@tvg::times)
# Since tvguide.com changes the layout, we'll start looking for
#	times between the Leftarrow and RightArrow (images)
#		Note: Leftarrow, RightArrow
sub read_times
{
	my $i=0;
	while (<tvg::INPUT>) {
		last if /Left(A|a)rrow/;
	}
	while (<tvg::INPUT>) {
		last if /Right(A|a)rrow/;
		if (/(\s+)(\d+):\d\d/) {	
			s/^\s+//;
			s/(\s)+$//;
			$tvg::times[$i++]=$_;				# Save the times
		}
	}
}

# Store actual shows/channels in @tvg:listings
# Since the new layout of tvguide.com, the listings for a time section are
#	all on one line.
sub read_shows
{
	while (<tvg::INPUT>) {
		if (/index.asp\?view/) { parse_shows($_) };
	}
}


sub parse_shows
{
	my ($tabs);
	my @tables = ();		# Clear out tables
	$_ = $_[0];

	s/<\/TD>/DELIMITER/g;
	@tvg::columns=split(/DELIMITER/,$_);

# channels don't have colspan

	foreach $_ (@tvg::columns) {
		$tabs=0;
		# colspan is per 5 minutes ( 1=5m, 3=15m, 6=30m, 12=60minutes )
		if (m/colspan/) {
			$tabs = $_;
			$tabs =~ s/.*?colspan=//;		# Get rid up til...
			$tabs = substr($tabs,0,2);		# At most 2 chars
			$tabs =~ s/>//;
		} else {		# Channels have no colspan, but they are links
			if ( ! (m/<\/a>/) ) { next; };
		};
		# We have the colspan now, trash everything but show/channel
		if (m/<\/a>/) {		# Most shows are links
			s/<(.*?)>//g;
			s/\&nbsp\;//g;
			s/\&gt\;//g;
			s/\&lt\;//g;
			s/\&(.*?)\;\s+//g;
			s/<(.*?)>//g;
			# Remove (TV-PG), (PG), etc....
			s/\((.*?)\)//g;
			s/-->//g;
			s/\t//g;
			s/\n//g;
			s/\015//g;	# \r = ^M
			s/^\s+//;
			s/\s+$//;
		} else {		# Show w/o any links (Off Air,To Be Announced)
			s/<(.*?)>//g;
			s/\&gt\;//g;
			s/\&lt\;//g;
		}
		next if length($_) < 2;

		if ( $tabs == 0 ) {		# Look for '18 NBC', remove the 18
			s/^\d+//;
			s/^\s+//;
		}
#print "<$_><$tabs>\n";
		my $fl = $_ . "#" . $tabs;
		push(@tvg::listings,$fl);
	}
}

sub read_info
{
# Find current date from web page
	while (<tvg::INPUT>) {
		last if (m/Listings For:/);
	}
# $_ has the time zone in it...
	s/\&nbsp\;/\t/g;
	s/\&gt\;//g;
	s/\&lt\;//g;
	s/\&(.*?)\;\s+//g;
	s/<(.*?)>//g;
	s/^\s+//;
	$tvg::TimeZone = $_;

	while (<tvg::INPUT>) {
		last if m/SELECTED/;		# Current Date will be SELECTED
	}
	s/.*?SELECTED>//;		# Get rid up til ...
	s/<(.*?)>//g;
	$tvg::CurrentDate = substr($_,0,7); # 7 chars at most
	$tvg::CurrentDate =~ s/\s+$//; 

}
