Table of Contents
What is it?
The other day my wife was lamenting the fact that the calendar for the City Pages, http://www.citypages.com/, (here in Minneapolis) thoroughly sucks, from any kind of usability standpoint. Indeed, it does. It's impossible to just glance at what is coming up in the next month or so. Instead, you have to slog through each day's listings and scroll down a page containing the full listings just to see what's happening on a given day. This isn't so bad if you are interested in what is happening on a particular day, but it's horrible for just seeing what shows are coming to town.
I decided to take it upon myself to bang out a quick script that would parse the daily rss feeds and convert them to iCal VEVENTS in a VCALENDAR. Once you have that, you can import it into any calendar app that understands iCal format (I tested with Google calendar). Then, you'll get a simple daily listing of all the events taking place that day at a glance and you can just click on the summary for the description and link to the main page.
Do note that this is a simple script that I banged out in a short amount of time to accomplish a simple task. It works well for that task, but I'm sure this could be taken to another level in terms of automation and such. I leave that as an exercise for others.
The Script
You can download the script by clicking here.
Here is the full source:
#!/usr/bin/env python # $Id: cpc2ical.py 312 2010-09-30 16:52:54Z jay $ # Copyright Jason Deiman 2010 # # This script's purpose is to convert the crappy City Pages "calendar" to # an ical calendar file for a given month. This was tested against a Google # calendar. # # This script requires the following non-standard libraries: # # python-feedparser: http://www.feedparser.org/ (apt-get install # python-feedparser) # from datetime import datetime , timedelta , tzinfo from optparse import OptionParser from hashlib import sha1 from uuid import uuid4 import time , sys , re try: import feedparser except: print >> sys.stderr , 'Could not import feedparser. You can download ' \ 'it from http://www.feedparser.org/ or if you are on a debian based ' \ 'machine, just "sudo apt-get install python-feedparser"' sys.exit(1) CPBASEURL = 'http://www.citypages.com/syndication/events/date:' RE_YM = re.compile('^\d{4}-\d{2}$') class UTC(tzinfo): """ UTC timezone for the datetime stuff """ def utcoffset(self , dt): return timedelta(0) def tzname(self , dt): return 'UTC' def dst(self , dt): return timedelta(0) class Vcal(object): """ This is just a structure to hold all the boiler-plate vcal stuff. I'm not using the icalendar stuff for this since it's stuff that's not going to change much """ def __init__(self): self.items = [ ('BEGIN' , 'VCALENDAR') , ('PRODID' , '-//Splitstreams//City Pages Calendar//EN') , ('VERSION' , '2.0') , ('CALSCALE' , 'GREGORIAN') , ('METHOD' , 'PUBLISH') , ('X-WR-CALNAME' , 'City Pages') , ('X-WR-TIMEZONE' , 'America/Chicago') , ('BEGIN' , 'VTIMEZONE') , ('TZID' , 'America/Chicago') , ('X-LIC-LOCATION' , 'America/Chicago') , ('BEGIN' , 'DAYLIGHT') , ('TZOFFSETFROM' , '-0600') , ('TZOFFSETTO' , '-0500') , ('TZNAME' , 'CDT') , ('DTSTART' , '19700308T020000') , ('RRULE' , 'FREQ=YEARLY;BYMONTH=3;BYDAY=2SU') , ('END' , 'DAYLIGHT') , ('BEGIN' , 'STANDARD') , ('TZOFFSETFROM' , '-0500') , ('TZOFFSETTO' , '-0600') , ('TZNAME' , 'CST') , ('DTSTART' , '19701101T020000') , ('RRULE' , 'FREQ=YEARLY;BYMONTH=11;BYDAY=1SU') , ('END' , 'STANDARD') , ('END' , 'VTIMEZONE') , ] def setCalName(self , name): for i , item in enumerate(self.items): if item[0] == 'X-WR-CALNAME': l = list(item) l[1] = name self.items[i] = tuple(l) def getCalName(self): for k , v in self.items: if k == 'X-WR-CALNAME': return v CalName = property(getCalName , setCalName) def getStart(self): ret = '' for k , v in self.items: ret += '%s:%s\r\n' % (k , v) return ret Start = property(getStart) def getEnd(self): return 'END:VCALENDAR\r\n' End = property(getEnd) class RssToIcal(object): dateTpl = '%Y%m%d' cpDateTpl = '%Y-%m-%d' datetimeTpl = '%Y%m%dT%H%M%SZ' def getUid(self , domain='splitstreams.com'): return '%s@%s' % (sha1(uuid4().bytes).hexdigest() , domain) def rssEntry2Vevent(self , entry , dt): end = dt + timedelta(days=1) now = datetime.utcnow() desc = entry.description.replace('\n' , '\\n').replace('\r' , '') ret = 'BEGIN:VEVENT\r\n' ret += 'DTSTART;VALUE=DATE:%s\r\n' % dt.strftime(self.dateTpl) ret += 'DTEND;VALUE=DATE:%s\r\n' % end.strftime(self.dateTpl) ret += 'DTSTAMP:%s\r\n' % now.strftime(self.datetimeTpl) ret += 'UID:%s\r\n' % self.getUid() ret += 'CREATED:%s\r\n' % now.strftime(self.datetimeTpl) ret += 'DESCRIPTION:%s\\n\\n%s\r\n' % (entry.link , desc) ret += 'LOCATION:\r\n' ret += 'SEQUENCE:0\r\n' ret += 'STATUS:CONFIRMED\r\n' ret += 'SUMMARY:%s\r\n' % entry.title ret += 'TRANSP:TRANSPARENT\r\n' ret += 'END:VEVENT\r\n' return ret def convert(self , year , month): """ This takes a numeric year and month and finds all the city pages events for that month and yields a list of Vevent strings """ year , month = (int(year) , int(month)) delta = timedelta(days=1) cpdt = datetime(year , month , 1 , tzinfo=UTC()) while cpdt.month == month: url = '%s%s' % (CPBASEURL , cpdt.strftime(self.cpDateTpl)) dfd = feedparser.parse(url) for e in dfd.entries: yield self.rssEntry2Vevent(e , cpdt) cpdt += delta def getOpts(): usage = 'Usage: %prog [options] YYYY-MM [YYYY-MM [YYYY-MM ...]]' p = OptionParser(usage=usage) p.add_option('-o' , '--output-file' , dest='outFile' , metavar='FILE' , default='-' , help='Send the calendar output to FILE instead of stdout ' '[default: STDOUT]') p.add_option('-n' , '--cal-name' , dest='calName' , metavar='CALNAME' , default='' , help='Set the calendar name to a name of your choosing. ' '[default: City Pages]') opts , args = p.parse_args() return (opts , args) def getYearMonth(dateArg): if not RE_YM.match(dateArg): print >> sys.stderr , 'Invalid month, must be in YYYY-MM ' \ 'format: %s' % a sys.exit(1) year , month = [int(i) for i in dateArg.split('-')] curYear = time.localtime().tm_year curMonth = time.localtime().tm_mon if year < curYear or year > curYear + 1: print >> sys.stderr , 'Invalid year. The year must be this, or ' \ 'next, year only: %d' % year sys.exit(2) if month < 1 or month > 12: print >> sys.stderr , 'Invalid month. It must be 1 to 12: %d' % month sys.exit(3) if month < curMonth and year == curYear: print >> sys.stderr , 'You can\'t get a calendar in the past' sys.exit(4) return (year , month) def main(): opts , args = getOpts() outfh = None if opts.outFile == '-': outfh = sys.stdout else: outfh = open(opts.outFile , 'w') vcal = Vcal() if opts.calName: vcal.CalName = opts.calName r2i = RssToIcal() events = [] ym = [] for a in args: ym.append(getYearMonth(a)) outfh.write(vcal.Start) for year , month in ym: for vev in r2i.convert(year , month): outfh.write(vev) outfh.write(vcal.End) if outfh != sys.stdout: outfh.close() if __name__ == '__main__': main()
Usage
The script is quite simple to use. If you use the –help
or -h
option, you get the limited options that you can specify on the command line.
$ ./cpc2ical.py -h Usage: cpc2ical.py [options] YYYY-MM [YYYY-MM [YYYY-MM ...]] Options: -h, --help show this help message and exit -o FILE, --output-file=FILE Send the calendar output to FILE instead of stdout [default: STDOUT] -n CALNAME, --cal-name=CALNAME Set the calendar name to a name of your choosing. [default: City Pages]
Essentially, you just specify a year and month, or a number of them, to generate. In this example, I'm generating an iCal for the months of October and November in 2010 and I'm outputting everything into a file called city_pages.ical
:
$ ./cpc2ical.py -o city_pages.ical 2010-10 2010-11
That's about it. NOTE: This will take a while to run so don't CTRL-C out of it prematurely! Once you have your file, just import it into a calendar app.
Importing into Google Calendar
This will be a very short description of how to import this info into a Google Calendar (as of September of 2010).
- Point your browser at http://google.com/calendar and log in
- In the top, right corner, click on
Settings
→Calendar settings
- Now click on the
Calendars
tab in the top, left under theCalendar Settings
heading (theGeneral
tab is selected by default) - Click the
Import calendar
link in the middle of the page - A javascript window should pop up allowing you to select your calendar file,
city_pages.ical
if you are using the example in the previous section - Select the calendar you wish to import this into. Personally, I created a separate calendar just for the city pages stuff.
- Click the
Import
button and wait for a bit and you should get a message telling you the number of events that were imported. - Go back to your calendar(s) and you should see listings for all events.
Future TODO
Here are some things that I may do in the future, but would also be a good exercise for others.
- Parse the description and create better VEVENTS for things which are ongoing (spanning many days)
- Parse the description for times and actually create VEVENTS that are not just simply “full day” events, but instead exist within the time period where the event actually takes place.