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.)