brian has been a Perl user since 1994. He is founder of the first Perl Users Group, NY.pm, and Perl Mongers, the Perl advocacy organization. He has been teaching Perl through Stonehenge Consulting for the past five years, and has been a featured speaker at The Perl Conference, Perl University, YAPC, COMDEX, and Builder.com. Contact brian at [email protected].
Instead of having a long list of bookmarks, I wanted to create a web page with links to the Internet radio shows that I listen to each day. It turned out not to be as simple as I thought. I had to find the real links to the programs so I didn't have to go through JavaScript redirection hacks, and I ended up displaying a month's worth of links on the same page. Working with dates tends to be tedious and annoying, but Date::Simple and HTML::Calendar::Simple made it, well, simple.
Building the Links
I had started with a script to create a page of the current day's links, but then I wanted links to the previous day, too. Then I wanted the previous week. Then I decided to display the entire month. Like a lot of small programs, this one got a lot bigger. Doing all this myself is something I wanted to avoid, so I looked around CPAN and found HTML::Calendar::Simple. Perfect: It would make the calendar and allow me to add linked text to each day's cell. I wouldn't have to worry about breaking up a month into weeks or working with months that start in the middle of the week.
Before I could create HTML links, I needed to figure out what they were. Most of the shows I want to listen to are on National Public Radio (NPR), which used to link directly to their Real Audio streams. Now they go through some JavaScript magic. I don't want to click on several links to listen to a show, and I don't want to remind NPR to ask me if I want to use RealPlayer or Windows Media Player every time. I just want to listen to the show.
With a little digging through the NPR web site, I discovered how to make the actual links. I examined the JavaScript functions and the CGI parameters, then turned that same logic into the npr_ram_href() subroutine in my npr_calendar script (Listing 1). The subroutine needs the date, along with a "program code" that NPR assigns to each show. The program code is the first argument and the date, as a Date::Simple object, is the second argument. Since NPR uses this same system for all of its shows, I can use this for all of the NPR shows I want to listen to. I only need to tell the subroutine which show I want, using its program code, and the date I want, which is usually the current day, but sometimes the day before if I missed a show.
I also like to listen to Marketplace from Public Radio International (PRI), which does things a bit differently. I still need the date, but instead of calling the URL directly, I call a CGI script that does it for me. The Marketplace.org server sends me a SMIL file (a Real media playlist file) that my browser, Firefox, does not like, so I use the CGI script to fix it before I send it back to my browser. All of that logic shows up in the pri_ram_href() subroutine.
Now that I know how to make the links, I need to figure out how to put them into the calendar. Some of the shows play only on weekdays, and some on the weekend. I don't want to simply put every link in every cell: Each day should have only links to that day's shows.
Eight Days a Week
After trying a couple things, I settled on using bit fields to remember which days of the week that each show plays. I had started with a lookup table, and then I changed that to an array, but the code to look up values dominated the script. Since there are seven days in a week, I can use 1 byte to represent the whole week, 1 bit per day, with a day left over in case you are the Beatles. Sunday is 0, Monday is 1, and so on. With a bit field, the lookup is just a bit operation.
On lines 23-25 of Listing 1, I create the bit field for shows that play on weekdays. The 0b numeric format introduced in Perl 5.6 helps out by allowing me to write out binary numbers as literals, and as long as I remember to read from right to left, I can use a bit field to represent the days of the week. I added underscores between Sunday and Monday and between Friday and Saturday. Perl ignores the underscores, so they are really just there to help me read the code. The $saturday variable has the high bit set (well, the seventh bit), and $sunday has the low bit set. The $weekday variable has all of the middle bits set. Later, when I need to compare a day of the week to one of these variables, I just use bit operators.
I have most things in place for creating a playlist description. I use an array to represent the shows. Each show is an array element that is an anonymous hash. The first element in the hash is the program code, the second element is the appropriate bit field, and the last element is the subroutine reference to create the right reference for that show.
Liness 36-38 set up the basic calendar. I use the today() function from Date::Simple to get today's date. I could just as easily use localtime() here (although I need to add 1900 to the year and 1 to the month), but I already need Date::Simple, and it looks much nicer than the localtime() gymnastics. I use today() in a map {} and go through a list of methods to call on the Date::Simple object that today() returns. I assign the resulting list to $year and $month.
I run this script just after midnight at the beginning of each month, so I only generate the calendar once a month, but if I wanted to turn this into a CGI script that always dynamically generates the calendar for other months, I could get the $year and $month values from the CGI parameters just as easily.
Generating the Calendar
Once I have the year and the month, I can create the calendar object with HTML::Calendar::Simple, on line 38. Its new() method takes a hash reference with keys for the year and the month. Now I just need to add the text for each date.
I need to add links to each of the days in that month, so on line 41, I run through each day with foreach(), which I also label with "DAY" so I can refer to it easily. I need to know how many days to go through, and Date::Simple comes to the rescue again. Instead of creating my own hash that knows how many days are in each month (and also figuring out leap years), I use the days_in_month() function to figure it out. This Date::Simple stuff is pretty slick!
Inside the DAY loop, I need to do some date manipulations, so I take $year and $month, which I assigned outside the loop, along with the loop variable $day, to get a Date::Simple object using its ymd() constructor. It doesn't get much simpler than that. I immediately turn that into the day of the week value using the day_of_the_week() method. I turn that into a bit mask by left shifting by 1 the index for the day of the week. Again, Sunday is 0, Monday is 1, and so on, just like the positions in my bit fields.
On line 46, in the SHOW loop, I go through each show. On the next line, I check if the show plays on that day by AND-ing the bit mask with the second element of the show array. That operation only returns True if the bits in the $day_of_week mask are also set in $show->[1]. If the operation does not return True, I skip to the next show. Only the shows that play on that day get to continue with the loop.
On line 49, I use the program code in $show->[0] along with the date as arguments to the anonymous subroutine, which is the last element of the show array, $show->[2]. Although my shows list only has two sources for shows, I could easily add more (like "Car Talk" maybe) without making this loop more complicated.
In the next statement, on line 51, I add the link to the calendar with the daily_info() method. I pass it an anonymous hash in which I specify the day and the link text. The link text that actually shows up in the HTML has its own key (in case I want to change it), so I just use the show's code. As I go through each show, I might add more links to a single day, and I can add as many as I like.
When I have finished this short loop, I create some HTML, and from within my HERE document, I call the calendar_month() method on my $calendar object to get back an HTML table that represents the calendar. I create it inside an anonymous array that I wrapped in an array dereference. It's just a little trick to interpolate a method call. In the HTML that I wrap around the calendar, I include a reference to a stylesheet so I can make things look prettythe final result is shown in Figure 1.
That's it. It took me longer to set everything up than it did to actually make the calendar. With everything I actually did in this script, I created very little date or calendar logic myself. Most of the script dealt with creating the links for each show. The Date::Simple module handled all of the date processing, and HTML::Calendar::Simple handled most of the HTML bits. I concentrated on the bits that mattered to me: Creating direct links to the shows and listing all the shows I wanted to listen to. When those modules say "Simple," they mean it.
References
- http://www.npr.org/
- http://www.marketplace.org/
- http://search.cpan.org/dist/Date-Simple/
- http://search.cpan.org/dist/HTML-Calendar-Simple/
TPJ
#!/usr/bin/perl use strict; use warnings; use Date::Simple qw(:all); use HTML::Calendar::Simple; my $npr = sub { "http://www.npr.org/dmg/dmg.php?prgCode=" . $_[0] . "&showDate=" . $_[1]->format( "%d-%B-%Y" ) . "&segNum=&mediaPref=RM" }; my $pri = sub { "http://www.panix.com/~comdog/marketplace.cgi/marketplace.smil?" . "http://www.marketplace.org/play/audio.php?media=/" . $_[1]->format( "%Y/%m/%d" ) . "_mpp" }; my $weekdays = 0b0_11111_0; # read from right to left. low my $saturday = 0b1_00000_0; # bit it sunday, high bit is my $sunday = 0b0_00000_1; # saturday my @shows = ( [ ATC => $weekdays, $npr ], # All Things Considered [ ME => $weekdays, $npr ], # Morning Edition [ FA => $weekdays, $npr ], # Fresh Air [ WESAT => $saturday, $npr ], # Weekend Edition Saturday [ WESUN => $sunday, $npr ], # Weekend Edition Sunday [ MP => $weekdays, $pri ], # Marketplace ); my( $year, $month ) = map { today()->$_ } qw(year month); my $calendar = HTML::Calendar::Simple->new( { year => $year, month => $month } ); DAY: foreach my $day ( 1 .. days_in_month( $year, $month ) ) { my $date = ymd( $year, $month, $day ); my $day_of_week = 1 << $date->day_of_week; SHOW: foreach my $show ( @shows ) { next SHOW unless $show->[1] & $day_of_week; my $link = $show->[2]( $show->[0], $date ); $calendar->daily_info( { day => $day, $show->[0] => qq|<a href="$link">$$show[0]</a>|, } ); } } print <<"HERE"; <html><head><title>brian's NPR Listening Calendar</title> <link rel="stylesheet" type="text/css" href="npr_calendar.css" /> </head><body><h1>brian's NPR Listening Calendar</h1> @{ [ $calendar->calendar_month ] } </body></html> HEREBack to article