Monday, November 17, 2008

Bash Cable, Dish and Local TV Listings Script For Linux Or Unix

Hey There,

IMPORTANT NOTE: I'm beginning to think that this script should be put on my SourceForge page and maintained there. It's not that I mind doing these weekly updates, but I feel like I'm writing the same thing over and over again, where I could be offering you, the reader, more value by producing different scripts in its place. If you have a few seconds, please answer the short poll on the right hand side of the blog. If public opinion wants this project worked out on the blog, then so be it. If you're all getting sick of these updates, I can move this to my SourceForge page, as a project, and produce more original content for our Monday script posts.

For this week's Monday Linux/Unix bash shell script continuation, we're following up on our script from last week. If you liked that one, please revisit last week's cabletv.sh script, as this one introduces some features which (while nice) introduce a bit of extra run time. This week's improvements include making the script output more readable (Thanks to Michael Seeley for this suggestion) and, finally, fixing the am/pm (or 12 to 1) hump that would throw the results out of chronological order. As usual, please skip the following paragraph, unless you're interested in all the other Monday scripts we've cranked out to date.

Our previous web-based Bash scripts, in backward chronological order, include our posts on accessing Wikipedia, accessing the Farmer's Almanac, accessing the International Dictionary, checking out the world's weather, spewing out famous quotations on pretty much any subject, doing encyclopedia lookups, accessing the online Thesaurus, translating between different languages and, of course, using the online dictionary.

This week's improvements don't involve any actual extra functionality (as the, still in progress, configuration file loading and creation process will), but they do make things look better and read more sensibly, although this comes at a cost (see below).

The first improvement is in the readability of the script. Now, when you get your results, you'll be able to easily differentiate the channels from the program listings. The picture below shows a comparison of the older version (at the top) and the updated version (at the bottom):

Click on the picture below and be sure to have on your 3D specs ready ;)

old and new listings displays

The other improvements gets us over quiet a few errors involving the zero hour, incorrect times being auto-generated and ensuring correct chronological sorting of listings when crossing from 12 to 1 (am/pm or pm/am):

Click on the pictures below and enjoy the sensible passage of time ;)

chronological tv listings

Of course, getting the listings in chronological order has imposed a stiff penalty (although I'm sure it's because of the way in which I implemented it). Leaving everything in Unix time actually speeds up the sort process, but then running awk's strftime() call on each entry as it's printed has (to my way of thinking) disastrous repercussions. Of course, I'll be spending some time working this out. If you run "time" against the old version of the script (assuming my lineup with approximately 8 or 900 channels), the old version ran at this speed to produce all results in the maximum time frame (3 hours):

real 0m27.846s
user 0m6.835s
sys 0m4.725s


With all the extra frills, that number (for the exact same output - although with the fixes in place), runs at this pace:

real 3m30.538s
user 0m44.129s
sys 1m17.421s


a full 3 minutes longer! I've narrowed it down to the repetitive strftime() calls, and you can see that most of the processing time is in "sys," which means I'm forcing my kernel to work harder than it should. The implementation of date conversion in awk is the actual issue, and I'll need to convert that to simpler time-conversion via the Gnu date command, although this causes some issues with formatting that need to be worked out. I figured you'd want something that worked (if even more slowly) at this point, rather than something that ran fast and spewed garbage to your terminal.

And here's something that might make you feel better about the 3 minutes. Check out the time it takes to do every line using sed, grep and date without messing with the basic structure of the output:

real 19m31.859s
user 3m48.153s
sys 7m44.685s


Mamma mia! ;)

Until next time (possibly as a project on my SourceForge page :)

Cheers,


Creative Commons License


This work is licensed under a
Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License

#!/bin/bash

#
# cabletv.sh - Get your local regular and HD Tv listings
#
# 2008 - Mike Golvach - eggi@comcast.net
#
# Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License
#
# TODO switch to enhanced getopt to deal with variable argument switches
# TODO add config file
# TODO check for bad provider code
# TODO rewrite unix to "pretty" time conversion to speed up returns
# ------------------------------
# DONE End Display Time For Previous day when start time is in zero hour
# DONE check for bad zip code
# DONE added optional time increments - up to 3 hours - for display
# DONE tightened up code using indirect variable references
# DONE date may now be entered in quotes numeric or alpha - e.g. 11/9/08 11/09/2008 "Nov 9 2008" "November 9th 2008" etc
# DONE time may now be entered as 10pm 10 (with am/pm defaulting to system time) 10:00am 23:00 etc
# DONE added error checking for invalid date and time entry - stop before querying online guide
# DONE fixed issue with 00:xx time causing script failure
# DONE added extra error checking
# DONE added better formatting of lineup for easier readability - Thanks to Michael Seeley for the suggestion!
# DONE You have entered an invalid ZIP or postal code
# DONE am to pm or pm to am ends up not in chronological order
# DONE 12 to 1 a/p ends up not in chronological order
#

function usage()
{
echo
echo "Usage: $0 ...all options are optional [-h for help]"
echo "[-z USZipCode] [-p LocalCableOrDishProviderID]"
echo "[-t Time] [-d date] [-n to leave out HD channel info]"
echo "[-s ListingTimeSpan 1 for just now, 2 for an additional half"
echo "hour, 3, 4, 5, 6, in half hour increments, and 7 for 3 hours"
echo "...defaults to 7 since this is the online listing default]"
echo
exit 1
}

while getopts t:z:hs:nd:p: option
do
case $option in
t)
opt_time="$OPTARG"
;;
d)
opt_date="$OPTARG"
;;
z)
opt_zip="$OPTARG"
;;
n)
opt_nohd=1
;;
p)
opt_provider="$OPTARG"
;;
s)
opt_timespan="$OPTARG"
;;
h)
usage
;;
*)
usage
;;
esac
done

wget=/usr/bin/wget
pager=/usr/bin/more

if [ ! "$opt_date" ]
then
nicedate=`date "+%m/%d/%y"`
else
nicedate=$opt_date
fi

if [ ! $opt_timespan ]
then
opt_timespan=7
elif [ $opt_timespan -lt 1 -o $opt_timespan -gt 7 ]
then
usage
fi

if [ ! $opt_time ]
then
hour=`date "+%I"`
minute=`date "+%M"`
ampm=`date "+%p"`
if [ $hour -lt 12 ]
then
if [ $minute -lt 30 ]
then
nicetime="${hour}:00 AM"
else
nicetime="${hour}:30 AM"
fi
else
if [ $minute -lt 30 ]
then
nicetime="${hour}:00 PM"
else
nicetime="${hour}:30 PM"
fi
fi
if [ $hour == "00" ]
then
cli_time=${hour}:${minute}
else
cli_time=${hour}:${minute}${ampm}
fi
else
cli_time=$opt_time
fi

date_ok=`date -d "$cli_time" +%s 2>&1`
if [[ "$date_ok" =~ "invalid date" ]]
then
echo
echo "INVALID TIME ENTERED: $opt_time"
echo "Be sure to include full am or pm"
echo "and do not add am or pm to 24 clock entries"
echo
usage
fi

ampm=`echo $opt_time|egrep 'a|p|m' 2>&1`
result2=$?
if [ $result2 -eq 0 ]
then
date_ok2=`echo $opt_time|egrep 'a|p'|grep m 2>&1`
result3=$?
if [[ $result3 -ne 0 ]]
then
echo
echo "INVALID TIME ENTERED: $opt_time"
echo "Be sure to include full am or pm"
echo "and do not add am or pm to 24 clock entries"
echo
usage
fi
fi

cli_time2=`date -d "$cli_time" +%s`
let fromtime1=$cli_time2*1000
let fromtime2=$fromtime1+1800000
let fromtime3=$fromtime2+1800000
let fromtime4=$fromtime3+1800000
let fromtime5=$fromtime4+1800000
let fromtime6=$fromtime5+1800000
let fromtime7=$fromtime6+1800000
if [ ! $opt_timespan ]
then
opt_timespan=7
fi

timespan_relay="$fromtime1"
timespan_counter=2
timespan_limit=$opt_timespan

while [ $timespan_limit -gt 1 ]
do
nextup_time=$(eval "echo \$$(echo fromtime${timespan_counter})")
timespan_relay="$timespan_relay $nextup_time"
let timespan_counter=$timespan_counter+1
let timespan_limit=$timespan_limit-1
done

from_timespan=$(eval "echo \$$(echo fromtime${opt_timespan})")
cli_time_end=`echo $from_timespan|awk '{printf("%20s",strftime("%I:%M %p - %x", $0 / 1000))}'`

if [ ! $opt_nohd ]
then
opt_nohd=0
fi

if [ ! $opt_zip ]
then
echo -n "Enter Zip Code: "
read zip_code
else
zip_code=$opt_zip
fi

$wget -nv -O - "http://tvlistings.zap2it.com/tvlistings/ZBChooseProvider.do?method=getProviders&zipcode=${zip_code}" 2>&1|sed -e :a -e 's/<[^>]*>/ /g;/</N;//ba' |sed -e '/^[ \t]*$/d' 2>&1|grep "You have entered an invalid ZIP or postal code" >/dev/null 2>&1
zip_ok=$?

if [ $zip_ok -eq 0 ]
then
echo
echo "INVALID ZIP CODE ENTERED: $opt_zip"
echo
usage
fi

if [ ! $opt_provider ]
then
$wget -nv -O - "http://tvlistings.zap2it.com/tvlistings/ZBChooseProvider.do?method=getProviders&zipcode=${zip_code}" 2>&1|sed 's/<.*&lineupId=\([^&]*\)\">/\1 \c/'|sed -e :a -e 's/<[^>]*>/ /g;/</N;//ba' |sed -e '/^[ \t]*$/d' |sed -e '1,/Choose Your Provider/{x;$!d;x;q;};x;$G' -e '/document.write/,$d' -e "s/'/'/" -e 's/&/\&/' -e 's/^[ \t]*//;s/[ \t]*$//'|sed '/\([A-Za-z0-9_][A-Za-z0-9_]*:[-X]\)/ {
N
s/ *\n/ \t/g
}'
echo "Enter Provider ID [e.g. \"IL57303:-\" \"4DTV:-\" \"IL12561:X]\""
read tv_provider
else
tv_provider=$opt_provider
fi

echo
echo "Television Listings for $nicedate from $cli_time to $cli_time_end"
echo

last=1

if [ $opt_nohd -eq 1 ]
then
for x in $timespan_relay
do
$wget -nv -O - "http://tvlistings.zap2it.com/tvlistings/ZCGrid.do?fromTimeInMillis=${x}&method=decideFwdForLineup&zipcode=${zip_code}&setMyPreference=false&lineupId=${tv_provider}" 2>&1|sed 's/<.*&sch=\([^&]*\)&[^>]*>/\1 /'|sed -e :a -e 's/<[^>]*>/ /g;/</N;//ba' |sed -e '/^[ \t]*$/d' |sed -e '1,/Forgotten password/d' -e '/isFavoritesAvailable/,$d'|sed -e '/[ECMP][SD]T/,+6d' -e "s/'/'/" -e 's/&/\&/' -e '/zc.getAdFrame/d' -e 's/^[ \t]*//;s/[ \t]*$//'| sed -n '/^[0-9][0-9]*$/,+2p'|sed 's/^\([0-9][0-9]*\)$/\n\1/'|sed '/^[0-9][0-9]*$/ {
N
N
s/ *\n/\t/g
}'|awk '{ if ( $1 ~ /^[0-9]/ ) {printf("%-4s %-8s ", $1,$2);for (i=4;i<NF+1;i++) {printf("%s ", $i)};printf("*** Start - %s", $3);print ""}}'
done|sort -t'-' -k1,1n -k2,2n | uniq|while read one two;do if [ $one -eq $last ]; then echo "$one $two"|awk '{aline=$NF;$NF="";printf("%s %s\n", $0, strftime("%x - %I:%M %p", aline/1000)) }'|while read one two three;do echo " $three";done;last=$one;else echo "$one $two"|awk '{aline=$NF;$NF="";printf("%s %s\n", $0, strftime("%x - %I:%M %p", aline/1000))}'|while read one two three;do echo " $one $two";echo " $three";done;last=$one;fi;done|$pager
else
for x in $timespan_relay
do
$wget -nv -O - "http://tvlistings.zap2it.com/tvlistings/ZCGrid.do?fromTimeInMillis=${x}&method=decideFwdForLineup&zipcode=${zip_code}&setMyPreference=false&lineupId=${tv_provider}" 2>&1|sed 's/<.*&sch=\([^&]*\)&[^>]*>/\1 /'|sed -e :a -e 's/<[^>]*>/ /g;/</N;//ba' |sed -e '/^[ \t]*$/d' |sed -e '1,/Forgotten password/d' -e '/isFavoritesAvailable/,$d'|sed -e '/[ECMP][SD]T/,+6d' -e "s/'/'/" -e 's/&/\&/' -e '/zc.getAdFrame/d' -e 's/^[ \t]*//;s/[ \t]*$//'| sed -n '/^[0-9][0-9]*\.*[0-9]*$/,+2p'|sed -e 's/^\([0-9][0-9]*\.*[0-9]*\)$/\n\1/'|sed '/^[0-9][0-9]*\.*[0-9]*$/ {
N
N
s/ *\n/\t/g
}'|awk '{ if ( $1 ~ /^[0-9]/ ) {printf("%-4s %-8s ", $1,$2);for (i=4;i<NF+1;i++) {printf("%s ", $i)};printf("*** Start - %s", $3);print ""}}'
done|sort -t'-' -k1,1n -k2,2n | uniq|while read one two three;do three_one=`echo "$three"|sed -e 's/.*\*\*\* Start - //' -e 's/^[ \t]*//;s/[ \t]*$//'`; let three_two=$three_one/1000; three_three=`echo "$three"|sed 's/^\(.*\*\*\* Start - \).*/\1/'`;three_four=`echo $three_two|awk '{printf("%s", strftime("%x - %I:%M %p", $0)) }'`;if [ $one -eq $last ];then echo " $three_three $three_four";last=$one;else echo "$one $two";echo " $three_three $three_four";last=$one;fi;done|$pager
fi

exit 0


, Mike




Please note that this blog accepts comments via email only. See our Mission And Policy Statement for further details.