A few days ago I blogged about my plans to make RSS Bandit a desktop client for Google Reader. As part of that process I needed to verify that it is possible to programmatically interact with Google Reader from a desktop client in a way that provides a reasonable user experience. To this end, I wrote a command line client in IronPython based on the documentation I found at the pyrfeed Website.
The command line client isn't terribly useful on its own as a way to read your feeds but it might be useful for other developers who are trying to interact with Google Reader programmatically who would learn better from code samples than reverse engineered API documentation.
Enjoy...
PS: Note the complete lack of error handling. I never got a hang of error handling in Python let alone going back and forth between handling errors in Python vs. handling underlying .NET/CLR errors.
import sys from System import * from System.IO import * from System.Net import * from System.Text import * from System.Globalization import DateTimeStyles import clr clr.AddReference("System.Xml") from System.Xml import * clr.AddReference("System.Web") from System.Web import * ################################################################# # # USAGE: ipy greader.py <Gmail username> <password> <path-to-directory-for-storing-feeds> # # username & password are required # feed directory location is optional, defaults to C:\Windows\Temp\ ################################################################# #API URLs auth_url = rhttps://www.google.com/accounts/ClientLogin?continue=http://www.google.com&service=reader&source=Carnage4Life&Email=%s&Passwd=%s feed_url_prefix = rhttp://www.google.com/reader/atom/ api_url_prefix = rhttp://www.google.com/reader/api/0/ feed_cache_prefix = r"C:\\Windows\Temp\\" add_url = r"http://www.google.com/reader/quickadd" #enumerations (add_label, remove_label) = range(1,3) class TagList: """Represents a list of the labels/tags used in Google Reader""" def __init__(self, userid, labels): self.userid = userid self.labels = labels class SubscriptionList: """Represents a list of RSS feeds subscriptions""" def __init__(self, modified, feeds): self.modified = modified self.feeds = feeds class Subscription: """Represents an RSS feed subscription""" def __init__(self, feedid, title, categories, firstitemmsec): self.feedid = feedid self.title = title self.categories = categories self.firstitemmsec = firstitemmsec def MakeHttpPostRequest(url, params, sid): """Performs an HTTP POST request to a Google service and returns the results in a HttpWebResponse object""" req = HttpWebRequest.Create(url) req.Method = "POST" SetGoogleCookie(req, sid) encoding = ASCIIEncoding(); data = encoding.GetBytes(params) req.ContentType="application/x-www-form-urlencoded" req.ContentLength = data.Length newStream=req.GetRequestStream() newStream.Write(data,0,data.Length) newStream.Close() resp = req.GetResponse() return resp def MakeHttpGetRequest(url, sid): """Performs an HTTP GET request to a Google service and returns the results in an XmlDocument""" req = HttpWebRequest.Create(url) SetGoogleCookie(req, sid) reader = StreamReader(req.GetResponse().GetResponseStream()) doc = XmlDocument() doc.LoadXml(reader.ReadToEnd()) return doc def GetToken(sid): """Gets an edit token which is needed for any edit operations using the Google Reader API""" token_url = api_url_prefix + "token" req = HttpWebRequest.Create(token_url) SetGoogleCookie(req, sid) reader = StreamReader(req.GetResponse().GetResponseStream()) return reader.ReadToEnd() def MakeSubscription(xmlNode): """Creates a Subscription class out of an XmlNode that was obtained from the feed list""" id_node = xmlNode.SelectSingleNode("string[@name='id']") feedid = id_node and id_node.InnerText or '' title_node = xmlNode.SelectSingleNode("string[@name='title']") title = title_node and title_node.InnerText or '' fim_node = xmlNode.SelectSingleNode("string[@name='firstitemmsec']") firstitemmsec = fim_node and fim_node.InnerText or '' categories = [MakeCategory(catNode) for catNode in xmlNode.SelectNodes("list[@name='categories']/object")] return Subscription(feedid, title, categories, firstitemmsec) def MakeCategory(catNode): """Returns a tuple of (label, category id) from an XmlNode representing a feed's labels that was obtained from the feed list""" id_node = catNode.SelectSingleNode("string[@name='id']") catid = id_node and id_node.InnerText or '' label_node = catNode.SelectSingleNode("string[@name='label']") label = label_node and label_node.InnerText or '' return (label, catid) def AuthenticateUser(username, password): """Authenticates the user and returns a username/password combination""" req = HttpWebRequest.Create(auth_url % (username, password)) reader = StreamReader(req.GetResponse().GetResponseStream()) response = reader.ReadToEnd().split('\n') for s in response: if s.startswith("SID="): return s[4:] def SetGoogleCookie(webRequest, sid): """Sets the Google authentication cookie on the HttpWebRequest instance""" cookie = Cookie("SID", sid, "/", ".google.com") cookie.Expires = DateTime.Now + TimeSpan(7,0,0,0) container = CookieContainer() container.Add(cookie) webRequest.CookieContainer = container def GetSubscriptionList(feedlist, sid): """Gets the users list of subscriptions""" feedlist_url = api_url_prefix + "subscription/list" #download the JSON-esque XML feed list doc = MakeHttpGetRequest(feedlist_url, sid) #create subscription nodes feedlist.feeds = [MakeSubscription(node) for node in doc.SelectNodes("/object/list[@name='subscriptions']/object")] feedlist.modified = False def GetTagList(sid): """Gets a list of the user's tags""" taglist_url = api_url_prefix + "tag/list" doc = MakeHttpGetRequest(taglist_url, sid) #get the user id needed for creating new labels from Google system tags userid = doc.SelectSingleNode("/object/list/object/string[contains(string(.), 'state/com.google/starred')]").InnerText userid = userid.replace("/state/com.google/starred", ""); userid = userid[5:] #get the user-defined labels tags = [node.InnerText.Replace("user/" + userid + "/label/" ,"") for node in doc.SelectNodes("/object/list[@name='tags']/object/string[@name='id']") if node.InnerText.IndexOf( "/com.google/") == -1 ] return TagList(userid, tags) def DownloadFeeds(feedlist, sid): """Downloads each feed from the subscription list to a local directory""" for feedinfo in feedlist.feeds: unixepoch = DateTime(1970, 1,1, 0,0,0,0, DateTimeKind.Utc) oneweek_ago = DateTime.Now - TimeSpan(7,0,0,0) ifmodifiedsince = oneweek_ago - unixepoch feed_url = feed_url_prefix + feedinfo.feedid + "?n=25&r=o&ot=" + str(int(ifmodifiedsince.TotalSeconds)) continuation = True continuation_token = '' feedDoc = None while True: print "Downloading feed at %s" % (feed_url + continuation_token) doc = MakeHttpGetRequest(feed_url + continuation_token, sid) continuation_node = doc.SelectSingleNode("//*[local-name()='continuation']") continuation_token = continuation_node and ("&c=" + continuation_node.InnerText) or '' if feedDoc is None: feedDoc = doc else: for node in doc.SelectNodes("//*[local-name()='entry']"): node = feedDoc.ImportNode(node, True) feedDoc.DocumentElement.AppendChild(node) if continuation_token == '': break print "Saving %s" % (feed_cache_prefix + feedinfo.title + ".xml") feedDoc.Save(feed_cache_prefix + feedinfo.title + ".xml") def ShowSubscriptionList(feedlist, sid): """Displays the users list of subscriptions including the labels applied to each item""" if feedlist.modified: GetSubscriptionList(feedlist, sid) count = 1 for feedinfo in feedlist.feeds: print "%s. %s (%s)" % (count, feedinfo.title, [category[0] for category in feedinfo.categories]) count = count + 1 def Subscribe(url, sid): """Subscribes to the specified feed URL in Google Reader""" params = "quickadd=" + HttpUtility.UrlEncode(url) + "&T=" + GetToken(sid) resp = MakeHttpPostRequest(add_url, params, sid) if resp.StatusCode == HttpStatusCode.OK: print "%s successfully added to subscription list" % url return True else: print resp.StatusDescription return False def Unsubscribe(index, feedlist, sid): """Unsubscribes from the feed at the specified index in the feed list""" unsubscribe_url = api_url_prefix + "subscription/edit" feed = feedlist.feeds[index] params = "ac=unsubscribe&i=null&T=" + GetToken(sid) + "&t=" + feed.title + "&s=" + feed.feedid resp = MakeHttpPostRequest(unsubscribe_url, params, sid) if resp.StatusCode == HttpStatusCode.OK: print "'%s' successfully removed from subscription list" % feed.title return True else: print resp.StatusDescription return False def Rename(new_title, index, feedlist, sid): """Renames the feed at the specified index in the feed list""" api_url = api_url_prefix + "subscription/edit" feed = feedlist.feeds[index] params = "ac=edit&i=null&T=" + GetToken(sid) + "&t=" + new_title + "&s=" + feed.feedid resp = MakeHttpPostRequest(api_url, params, sid) if resp.StatusCode == HttpStatusCode.OK: print "'%s' successfully renamed to '%s'" % (feed.title, new_title) return True else: print resp.StatusDescription return False def EditLabel(label, editmode, userid, feedlist, index, sid): """Adds or removes the specified label to the feed at the specified index depending on the edit mode""" full_label = "user/" + userid + "/label/" + label label_url = api_url_prefix + "subscription/edit" feed = feedlist.feeds[index] params = "ac=edit&i=null&T=" + GetToken(sid) + "&t=" + feed.title + "&s=" + feed.feedid if editmode == add_label: params = params + "&a=" + full_label elif editmode == remove_label: params = params + "&r=" + full_label else: return resp = MakeHttpPostRequest(label_url, params, sid) if resp.StatusCode == HttpStatusCode.OK: print "Successfully edited label '%s' of feed '%s'" % (label, feed.title) return True else: print resp.StatusDescription return False def MarkAllItemsAsRead(index, feedlist, sid): """Marks all items from the selected feed as read""" unixepoch = DateTime(1970, 1,1, 0,0,0,0, DateTimeKind.Utc) markread_url = api_url_prefix + "mark-all-as-read" feed = feedlist.feeds[index] params = "s=" + feed.feedid + "&T=" + GetToken(sid) + "&ts=" + str(int((DateTime.Now - unixepoch).TotalSeconds)) MakeHttpPostRequest(markread_url, params, sid) print "All items in '%s' have been marked as read" % feed.title def GetFeedIndexFromUser(feedlist): """prompts the user for the index of the feed they are interested in and returns the index as the result of this function""" print "Enter the numeric position of the feed from 1 - %s" % (len(feedlist.feeds)) index = int(sys.stdin.readline().strip()) if (index < 1) or (index > len(feedlist.feeds)): print "Invalid index specified: %s" % feed2label_indx return -1 else: return index if __name__ == "__main__": if len(sys.argv) < 3: print "ERROR: Please specify a Gmail username and password" else: if len(sys.argv) > 3: feed_cache_prefix = sys.argv[3] SID = AuthenticateUser(sys.argv[1], sys.argv[2]) feedlist = SubscriptionList(True, []) GetSubscriptionList(feedlist, SID) taglist = GetTagList(SID) options = "***Your options are (f)etch your feeds, (l)ist your subscriptions, (s)ubscribe to a new feed, (u)nsubscribe, (m)ark read , (r)ename, (a)dd a label to a feed, (d)elete a label from a feed or (e)xit***" print "\n" while True: print options cmd = sys.stdin.readline() if cmd == "e\n": break elif cmd == "l\n": #list subscriptions ShowSubscriptionList(feedlist, SID) elif cmd == "s\n": #subscribe to a new feed print "Enter url: " new_feed_url = sys.stdin.readline().strip() success = Subscribe(new_feed_url, SID) if feedlist.modified == False: feedlist.modified = success elif cmd == "u\n": #unsubscribe from a feed feed2remove_indx = GetFeedIndexFromUser(feedlist) if feed2remove_indx != -1: success = Unsubscribe(feed2remove_indx-1, feedlist, SID) if feedlist.modified == False: feedlist.modified = success elif cmd == "r\n": #rename a feed feed2rename_indx = GetFeedIndexFromUser(feedlist) if feed2rename_indx != -1: print "'%s' selected" % feedlist.feeds[feed2rename_indx -1].title print "Enter the new title for the subscription:" success = Rename(sys.stdin.readline().strip(), feed2rename_indx-1, feedlist, SID) if feedlist.modified == False: feedlist.modified = success elif cmd == "f\n": #fetch feeds feedlist = DownloadFeeds(feedlist, SID) elif cmd == "m\n": #mark all items as read feed2markread_indx = GetFeedIndexFromUser(feedlist) if feed2markread_indx != -1: MarkAllItemsAsRead(feed2markread_indx-1, feedlist, SID) elif (cmd == "a\n") or (cmd == "d\n"): #add/remove a label on a feed editmode = (cmd == "a\n") and add_label or remove_label feed2label_indx = GetFeedIndexFromUser(feedlist) if feed2label_indx != -1: feed = feedlist.feeds[feed2label_indx-1] print "'%s' selected" % feed.title print "%s" % ((cmd == "a\n") and "Enter the new label:" or "Enter the label to delete:") label_name = sys.stdin.readline().strip() success = EditLabel(label_name, editmode, taglist.userid, feedlist, feed2label_indx-1, SID) if feedlist.modified == False: feedlist.modified = success else: print "Unknown command"
Now Playing: DJ Drama - Cannon (Remix) (Feat. Lil Wayne, Willie The Kid, Freeway And T.I.)